Dans une application de métro, je dois exécuter un certain nombre d'appels WCF. Il y a un nombre important d'appels à faire, donc je dois les faire en boucle parallèle. Le problème est que la boucle parallèle se termine avant que les appels WCF ne soient tous terminés.
Comment refactoriez-vous cela pour fonctionner comme prévu?
var ids = new List<string>() { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" };
var customers = new System.Collections.Concurrent.BlockingCollection<Customer>();
Parallel.ForEach(ids, async i =>
{
ICustomerRepo repo = new CustomerRepo();
var cust = await repo.GetCustomer(i);
customers.Add(cust);
});
foreach ( var customer in customers )
{
Console.WriteLine(customer.ID);
}
Console.ReadKey();
L'idée de Parallel.ForEach()
est que vous avez un ensemble de threads et que chaque thread traite une partie de la collection. Comme vous l'avez remarqué, cela ne fonctionne pas avec async
-await
, où vous souhaitez libérer le thread pour la durée de l'appel asynchrone.
Vous pouvez "réparer" cela en bloquant les threads ForEach()
, mais cela annule le point entier de async
-await
.
Ce que vous pouvez faire est d'utiliser TPL Dataflow au lieu de Parallel.ForEach()
, qui prend en charge asynchrone Task
s.
Spécifiquement, votre code pourrait être écrit en utilisant un TransformBlock
qui transforme chaque identifiant en un Customer
en utilisant le async
lambda. Ce bloc peut être configuré pour s'exécuter en parallèle. Vous lieriez ce bloc à un ActionBlock
qui écrit chaque Customer
sur la console. Après avoir configuré le réseau de blocs, vous pouvez Post()
chaque id pour le TransformBlock
.
Dans du code:
var ids = new List<string> { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" };
var getCustomerBlock = new TransformBlock<string, Customer>(
async i =>
{
ICustomerRepo repo = new CustomerRepo();
return await repo.GetCustomer(i);
}, new ExecutionDataflowBlockOptions
{
MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded
});
var writeCustomerBlock = new ActionBlock<Customer>(c => Console.WriteLine(c.ID));
getCustomerBlock.LinkTo(
writeCustomerBlock, new DataflowLinkOptions
{
PropagateCompletion = true
});
foreach (var id in ids)
getCustomerBlock.Post(id);
getCustomerBlock.Complete();
writeCustomerBlock.Completion.Wait();
Bien que vous souhaitiez probablement limiter le parallélisme de la TransformBlock
à une petite constante. Vous pouvez également limiter la capacité de la variable TransformBlock
et lui ajouter les éléments de manière asynchrone à l'aide de SendAsync()
, par exemple si la collection est trop volumineuse.
Un avantage supplémentaire par rapport à votre code (si cela a fonctionné) est que l'écriture commencera dès qu'un élément est terminé, et n'attend pas que tout le traitement soit terminé.
réponse de svick est (comme d'habitude) excellent.
Cependant, je trouve que Dataflow est plus utile lorsque vous devez réellement transférer de grandes quantités de données. Ou lorsque vous avez besoin d'une file d'attente compatible async
-.
Dans votre cas, une solution plus simple consiste simplement à utiliser le parallélisme de style async
-:
var ids = new List<string>() { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" };
var customerTasks = ids.Select(i =>
{
ICustomerRepo repo = new CustomerRepo();
return repo.GetCustomer(i);
});
var customers = await Task.WhenAll(customerTasks);
foreach (var customer in customers)
{
Console.WriteLine(customer.ID);
}
Console.ReadKey();
L'utilisation de DataFlow comme suggéré par svick peut s'avérer excessive et la réponse de Stephen ne fournit pas le moyen de contrôler la simultanéité de l'opération. Cependant, cela peut être réalisé assez simplement:
public static async Task RunWithMaxDegreeOfConcurrency<T>(
int maxDegreeOfConcurrency, IEnumerable<T> collection, Func<T, Task> taskFactory)
{
var activeTasks = new List<Task>(maxDegreeOfConcurrency);
foreach (var task in collection.Select(taskFactory))
{
activeTasks.Add(task);
if (activeTasks.Count == maxDegreeOfConcurrency)
{
await Task.WhenAny(activeTasks.ToArray());
//observe exceptions here
activeTasks.RemoveAll(t => t.IsCompleted);
}
}
await Task.WhenAll(activeTasks.ToArray()).ContinueWith(t =>
{
//observe exceptions in a manner consistent with the above
});
}
Les appels ToArray()
peuvent être optimisés en utilisant un tableau au lieu d'une liste et en remplaçant les tâches terminées, mais je doute que cela fasse une grande différence dans la plupart des scénarios. Exemple d'utilisation selon la question du PO:
RunWithMaxDegreeOfConcurrency(10, ids, async i =>
{
ICustomerRepo repo = new CustomerRepo();
var cust = await repo.GetCustomer(i);
customers.Add(cust);
});
ÉDITE Fellow SO utilisateur et assistant TPL Eli Arbel m'a dirigé vers un article lié de Stephen Toub . Comme à son habitude, sa mise en œuvre est à la fois élégante et efficace:
public static Task ForEachAsync<T>(
this IEnumerable<T> source, int dop, Func<T, Task> body)
{
return Task.WhenAll(
from partition in Partitioner.Create(source).GetPartitions(dop)
select Task.Run(async delegate {
using (partition)
while (partition.MoveNext())
await body(partition.Current).ContinueWith(t =>
{
//observe exceptions
});
}));
}
Vous pouvez économiser des efforts avec le nouveau paquet AsyncEnumerator NuGet Package , qui n'existait pas il y a 4 ans lorsque la question a été publiée. Il vous permet de contrôler le degré de parallélisme:
using System.Collections.Async;
...
await ids.ParallelForEachAsync(async i =>
{
ICustomerRepo repo = new CustomerRepo();
var cust = await repo.GetCustomer(i);
customers.Add(cust);
},
maxDegreeOfParallelism: 10);
Disclaimer: Je suis l'auteur de la bibliothèque AsyncEnumerator, qui est open source et sous licence MIT, et je poste ce message uniquement pour aider la communauté.
Enveloppez le Parallel.Foreach
dans une Task.Run()
et au lieu du mot clé await
, utilisez [yourasyncmethod].Result
(vous devez faire la tâche Task.Run pour ne pas bloquer le thread d'interface utilisateur)
Quelque chose comme ça:
var yourForeachTask = Task.Run(() =>
{
Parallel.ForEach(ids, i =>
{
ICustomerRepo repo = new CustomerRepo();
var cust = repo.GetCustomer(i).Result;
customers.Add(cust);
});
});
await yourForeachTask;
Cela devrait être assez efficace et plus facile que de faire fonctionner tout le flux de données TPL:
var customers = await ids.SelectAsync(async i =>
{
ICustomerRepo repo = new CustomerRepo();
return await repo.GetCustomer(i);
});
...
public static async Task<IList<TResult>> SelectAsync<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, Task<TResult>> selector, int maxDegreesOfParallelism = 4)
{
var results = new List<TResult>();
var activeTasks = new HashSet<Task<TResult>>();
foreach (var item in source)
{
activeTasks.Add(selector(item));
if (activeTasks.Count >= maxDegreesOfParallelism)
{
var completed = await Task.WhenAny(activeTasks);
activeTasks.Remove(completed);
results.Add(completed.Result);
}
}
results.AddRange(await Task.WhenAll(activeTasks));
return results;
}
Je suis un peu en retard pour faire la fête, mais vous pouvez envisager d'utiliser GetAwaiter.GetResult () pour exécuter votre code asynchrone dans un contexte synchronisé, mais de la même manière que ci-dessous;
Parallel.ForEach(ids, i =>
{
ICustomerRepo repo = new CustomerRepo();
// Run this in thread which Parallel library occupied.
var cust = repo.GetCustomer(i).GetAwaiter().GetResult();
customers.Add(cust);
});
Après avoir introduit plusieurs méthodes d’aide, vous pourrez exécuter des requêtes parallèles avec cette syntaxe simple:
const int DegreeOfParallelism = 10;
IEnumerable<double> result = await Enumerable.Range(0, 1000000)
.Split(DegreeOfParallelism)
.SelectManyAsync(async i => await CalculateAsync(i).ConfigureAwait(false))
.ConfigureAwait(false);
Voici ce qui se passe ici: nous divisons la collection source en 10 morceaux (.Split(DegreeOfParallelism)
), puis exécutons 10 tâches, chacune traitant ses éléments un par un (.SelectManyAsync(...)
) et les fusionnant dans une liste unique.
Il est à noter qu'il existe une approche plus simple:
double[] result2 = await Enumerable.Range(0, 1000000)
.Select(async i => await CalculateAsync(i).ConfigureAwait(false))
.WhenAll()
.ConfigureAwait(false);
Mais il faut précaution: si vous avez une collection source trop volumineuse, elle planifiera immédiatement une Task
pour chaque élément, ce qui peut avoir un impact significatif sur les performances.
Les méthodes d'extension utilisées dans les exemples ci-dessus se présentent comme suit:
public static class CollectionExtensions
{
/// <summary>
/// Splits collection into number of collections of nearly equal size.
/// </summary>
public static IEnumerable<List<T>> Split<T>(this IEnumerable<T> src, int slicesCount)
{
if (slicesCount <= 0) throw new ArgumentOutOfRangeException(nameof(slicesCount));
List<T> source = src.ToList();
var sourceIndex = 0;
for (var targetIndex = 0; targetIndex < slicesCount; targetIndex++)
{
var list = new List<T>();
int itemsLeft = source.Count - targetIndex;
while (slicesCount * list.Count < itemsLeft)
{
list.Add(source[sourceIndex++]);
}
yield return list;
}
}
/// <summary>
/// Takes collection of collections, projects those in parallel and merges results.
/// </summary>
public static async Task<IEnumerable<TResult>> SelectManyAsync<T, TResult>(
this IEnumerable<IEnumerable<T>> source,
Func<T, Task<TResult>> func)
{
List<TResult>[] slices = await source
.Select(async slice => await slice.SelectListAsync(func).ConfigureAwait(false))
.WhenAll()
.ConfigureAwait(false);
return slices.SelectMany(s => s);
}
/// <summary>Runs selector and awaits results.</summary>
public static async Task<List<TResult>> SelectListAsync<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, Task<TResult>> selector)
{
List<TResult> result = new List<TResult>();
foreach (TSource source1 in source)
{
TResult result1 = await selector(source1).ConfigureAwait(false);
result.Add(result1);
}
return result;
}
/// <summary>Wraps tasks with Task.WhenAll.</summary>
public static Task<TResult[]> WhenAll<TResult>(this IEnumerable<Task<TResult>> source)
{
return Task.WhenAll<TResult>(source);
}
}
Une méthode d'extension pour cela qui utilise SemaphoreSlim et permet également de définir un degré maximal de parallélisme
/// <summary>
/// Concurrently Executes async actions for each item of <see cref="IEnumerable<typeparamref name="T"/>
/// </summary>
/// <typeparam name="T">Type of IEnumerable</typeparam>
/// <param name="enumerable">instance of <see cref="IEnumerable<typeparamref name="T"/>"/></param>
/// <param name="action">an async <see cref="Action" /> to execute</param>
/// <param name="maxDegreeOfParallelism">Optional, An integer that represents the maximum degree of parallelism,
/// Must be grater than 0</param>
/// <returns>A Task representing an async operation</returns>
/// <exception cref="ArgumentOutOfRangeException">If the maxActionsToRunInParallel is less than 1</exception>
public static async Task ForEachAsyncConcurrent<T>(
this IEnumerable<T> enumerable,
Func<T, Task> action,
int? maxDegreeOfParallelism = null)
{
if (maxDegreeOfParallelism.HasValue)
{
using (var semaphoreSlim = new SemaphoreSlim(
maxDegreeOfParallelism.Value, maxDegreeOfParallelism.Value))
{
var tasksWithThrottler = new List<Task>();
foreach (var item in enumerable)
{
// Increment the number of currently running tasks and wait if they are more than limit.
await semaphoreSlim.WaitAsync();
tasksWithThrottler.Add(Task.Run(async () =>
{
await action(item).ContinueWith(res =>
{
// action is completed, so decrement the number of currently running tasks
semaphoreSlim.Release();
});
}));
}
// Wait for all tasks to complete.
await Task.WhenAll(tasksWithThrottler.ToArray());
}
}
else
{
await Task.WhenAll(enumerable.Select(item => action(item)));
}
}
Exemple d'utilisation:
await enumerable.ForEachAsyncConcurrent(
async item =>
{
await SomeAsyncMethod(item);
},
5);