Наблюдайте следующую функцию:
public Task RunInOrderAsync<TTaskSeed>(IEnumerable<TTaskSeed> taskSeedGenerator,
CreateTaskDelegate<TTaskSeed> createTask,
OnTaskErrorDelegate<TTaskSeed> onError = null,
OnTaskSuccessDelegate<TTaskSeed> onSuccess = null) where TTaskSeed : class
{
Action<Exception, TTaskSeed> onFailed = (exc, taskSeed) =>
{
if (onError != null)
{
onError(exc, taskSeed);
}
};
Action<Task> onDone = t =>
{
var taskSeed = (TTaskSeed)t.AsyncState;
if (t.Exception != null)
{
onFailed(t.Exception, taskSeed);
}
else if (onSuccess != null)
{
onSuccess(t, taskSeed);
}
};
var enumerator = taskSeedGenerator.GetEnumerator();
Task task = null;
while (enumerator.MoveNext())
{
if (task == null)
{
try
{
task = createTask(enumerator.Current);
Debug.Assert(ReferenceEquals(task.AsyncState, enumerator.Current));
}
catch (Exception exc)
{
onFailed(exc, enumerator.Current);
}
}
else
{
task = task.ContinueWith((t, taskSeed) =>
{
onDone(t);
var res = createTask((TTaskSeed)taskSeed);
Debug.Assert(ReferenceEquals(res.AsyncState, taskSeed));
return res;
}, enumerator.Current).TaskUnwrap();
}
}
if (task != null)
{
task = task.ContinueWith(onDone);
}
return task;
}
Где TaskUnwrap
- это сохраняющая состояние версия стандарта Task.Unwrap
:
public static class Extensions
{
public static Task TaskUnwrap(this Task<Task> task, object state = null)
{
return task.Unwrap().ContinueWith((t, _) =>
{
if (t.Exception != null)
{
throw t.Exception;
}
}, state ?? task.AsyncState);
}
}
Метод RunInOrderAsync
позволяет выполнять N задач асинхронно, но последовательно - один за другим. По сути, он запускает задачи, созданные из заданных семян, с пределом concurrency равным 1.
Предположим, что задачи, созданные из семян делегатом createTask
, не соответствуют нескольким параллельным задачам.
Теперь я хотел бы добавить параметр maxConcurrencyLevel, поэтому подпись функции будет выглядеть так:
Task RunInOrderAsync<TTaskSeed>(int maxConcurrencyLevel,
IEnumerable<TTaskSeed> taskSeedGenerator,
CreateTaskDelegate<TTaskSeed> createTask,
OnTaskErrorDelegate<TTaskSeed> onError = null,
OnTaskSuccessDelegate<TTaskSeed> onSuccess = null) where TTaskSeed : class
И здесь я немного застрял.
У SO есть такие вопросы:
- System.Threading.Tasks - ограничение количества одновременных задач
- Обработка на основе задач с ограничением для одновременного номера задачи с .NET 4.5 и С#
- .Net TPL: ограниченный concurrency планировщик заданий уровня с приоритетом задачи?
Что в основном предлагает два способа атаки на проблему:
- Используя
Parallel.ForEach
сParallelOptions
, указав значение свойстваMaxDegreeOfParallelism
как равное требуемому максимальному уровню concurrency. - Использование пользовательского
TaskScheduler
с желаемым значениемMaximumConcurrencyLevel
.
Второй подход не сокращает его, потому что все задействованные задачи должны использовать один и тот же экземпляр планировщика задач. Для этого все методы, используемые для возврата Task
, должны иметь перегруз, принимающий пользовательский экземпляр TaskScheduler
. К сожалению, Microsoft не очень согласна с этим. Например, SqlConnection.OpenAsync
не принимает такой аргумент (но TaskFactory.FromAsync
делает).
Первый подход подразумевает, что мне придется преобразовать задачи в действия, примерно так:
() => t.Wait()
Я не уверен, что это хорошая идея, но я буду рад получить больше информации об этом.
Другой подход - использовать TaskFactory.ContinueWhenAny
, но это грязно.
Любые идеи?
РЕДАКТИРОВАТЬ 1
Я хотел бы уточнить причины, требующие ограничения. Наши задачи в конечном счете выполняют SQL-запросы на одном сервере SQL. Мы хотим, чтобы ограничить количество параллельных исходящих SQL-запросов. Вполне возможно, что другие SQL-операторы будут выполняться одновременно с другими фрагментами кода, но это пакетный процессор и потенциально может наводнить сервер.
Теперь, имейте в виду, что, хотя мы говорим об одном и том же SQL-сервере, на этом же сервере имеется множество баз данных. Таким образом, речь идет не о том, чтобы ограничить количество открытых подключений SQL к одной и той же базе данных, потому что база данных может быть совсем не такой.
Вот почему решения doom day, такие как ThreadPool.SetMaxThreads()
, не имеют значения.
Теперь о SqlConnection.OpenAsync
. Он был сделан асинхронным по какой-либо причине - он мог бы сделать обратный переход к серверу и, следовательно, может быть подвержен сетевой латентности и другим прекрасным побочным эффектам распределенной среды. Таким образом, это не отличается от других асинхронных методов, которые принимают параметр TaskScheduler
. Я склонен думать, что не принимать его - это просто ошибка.
РЕДАКТИРОВАТЬ 2
Я хотел бы сохранить асинхронный дух исходной функции. Поэтому я хочу избежать каких-либо явных блокирующих решений.
РЕДАКТИРОВАТЬ 3
Благодаря @fsimonazzi answer Теперь у меня есть рабочая реализация желаемой функциональности. Вот код:
var sem = new SemaphoreSlim(maxConcurrencyLevel);
var tasks = new List<Task>();
var enumerator = taskSeedGenerator.GetEnumerator();
while (enumerator.MoveNext())
{
tasks.Add(sem.WaitAsync().ContinueWith((_, taskSeed) =>
{
Task task = null;
try
{
task = createTask((TTaskSeed)taskSeed);
if (task != null)
{
Debug.Assert(ReferenceEquals(task.AsyncState, taskSeed));
task = task.ContinueWith(t =>
{
sem.Release();
onDone(t);
});
}
}
catch (Exception exc)
{
sem.Release();
onFailed(exc, (TTaskSeed)taskSeed);
}
return task;
}, enumerator.Current).TaskUnwrap());
}
return Task.Factory.ContinueWhenAll(tasks.ToArray(), _ => sem.Dispose());