Почему TaskScheduler.Current является TaskScheduler по умолчанию?

Библиотека параллельных задач прекрасна, и я много использовал ее в последние месяцы. Однако кое-что меня действительно беспокоит: тот факт, что 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 , который использует По умолчанию .
  • Захват стека вызовов требует дополнительных затрат производительности.
  • Когда мне действительно нужна задача чтобы быть зависимым от другой родительской запущенной задачи, я предпочитаю указывать это явно, чтобы облегчить чтение кода, а не полагаться на магию стека вызовов.

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

60
задан Theodor Zoulias 5 November 2019 в 08:15
поделиться