Библиотека параллельных задач прекрасна, и я много использовал ее в последние месяцы. Однако кое-что меня действительно беспокоит: тот факт, что TaskScheduler.Current
является планировщиком задач по умолчанию, а не TaskScheduler.Default
. Это абсолютно неочевидно на первый взгляд ни в документации, ни в примерах.
Current
может приводить к незаметным ошибкам, поскольку его поведение меняется в зависимости от того, находитесь ли вы внутри другой задачи. Что не может быть легко определено.
Предположим, я пишу библиотеку асинхронных методов, используя стандартный асинхронный шаблон на основе событий, чтобы сигнализировать о завершении в исходном контексте синхронизации, точно так же, как методы XxxAsync в .NET. Framework (например, DownloadFileAsync
). Я решил использовать для реализации параллельную библиотеку задач, потому что это действительно легко реализовать с помощью следующего кода:
public class MyLibrary
{
public event EventHandler SomeOperationCompleted;
private void OnSomeOperationCompleted()
{
SomeOperationCompleted?.Invoke(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(); // trigger the event
}, TaskScheduler.FromCurrentSynchronizationContext());
}
}
Пока все работает хорошо. Теперь давайте вызовем эту библиотеку нажатием кнопки в приложении WPF или WinForms:
private void Button_OnClick(object sender, EventArgs args)
{
var myLibrary = new MyLibrary();
myLibrary.SomeOperationCompleted += (s, e) => DoSomethingElse();
myLibrary.DoSomeOperationAsync(); // call that triggers the event asynchronously
}
private void DoSomethingElse() // the event handler
{
//...
Task.Factory.StartNew(() => Thread.Sleep(5000)); // simulate a long operation
//...
}
Здесь человек, выполняющий вызов библиотеки, решил запустить новую Задачу
, когда операция завершится. Ничего необычного. Он или она следует примерам, которые можно найти повсюду в Интернете, и просто использовать Task.Factory.StartNew
без указания TaskScheduler
(и указать его во втором параметре непросто). Метод DoSomethingElse
отлично работает, когда вызывается отдельно, но как только он вызывается событием, пользовательский интерфейс зависает, поскольку TaskFactory.Current
повторно использует планировщик задач контекста синхронизации из продолжения моей библиотеки.
Выяснение этого может занять некоторое время, особенно если второй вызов задачи находится в каком-то сложном стеке вызовов. Конечно, исправить здесь просто, если вы знаете, как все работает: всегда указывайте TaskScheduler.Default
для любой операции, которую вы ожидаете выполнять в пуле потоков. Однако, возможно, вторая задача запускается другой внешней библиотекой, не зная об этом поведении и наивно используя StartNew
без определенного планировщика. Я ожидаю, что этот случай будет довольно распространенным.
После того, как я осмотрелся, я не могу понять, что команда, пишущая TPL, выбрала для использования TaskScheduler.Current
вместо TaskScheduler.Default
по умолчанию:
По умолчанию
не по умолчанию! И документации очень не хватает. Current
, зависит от стека вызовов! При таком поведении сложно поддерживать инварианты. StartNew
, поскольку сначала необходимо указать параметры создания задачи и токен отмены, что приводит к длинным, менее читаемым строкам. Этого можно избежать, написав метод расширения или создав TaskFactory
, который использует По умолчанию
. Я знаю, что этот вопрос может звучать довольно субъективно, но я не могу найти хороший объективный аргумент, поскольку почему это поведение именно такое. Я уверен, что здесь чего-то не хватает: поэтому я обращаюсь к вам.