Параллельная библиотека задач великолепна, и я использовал ее много в последние месяцы. Однако меня что-то действительно беспокоит: тот факт, что TaskScheduler.Current
является планировщиком задач по умолчанию, а не TaskScheduler.Default
. Это абсолютно не очевидно на первый взгляд в документации и образцах.
Current
может привести к тонким ошибкам, поскольку его поведение меняется в зависимости от того, находитесь ли вы в другой задаче. Что не может быть легко определено.
Предположим, что я пишу библиотеку асинхронных методов, используя стандартный шаблон асинхронизации на основе событий для завершения сигнала в исходном контексте синхронизации точно так же, как и методы XxxAsync в .NET Framework (например, DownloadFileAsync
). Я решил использовать параллельную библиотеку задач для реализации, потому что очень просто реализовать это поведение со следующим кодом:
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
. - Захват стека вызовов имеет дополнительные затраты на производительность.
- Когда я действительно хочу, чтобы задача зависела от другой выполняемой задачи родителя, я предпочитаю явно указывать ее, чтобы облегчить чтение кода, а не полагаться на магию стека вызовов.
Я знаю, что этот вопрос может звучать довольно субъективно, но я не могу найти хороший объективный аргумент в отношении того, почему это поведение так и есть. Я уверен, что здесь что-то не хватает: почему я обращаюсь к вам.