Библиотека параллельных заданий великолепна, и я часто использовал ее в последние месяцы.Однако, что-то действительно беспокоит меня: тот факт, что TaskScheduler.Current
является планировщиком заданий по умолчанию, а не TaskScheduler.Default
.Это абсолютно неочевидно на первый взгляд ни в документации, ни в примерах.
Current
может привести к незначительным ошибкам, поскольку его поведение меняется в зависимости от того, находитесь ли вы в другой задаче.Что не может быть легко определено.
Предположим, я пишу библиотеку асинхронных методов, используя стандартный асинхронный шаблон, основанный на событиях, для оповещения о завершении в исходном контексте синхронизации, точно так же, как методы XxxAsync в.NET Framework (например, DownloadFileAsync
).Я решил использовать Task Parallel Library для реализации, потому что это действительно легко реализовать с помощью следующего кода:
public class MyLibrary {
public event EventHandler SomeOperationCompleted;
private void OnSomeOperationCompleted() {
var handler = SomeOperationCompleted;
if (handler != null)
handler(this, EventArgs.Empty);
}
public void DoSomeOperationAsync() {
Task.Factory
.StartNew
(
() => Thread.Sleep(1000) // simulate a long operation
, CancellationToken.None
, TaskCreationOptions.None
, TaskScheduler.Default
)
.ContinueWith
(t => OnSomeOperationCompleted()
, TaskScheduler.FromCurrentSynchronizationContext()
);
}
}
Пока что все работает хорошо.Теперь давайте вызовем эту библиотеку нажатием кнопки в приложении WPF или WinForms:
private void Button_OnClick(object sender, EventArgs args) {
var myLibrary = new MyLibrary();
myLibrary.SomeOperationCompleted += (s, e) => DoSomethingElse();
myLibrary.DoSomeOperationAsync();
}
private void DoSomethingElse() {
...
Task.Factory.StartNew(() => Thread.Sleep(5000)/*simulate a long operation*/);
...
}
Здесь человек, пишущий вызов библиотеки, решил начать новый Task
после завершения операции.Ничего необычногоОн или она следует примерам, найденным повсюду в Интернете, и просто использует Task.Factory.StartNew
, не указывая TaskScheduler
(и нет легкой перегрузки, чтобы указать его во втором параметре).Метод DoSomethingElse
отлично работает, когда вызывается один, но как только он вызывается событием, пользовательский интерфейс зависает, так как TaskFactory.Current
повторно использует планировщик задач контекста синхронизации из продолжения моей библиотеки.
Узнать, что это можетзаймет некоторое время, особенно если второй вызов задачи скрыт в некотором сложном стеке вызовов.Конечно, исправить это просто, если вы знаете, как все работает: всегда указывайте TaskScheduler.Default
для любой операции, которую вы ожидаете выполнить в пуле потоков.Однако, возможно, вторая задача запускается другой внешней библиотекой, не зная об этом, и наивно использует StartNew
без определенного планировщика.Я ожидаю, что этот случай будет довольно распространенным.
После того, как я обдумаю его, я не могу понять выбор команды, пишущей TPL, для использования TaskScheduler.Current
вместо TaskScheduler.Default
по умолчанию:
- Это совсем не очевидно,
Default
не по умолчанию!И документации серьезно не хватает. - Реальный планировщик задач, используемый
Current
, зависит от стека вызовов!С таким поведением сложно поддерживать инварианты. - Трудно указывать планировщик задач с помощью
StartNew
, поскольку сначала необходимо указать параметры создания задачи и маркер отмены, что приведет к длинным, менее читаемым строкам.Этого можно избежать, написав метод расширения или создав TaskFactory
, использующий Default
. - . Захват стека вызовов требует дополнительных затрат производительности.
- Когда я действительно хочу, чтобы задача былав зависимости от другой родительской выполняемой задачи, я предпочитаю указывать ее явно, чтобы облегчить чтение кода, а не полагаться на магию стека вызовов.
Я знаю, что этот вопрос может звучать довольно субъективно, но я не могу найтихороший объективный аргумент о том, почему это поведение так, как оно есть.Я уверен, что чего-то здесь не хватает: вот почему я обращаюсь к вам.