Когда должна быть вызвана задача Task.ContinueWed с TaskScheduler.Current в качестве аргумента?

Мы используем этот фрагмент кода из StackOverflow для создания задачи, которая завершается сразу после успешного завершения первого набора задач. Из-за нелинейного характера его выполнения async/await не является действительно жизнеспособным, и поэтому этот код использует ContinueWith(). Она не определяет TaskScheduler, хотя, которого ряд из источников затронувших может быть опасно, потому что он использует TaskScheduler.Current когда большинство разработчиков обычно ожидают TaskScheduler.Default поведения от продолжений.

Преобладающая мудрость заключается в том, что вы всегда должны передавать явный TaskScheduler в ContinueWith. Тем не менее, я не видел четкого объяснения того, когда различные TaskSchedulers будут наиболее подходящими.

Каков конкретный пример случая, когда было бы лучше передать TaskScheduler.Current в ContinueWith(), а не TaskScheduler.Default? При принятии этого решения необходимо придерживаться правил?

Для контекста, здесь фрагмент кода, который я имею в виду:

public static Task<T> FirstSuccessfulTask<T>(IEnumerable<Task<T>> tasks)
{
    var taskList = tasks.ToList();
    var tcs = new TaskCompletionSource<T>();
    int remainingTasks = taskList.Count;
    foreach(var task in taskList)
    {
        task.ContinueWith(t =>
            if(task.Status == TaskStatus.RanToCompletion)
                tcs.TrySetResult(t.Result));
            else
                if(Interlocked.Decrement(ref remainingTasks) == 0)
                    tcs.SetException(new AggregateException(
                        tasks.SelectMany(t => t.Exception.InnerExceptions));
    }
    return tcs.Task;
}

Ответ 1

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

Рассмотрим следующие примеры:

Task ContinueWithUnknownAction(Task task, Action<Task> actionOfTheUnknownNature)
{
    // We know nothing about what the action do, so we decide to respect environment
    // in which current function is called
    return task.ContinueWith(actionOfTheUnknownNature, TaskScheduler.Current);
}

int count;
Task ContinueWithKnownAction(Task task)
{
    // We fully control a continuation action and we know that it can be safely 
    // executed by thread pool thread.
    return task.ContinueWith(t => Interlocked.Increment(ref count), TaskScheduler.Default);
}

Func<int> cpuHeavyCalculation = () => 0;
Action<Task> printCalculationResultToUI = task => { };
void OnUserAction()
{
    // Assert that SynchronizationContext.Current is not null.
    // We know that continuation will modify an UI, and it can be safely executed 
    // only on an UI thread.
    Task.Run(cpuHeavyCalculation)
        .ContinueWith(printCalculationResultToUI, TaskScheduler.FromCurrentSynchronizationContext());
}

Ваш FirstSuccessfulTask() вероятно, является примером, где вы можете использовать TaskScheduler.Default, потому что экземпляр делегирования продолжения можно безопасно выполнить в пуле потоков.

Вы также можете использовать персонализированный планировщик задач для реализации собственной логики планирования в своей библиотеке. Например, см. Страницу Планировщик на веб-сайте системы Orleans.

Для получения дополнительной информации проверьте:

Ответ 2

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

BackgroundWorker был первым, скромной и разумной попыткой скрыть осложнения. Но никто не понимает, что рабочий бежит по потоку, поэтому никогда не должен занимать I/O. Все это ошибаются, но не многие замечают. И, забыв проверить e.Error в событии RunWorkerCompleted, скрытие исключений в потоковом коде является универсальной проблемой с оболочками.

Асинхронный/ожидающий шаблон является последним, он делает его действительно легким. Но он сочиняет необычайно плохо, асинговые черепахи полностью вниз, пока вы не доберетесь до Майн(). Им пришлось исправить это в версии С# версии 7.2, потому что все застряли на ней. Но не исправление жесткой проблемы ConfigureAwait() в библиотеке. Это полностью предвзято по отношению к библиотечным авторам, зная, что они делают, примечательно то, что многие из них работают на Microsoft и возились с WinRT.

Класс Task преодолел разрыв между ними, его цель дизайна заключалась в том, чтобы сделать его очень сложным. Хороший план, они не могли предсказать, как программисты собираются его использовать. Но также ответственность, вдохновляющая программистов на ContinueWith() на шторм, чтобы склеить задачи вместе. Даже если это не имеет смысла, потому что эти задачи просто выполняются последовательно. Примечательно, что они даже добавили оптимизацию, чтобы гарантировать, что продолжение выполняется в одном потоке, чтобы избежать накладных расходов коммутатора контекста. Хороший план, но создавая неразрешимую проблему, для которой назван этот веб-сайт.

Так что да, совет, который вы видели, был хорошим. Задача полезна для решения асинхронности. Общей проблемой, с которой вам приходится иметь дело, когда услуги переходят в "облако" и латентность, становятся детали, которые вы больше не можете игнорировать. Если вы продолжаете этот символ(), то вы всегда заботитесь о конкретном потоке, который выполняет продолжение. Предоставленный TaskScheduler, низкий коэффициент, что он не тот, который предоставляется FromCurrentSynchronizationContext(). Вот как произошло асинхронное/ожидание.

Ответ 3

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

Если вы используете TaskScheduler.Default, он всегда будет обращаться к ThreadPool.

Единственная причина, по которой вы бы использовали TaskScheduler.Current:

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

От Стивена Клири сообщение Продолжить С Опасным, слишком.

Вот еще одно объяснение от Стивена Тууба на его блоге MSDN.

Ответ 4

Я, конечно же, не думаю, что смогу дать ответ пуленепробиваемого, но я дам свои пять центов.

Каков конкретный пример случая, когда было бы лучше передать TaskScheduler.Current в ContinueWith(), а не TaskScheduler.Default?

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

Теперь ваш api должен запросить некоторую базу данных и заказать результаты, но эти результаты - миллионы, поэтому вы решили сделать это с помощью Merge Sort (делить и побеждать), тогда вам нужно, чтобы все ваши дочерние задачи этого алгоритма удовлетворяли вашей пользовательской задаче scheduler (TaskScheduler.Current), потому что иначе вы в конечном итоге TaskScheduler.Current все ресурсы для алгоритма, и ваш пул потоков веб-сервера будет голодать.

Когда использовать TaskScheduler.Current, TaskScheduler.Default, TaskScheduler.FromCurrentSynchronizationContext() или какой-либо другой TaskScheduler

  • TaskScheduler.FromCurrentSynchronizationContext() - Конкретный для WPF, Forms-приложений контекст потока пользовательского интерфейса, вы используете это в основном, когда хотите вернуться к потоку пользовательского интерфейса после того, как его выгрузили некоторую работу в поток, отличный от UI

пример, взятый отсюда

private void button_Click(…) 
{ 
    … // #1 on the UI thread 
    Task.Factory.StartNew(() => 
    { 
        … // #2 long-running work, so offloaded to non-UI thread 
    }).ContinueWith(t => 
    { 
        … // #3 back on the UI thread 
    }, TaskScheduler.FromCurrentSynchronizationContext()); 
}
  • TaskScheduler.Default - почти все время, когда у вас нет каких-либо конкретных требований, граничные случаи для сопоставления.
  • TaskScheduler.Current. Я думаю, что я привел один общий пример выше, но в целом его следует использовать, когда у вас есть либо пользовательский планировщик, либо явным образом передал TaskScheduler.FromCurrentSynchronizationContext() в TaskFactory или Task.StartNew а затем вы используете задачи продолжения или внутренние задачи (так чертовски редкие имо).