Async CTP - рекомендуемый подход для планирования задач

В настоящее время я работаю над в значительной степени асинхронным приложением, которое использует TAP. У каждого класса, у которого есть методы для нереста Task, также есть TaskScheduler, введенный в него. Это позволяет нам выполнять явное планирование задач, которые, как я понимаю, не так, как Microsoft идет с Async CTP.

Единственная проблема с новым подходом (неявное планирование) заключается в том, что наша предыдущая философия всегда была "мы знаем, что продолжение всегда будет определять их планировщик задач, поэтому нам не нужно беспокоиться о том, какой контекст мы завершаем задача на".

Отвлекаясь от этого, мы немного беспокоимся, потому что он работал очень хорошо с точки зрения избежания тонких ошибок потоковой передачи, потому что для каждого фрагмента кода мы видим, что кодер вспомнил, что он задумал. Если они пропустили задание планировщика задач, это ошибка.

Вопрос 1: Кто-нибудь может успокоить меня, что неявный подход - хорошая идея? Я вижу так много проблем, которые возникают в ConfigureAwait (false) и явное планирование в устаревшем/стороннем коде. Как я могу быть уверен, что мой код "ожидание" всегда работает в потоке пользовательского интерфейса, например?

Вопрос 2: Итак, предположим, что мы удаляем все TaskScheduler DI из нашего кода и начинаем использовать неявное планирование, как мы тогда устанавливаем планировщик заданий по умолчанию? Как насчет изменения планировщика в середине метода, прежде чем ждать дорогого метода, а затем снова установить его обратно?

(стр. я уже прочитал http://msmvps.com/blogs/jon_skeet/archive/2010/11/02/configuring-waiting.aspx)

Ответ 1

Я попытаюсь ответить.;)

Вопрос 1: Кто-нибудь может успокоить меня, что неявный подход - хорошая идея? Я вижу так много проблем, которые возникают в ConfigureAwait (false) и явное планирование в устаревшем/стороннем коде. Как я могу быть уверен, что мой код "ожидание" всегда работает в потоке пользовательского интерфейса, например?

Правила для ConfigureAwait(false) довольно просты: используйте его, если остальная часть вашего метода может быть запущена на threadpool и не использовать его, если остальная часть вашего метода должна выполняться в заданном контексте (например, пользовательский интерфейс контекст).

Вообще говоря, ConfigureAwait(false) должен использоваться кодом библиотеки, а не кодом UI-слоя (включая слои типа UI, такие как ViewModels в MVVM). Если метод является частично-фоновым вычислением и частично-UI-обновлениями, то его следует разделить на два метода.

Вопрос 2: Итак, если мы удалим весь TaskScheduler DI из нашего кода и начнем использовать неявное планирование, как мы тогда зададим планировщик задач по умолчанию?

async/await обычно не использует TaskScheduler; они используют концепцию "контекста планирования". Это на самом деле SynchronizationContext.Current и возвращается к TaskScheduler.Current только в том случае, если нет SynchronizationContext. Таким образом, подставляя свой собственный планировщик, можно использовать SynchronizationContext.SetSynchronizationContext. Вы можете больше узнать о SynchronizationContext в этой статье MSDN по этому вопросу.

Контекст планирования по умолчанию должен быть тем, что вам нужно почти все время, а это значит, что вам не нужно возиться с ним. Я изменяю его только при выполнении модульных тестов или для консольных программ/сервисов Win32.

Как насчет изменения планировщика на полпути через метод, прежде чем ждать дорогого метода, а затем снова установить его обратно?

Если вы хотите выполнить дорогостоящую операцию (предположительно, на threadpool), тогда await результат TaskEx.Run.

Если вы хотите изменить планировщик по другим причинам (например, concurrency), тогда await результат TaskFactory.StartNew.

В обоих случаях метод (или делегат) запускается на другом планировщике, а затем остальная часть метода возобновляется в его обычном контексте.

В идеале вы хотите, чтобы каждый метод async существовал в пределах одного контекста выполнения. Если есть разные части метода, которые нуждаются в разных контекстах, разделите их на разные методы. Единственным исключением из этого правила является ConfigureAwait(false), который позволяет запустить метод в произвольном контексте и затем вернуться к контексту threadpool для остальной части его выполнения. ConfigureAwait(false) следует рассматривать как оптимизацию (которая по умолчанию для кода библиотеки), а не как философия дизайна.

Вот некоторые моменты из моего "Thread is Dead", который, я думаю, может помочь вам в вашем дизайне:

  • Следуйте инструкциям Asynchronous Pattern на основе задач.
  • Поскольку ваша база кода становится более асинхронной, она станет более функциональной по своей природе (в отличие от традиционно объектно-ориентированной). Это нормально и должно быть охвачено.
  • По мере того как ваша база кода становится более асинхронной, shared-memory concurrency постепенно переходит к передаче сообщений concurrency (т.е. ConcurrentExclusiveSchedulerPair - новый ReaderWriterLock).