Библиотека параллельных задач великолепна, и я много использовал ее в последние месяцы. Однако кое-что меня действительно беспокоит: тот факт, что TaskScheduler.Current
— планировщик задач по умолчанию, а не TaskScheduler.Default
. Это совершенно не очевидно на первый взгляд ни в документации, ни в примерах.
Current
может привести к тонким ошибкам, поскольку его поведение меняется в зависимости от того, находитесь ли вы внутри другой задачи. Который не может быть определен легко.
Предположим, я пишу библиотеку асинхронных методов, используя стандартный асинхронный шаблон, основанный на событиях, чтобы сигнализировать о завершении исходного контекста синхронизации точно так же, как методы XxxAsync делают в .NET Framework (например, DownloadFileAsync
). Я решил использовать для реализации Task Parallel Library, потому что такое поведение действительно легко реализовать с помощью следующего кода:
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
после завершения операции. Ничего необычного. Он или она следует примерам, найденным повсюду в Интернете, и просто использует Task.Factory.StartNew
без указания TaskScheduler
(и нет простой перегрузки, чтобы указать его во втором параметре). Метод DoSomethingElse
отлично работает при вызове в одиночку, но как только он вызывается событием, пользовательский интерфейс зависает, поскольку TaskFactory.Current
будет повторно использовать планировщик задач контекста синхронизации из продолжения моей библиотеки.
Выяснение этого может занять некоторое время, особенно если второй вызов задачи скрыт в каком-то сложном стеке вызовов. Конечно, исправление здесь простое, если вы знаете, как все работает: всегда указывайте TaskScheduler.Default
для любой операции, которую вы ожидаете выполнить в пуле потоков. Однако, возможно, вторую задачу запускает другая внешняя библиотека, не зная о таком поведении и наивно используя StartNew
без конкретного планировщика. Я ожидаю, что этот случай будет довольно частым.
После того, как я обдумал это, я не могу понять выбор команды, пишущей TPL, использовать TaskScheduler.Current
вместо TaskScheduler.Default
по умолчанию:
- Это вообще не очевидно,
Default
не по умолчанию! И документации серьезно не хватает. - Реальный планировщик задач, используемый
Current
, зависит от стека вызовов! С таким поведением трудно поддерживать инварианты. - Указывать планировщик задач с помощью
StartNew
неудобно, так как сначала нужно указать параметры создания задачи и токен отмены, что приводит к длинным и менее читаемым строкам. Это можно облегчить, написав метод расширения или создавTaskFactory
, использующийDefault
. - Захват стека вызовов требует дополнительных затрат на производительность.
- Когда я действительно хочу, чтобы задача зависела от другой запущенной родительской задачи, я предпочитаю указывать это явно, чтобы облегчить чтение кода, а не полагаться на магию стека вызовов.
Я знаю, что этот вопрос может показаться довольно субъективным, но я не могу найти хорошего объективного аргумента в пользу того, почему такое поведение именно такое. Я уверен, что здесь что-то упускаю: именно поэтому я обращаюсь к вам.
DoSomethingElse
), предполагающий, что он будет вызываться в контексте пользовательского интерфейса? (Если это то, что вы пытаетесь сделать - это создание задач не в контексте пользовательского интерфейса) - person Damien_The_Unbeliever   schedule 23.07.2011DoSomethingElse
может выполняться в любом контексте, но в данном конкретном случае создаваемая им задача будет выполняться в контексте родительской задачи, которая сама выполняется в потоке пользовательского интерфейса, не зная об этом. Нет проблем, если использовался планировщик задачDefault
. У меня нет проблем с его указанием, но я не контролирую каждую стороннюю библиотеку, не всегда зная об этом факте. Чего я действительно не понимаю, так это почемуCurrent
используется по умолчанию со всеми этими потенциально опасными меняющимися контекстами. Хотя этот вопрос, наверное, слишком спорный. - person Julien Lebosquain   schedule 23.07.2011