2012-01-06 8 views
11

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

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

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

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

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

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

ответ

10

Я возьму выстрел в ответе. ;)

Вопрос 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 в this MSDN article on the subject.

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

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

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

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

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

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

Вот некоторые моменты из моей «Темы Мертвы» разговоры, что я думаю, что может помочь вам с вашим дизайном:

  • Выполните задачи на основе асинхронных принципов Pattern.
  • Поскольку ваша база кода становится более асинхронной, она станет более функциональной по своей природе (в отличие от традиционно объектно-ориентированной). Это нормально и нужно обнимать.
  • Поскольку ваша база кода становится более асинхронной, параллелизм совместно используемой памяти постепенно эволюционирует до параллелизма передачи сообщений (т. Е. ConcurrentExclusiveSchedulerPair - это новый ReaderWriterLock).
+0

Отличный ответ Стивену, но для уточнения: если ConfigureAwait (false) были использованы внутри методом async «A», то какой контекст я ожидал бы, если бы я ждал метод «A»? Путь потока, или он возобновит исходный контекст, прежде чем делать ожидаемый вызов метода «А»? –

+2

Ответ Стивена достаточно прочен. Обратите внимание, что если вы устарели на старой модели, вы всегда можете создать настраиваемую оболочку (как и для ConfigureAwait()) и использовать ее в качестве метода расширения для задачи/задачи . Например, если ваш метод расширения был вызван ResumeOn (TaskScheduler ts), тогда код может выглядеть так: Ожидать Foo (...). ResumeOn (ts); И тогда у вас есть все те же семантики планирования, что и ваш собственный код, но со всеми улучшенными качествами потока/исполнения, которые «ожидают». –

+2

@Lawrence: Каждый «слой» методов async передает свой контекст вниз, но не вверх. Поэтому, если 'A' вызывает' ConfigureAwait (false) ', то он завершит работу в пуле потоков. Затем, когда 'B' вызывает' await A() ', тогда' B' возобновится в * своем собственном * исходном контексте после 'await'. Тот факт, что 'A' заканчивается в пуле потоков, не влияет на остаток' B'. –