Tenho um cenário estranho. Tenho uma lista de grupos. Para cada grupo, preciso iniciar uma tarefa para todos os conteúdos, mas preciso garantir que o último valor em cada grupo seja executado por último. Função simplificada abaixo:
public async Task DoAllTheWork(IEnumerable<IGrouping<string, string>> groups)
{
var allTasks = new List<Task>();
foreach (var group in groups)
{
var tasksInGroup = new List<Task>();
var values = group.ToList();
// start tasks for all except last value
foreach (var value in values.SkipLast(1))
{
var theTask = SomeAsyncFunction(value);
allTasks.Add(theTask);
tasksInGroup.Add(theTask);
}
// start a task for last value once all previous tasks are done
var lastTask = Task.Run(async () =>
{
await Task.WhenAll(tasksInGroup);
SomeAsyncFunction(values.Last());
});
allTasks.Add(lastTask);
}
await Task.WhenAll(allTasks);
}
Isso funciona bem. O problema é que todas as tarefas são executadas de uma vez, o que pode acabar sendo um problema se houver muitos grupos ou se os grupos tiverem muitos valores.
Gostaria de limitar o número de tarefas simultâneas sendo executadas. A maneira mais fácil que consigo pensar é que if allTasks
continha uma lista de tarefas que ainda não foram realmente iniciadas. Então eu poderia simplesmente fazer algo como:
Parallel.ForEach(allTasks, new ParallelOptions {MaxDegreeOfParallelism = 10},
aTask =>
{
aTask.Start();
});
Existe alguma maneira de fazer algo assim?
Você imaginou uma solução errada para o problema. Esta abordagem não fará o que você pensa:
Isso limitará a simultaneidade do início das tarefas somente. Iniciar uma tarefa com
Start
é praticamente instantâneo, já que tudo o que ele faz é agendar a execução da tarefa para o padrãoTaskScheduler
, que é oThreadPool
. Então oSomeAsyncFunction
não será limitado. A solução correta é usar oParallel.ForEach
, ou melhor ainda oParallel.ForEachAsync
, para executar oSomeAsyncFunction
diretamente, sem gerenciamento explícito de tarefas. AParallel
classe usa tarefas internamente, para que você não tenha que fazer isso sozinho (é por isso que a tecnologia geral é chamada de Task Parallel Library ).Esta solução não é perfeita porque você perderá alguma simultaneidade enquanto o
firstValues
estiver terminando e antes de olastValues
estiver iniciado, mas o efeito desta imperfeição deve ser pequeno. Implementar uma solução perfeita, onde oMaxDegreeOfParallelism
é imposto corretamente do início ao fim, não é trivial.Eu sugeriria usar um semáforo como mecanismo de sincronização - por exemplo,
SemaphoreSlim
.Primeiro, defina um método conveniente para lidar com a aquisição e liberação do semáforo:
Então você pode usar esse método na sua lógica.
Abaixo está um exemplo simplificado e completo, incorporando esta abordagem:
Referência
Você pode usar minha biblioteca PowerThreadPool para atingir seu objetivo.
Você pode definir
MaxThreads
como 10 usando oPowerPoolOption
para controlar o número máximo de threads simultâneos. ( wiki )Você pode definir
StartSuspended
paratrue
. Isso permite que você enfileire tarefas sem iniciar o pool de threads imediatamente. ( wiki )Você pode usar a
WorkOption.Dependents
propriedade para garantir que o último valor de cada grupo só comece depois que as tarefas anteriores do grupo forem concluídas. ( wiki )Depois que todas as tarefas e dependências forem adicionadas, chame
Start()
para iniciar a execução.Exemplo:
Espero que ajude você.
Aqui estão algumas respostas interessantes. Eu ofereço a você a maneira como eu faria isso:
Se o seu "trabalho" não for um cálculo complexo, eu não recomendaria usar Parallel.ForEach, especialmente se for algum tipo de operação de multiplicação de E/S.