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

Я хочу реализовать класс или шаблон, который гарантирует, что я никогда не буду выполнять более одной задачи за раз для определенного набора операций (вызовов HTTP). Вызовы задач могут исходить из разных потоков в случайное время. Я хочу использовать шаблон async-await, чтобы вызывающая сторона могла обрабатывать исключения, заключая вызов в try-catch.

Вот иллюстрация предполагаемого потока выполнения:

введите здесь описание изображения

Псевдокод от вызывающего абонента:

try {
    Task someTask = GetTask();
    await SomeScheduler.ThrottledRun(someTask);
} 
catch(Exception ex) { 
    // Handle exception
}

Вместо этого Taskclass здесь может быть классом Action в зависимости от решения.

Обратите внимание, что когда я использую слово «Расписание» в этом вопросе, я не обязательно использую его по отношению к Планировщик заданий .NET. Я недостаточно хорошо знаю библиотеку async-await, чтобы знать, под каким углом и с помощью каких инструментов подходить к этой проблеме. TaskScheduler может быть здесь уместен, а может и нет. Я прочитал документ Шаблон TAP и нашел шаблоны которые почти решают эту проблему, но не совсем (глава о чередовании).


person Nilzor    schedule 22.08.2012    source источник


Ответы (1)


Появился новый ConcurrentExclusiveSchedulerPair введите .NET 4.5 (я не помню, был ли он включен в Async CTP), и вы можете использовать его ExclusiveScheduler, чтобы ограничить выполнение одним Task за раз.

Попробуйте структурировать свою проблему как поток данных. Легко просто передать TaskScheduler в параметры блока для тех частей потока данных, которые вы хотите ограничить.

Если вы не хотите (или не можете) использовать Dataflow, вы можете сделать что-то подобное самостоятельно. Помните, что в TAP вы всегда возвращаете запущенные задачи, поэтому «создание» не отделено от «планирования», как в TPL.

Вы можете использовать ConcurrentExclusiveSchedulerPair для планирования Actions (или async lambdas без возвращаемых значений) следующим образом:

public static ConcurrentExclusiveSchedulerPair schedulerPair =
    new ConcurrentExclusiveSchedulerPair();
public static TaskFactory exclusiveTaskFactory =
    new TaskFactory(schedulerPair.ExclusiveScheduler);
...
public static Task RunExclusively(Action action)
{
  return exclusiveTaskFactory.StartNew(action);
}
public static Task RunExclusively(Func<Task> action)
{
  return exclusiveTaskFactory.StartNew(action).Unwrap();
}

Есть несколько замечаний по этому поводу:

  • Один экземпляр ConcurrentExclusiveSchedulerPair координирует только Task, которые поставлены в очередь его планировщикам. Второй экземпляр ConcurrentExclusiveSchedulerPair будет независимым от первого, поэтому вы должны убедиться, что один и тот же экземпляр используется во всех частях вашей системы, которые вы хотите скоординировать.
  • Метод async по умолчанию возобновит работу с того же TaskScheduler, с которого он был запущен. Таким образом, это означает, что если один метод async вызывает другой метод async, «дочерний» метод «унаследует» родительский метод TaskScheduler. Любой метод async может отказаться от продолжения своего TaskScheduler, используя ConfigureAwait(false) (в этом случае он продолжает работать непосредственно с пулом потоков).
person Stephen Cleary    schedule 22.08.2012
comment
Я мог бы заставить ваш пример работать при использовании перегрузки Action, но не при отправке Func‹Task›. Я был бы очень признателен, если бы вы могли объяснить, почему модульный тест в этой сути не работает, как и во второй задаче, не ожидающей завершения первой: gist.github.com/3424332 - person Nilzor; 22.08.2012
comment
Func<Task> не работает таким образом, потому что ConcurrentExclusiveSchedulerPair позволяет выполнять одну задачу за раз, поэтому, когда метод async awaits, он больше не выполняется. Если вам нужно заблокировать, пока весь метод async не будет завершен, вам, вероятно, лучше использовать async блокировка. - person Stephen Cleary; 22.08.2012
comment
Спасибо за вашу помощь. Я начинаю понимать, что два ключевых слова async и await — это лишь верхушка айсберга, на раскрытие которого уйдут месяцы. - person Nilzor; 22.08.2012
comment
Если вы используете ExclusiveScheduler с async действиями, имейте в виду, что выполнение нескольких из этих действий может чередоваться. Таким образом, когда первое действие уступает, второе действие может начать выполняться. Но остальная часть первого действия не начнется, пока второе действие не уступит или не завершится. Если это не то, что вам нужно, вы можете использовать блок потока данных TPL с MaxDegreeOfParallelism = 1. - person svick; 22.08.2012
comment
Есть ли способ сделать это действительно однопоточным? Прямо сейчас это гарантирует, что в любой момент времени работает не более 1 потока. Но что, если вы хотите гарантировать, что вся программа будет генерировать не более одного потока? - person Asad Saeeduddin; 17.05.2015
comment
@Asad: Если вы должны использовать один поток, возьмите его из пула потоков (или создайте свой собственный) и установите на нем свой собственный бесконечный цикл обработки. Однако это крайне необходимо. Невозможно ограничить весь процесс одним потоком; каждый процесс .NET имеет несколько потоков, даже если вы никогда не создавали их самостоятельно. - person Stephen Cleary; 17.05.2015
comment
@StephenCleary Причина, по которой я задавался этим вопросом, заключалась в том, что ExclusiveScheduler ограничивает степень параллелизма до 1, но не обязательно степень параллелизма. Например, если у вас есть метод, который не является потокобезопасным, ExclusiveScheduler не гарантирует, что вы не сможете вызвать его во второй раз, пока не будет обработано продолжение первого вызова. - person Asad Saeeduddin; 18.05.2015
comment
@Asad: Нет, ExclusiveScheduler будет выполнять только одно действие за раз. - person Stephen Cleary; 18.05.2015
comment
@StephenCleary Верно, но даже в этом случае метод может быть приостановлен из-за ожидания, а затем снова вызван до ожидания, и все это без одновременного выполнения более одной вещи. Вот фрагмент, который, надеюсь, лучше демонстрирует, что я имею в виду: gist.github.com/masaeedu/fb1b5065b9df84004dd8 . - person Asad Saeeduddin; 18.05.2015
comment
@Asad: Да, так работают планировщики задач. Они планируют единицы работы, представленные задачами. Метод async потенциально представляет собой несколько единиц работы, разделенных операторами await. Почему бы вам не задать свой собственный вопрос, стараясь избежать проблемы A/B (т. е. описать реальную проблему, которую вы пытаетесь решить)? - person Stephen Cleary; 18.05.2015
comment
Как всегда кратко и по делу @StephenCleary, спасибо! - person John Leidegren; 17.06.2018