Почему TaskScheduler.Current используется по умолчанию для TaskScheduler? - PullRequest
59 голосов
/ 23 июля 2011

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

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

Ответы [ 5 ]

18 голосов
/ 23 июля 2011

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

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

Относительно ваших конкретных проблем:

  • Я думаю, что самый простой способ запустить новое задание в указанном планировщике - new Task(lambda).Start(scheduler). Недостатком является то, что вы должны указывать аргумент типа, если задача что-то возвращает. TaskFactory.Create может определить тип для вас.
  • Вы можете использовать Dispatcher.Invoke() вместо TaskScheduler.FromCurrentSynchronizationContext().
5 голосов
/ 20 ноября 2011

[РЕДАКТИРОВАТЬ] Следующее только решает проблему с планировщиком, используемым Task.Factory.StartNew.
Однако, Task.ContinueWith имеет жестко закодированный TaskScheduler.Current.[/ EDIT]

Во-первых, доступно простое решение - см. Нижнюю часть этого поста.

Причина этой проблемы проста: существует не только планировщик задач по умолчанию (TaskScheduler.Default), но также планировщик задач по умолчанию для TaskFactory (TaskFactory.Scheduler).Этот планировщик по умолчанию может быть указан в конструкторе TaskFactory при его создании.

Однако TaskFactory за Task.Factory создается следующим образом:

s_factory = new TaskFactory();

Как выможно видеть, TaskFactory не указано;null используется для конструктора по умолчанию - лучше будет TaskScheduler.Default (в документации указано, что используется «Current», что имеет те же последствия).
Это снова приводит к реализации TaskFactory.DefaultScheduler (закрытый член):

private TaskScheduler DefaultScheduler 
{ 
   get
   { 
      if (m_defaultScheduler == null) return TaskScheduler.Current;
      else return m_defaultScheduler;
   }
}

Здесь вы должны увидеть причину такого поведения: поскольку Task.Factory не имеет планировщика задач по умолчанию, будет использоваться текущий.

Так почемуразве мы не сталкиваемся с NullReferenceExceptions тогда, когда в данный момент не выполняется ни одна Задача (т.е. у нас нет текущего TaskScheduler)?
Причина проста:

public static TaskScheduler Current
{
    get
    {
        Task internalCurrent = Task.InternalCurrent;
        if (internalCurrent != null)
        {
            return internalCurrent.ExecutingTaskScheduler;
        }
        return Default;
    }
}

TaskScheduler.Current по умолчанию TaskScheduler.Default.

Я бы назвал это очень неудачной реализацией.

Однако есть простое исправление: мы можем просто установить значение по умолчанию TaskScheduler из Task.Factory в TaskScheduler.Default

TaskFactory factory = Task.Factory;
factory.GetType().InvokeMember("m_defaultScheduler", BindingFlags.SetField | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly, null, factory, new object[] { TaskScheduler.Default });

Я надеюсь, что смогу помочь с ответом, хотя уже довольно поздно: -)

4 голосов
/ 28 августа 2013

Вместо Task.Factory.StartNew()

рассмотрите возможность использования: Task.Run()

Это всегда будет выполняться в потоке пула потоков.У меня была та же проблема, описанная в вопросе, и я думаю, что это хороший способ решения этой проблемы.

См. Эту запись в блоге: http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx

2 голосов
/ 27 февраля 2013

Это совсем не очевидно, По умолчанию не по умолчанию!И документация серьезно отсутствует.

Default по умолчанию, но это не всегда Current.

Как уже отвечали другие, если вы хотите, чтобы задача выполнялась в пуле потоков, вам нужно явно установить планировщик Current, передав планировщик Default в TaskFactory или StartNew method.

Так как ваш вопрос касался библиотеки, я думаю, что ответ заключается в том, что вы не должны делать ничего, что изменит планировщик Current, который виден кодом вне вашей библиотеки.Это означает, что вы не должны использовать TaskScheduler.FromCurrentSynchronizationContext(), когда вы вызываете событие SomeOperationCompleted.Вместо этого сделайте что-то вроде этого:

public void DoSomeOperationAsync() {
    var context = SynchronizationContext.Current;
    Task.Factory
        .StartNew(() => Thread.Sleep(1000) /* simulate a long operation */)
        .ContinueWith(t => {
            context.Post(_ => OnSomeOperationCompleted(), null);
        });
}

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

0 голосов
/ 31 мая 2013

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

К счастью, это весь код, которым я владею, поэтому я могу это исправить, убедившись, что мой код указывает TaskScheduler.Default при запуске новых задач, но если вам не так повезло, мое предложениебыло бы использовать Dispatcher.BeginInvoke вместо использования планировщика пользовательского интерфейса.

Итак, вместо:

var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
var task = Task.Factory.StartNew(() => Thread.Sleep(5000));
task.ContinueWith((t) => UpdateUI(), uiScheduler);

Попробуйте:

var uiDispatcher = Dispatcher.CurrentDispatcher;
var task = Task.Factory.StartNew(() => Thread.Sleep(5000));
task.ContinueWith((t) => uiDispatcher.BeginInvoke(new Action(() => UpdateUI())));

Это немного менее читабельнохотя.

...