我想知道循环异步方法的最佳方法是什么。 假设我有一个方法:
public async Task<bool> DownloadThenWriteThenReturnResult(string id)
{
// async/await stuff....
}
我想把这个方法称为10000次,假设我已经有一个名为“_myStrings”的参数的10 000个字符串列表。 我希望最多4个线程来分享这项工作(在生产中我会使用ProcessorCount - 1)。我希望能够取消一切。最后我想要每个电话的结果。 我想知道有什么区别,最好的方式和原因是什么:
* 1 -
var allTasks = _myStrings.Select(st =>DownloadThenWriteThenReturnResult(st));
bool[] syncSuccs = await Task.WhenAll(syncTasks);
* 2 -
await Task.Run(() =>
{
var result = new ConcurrentQueue<V>();
var po = new ParallelOptions(){MaxDegreeOfParallelism = 4};
Parallel.ForEach(_myStrings, po, (st) =>
{
result.Enqueue(DownloadThenWriteThenReturnResult(st).Result);
po.CancellationToken.ThrowIfCancellationRequested();
});
});
* 3 -
using (SemaphoreSlim throttler = new SemaphoreSlim(initialCount: 4))
{
var results = new List<bool>();
var allTasks = new List<Task>();
foreach (var st in _myStrings)
{
await throttler.WaitAsync();
allTasks.Add(Task.Run(async () =>
{
try
{
results.Add(await DownloadThenWriteThenReturnResult(st));
}
finally
{
throttler.Release();
}
}));
}
await Task.WhenAll(allTasks);
}
* 4 -
var block = new TransformBlock<string, bool>(
async st =>
{
return await DownloadThenWriteThenReturnResult(st);
}, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 4});
foreach (var st in _myStrings)
{
await block.SendAsync(st);
}
var results = new List<bool>();
foreach (var st in _myStrings)
{
results.Add(await block.ReceiveAsync());
}
还有其他方法吗?这4个给我类似的结果,而只有* 2,* 3和* 4使用4个线程。 你能确认一下:
* 1在线程池线程上创建10 000个任务,但只在一个线程中执行
* 2将创建4个线程T1 T2 T3和T4。它使用.Result因此它一直不是异步(我应该避免在这里?)。由于DownloadThenWriteThenReturnResult在4个线程T1 T2 T3或T4之一中执行, 嵌套任务放在哪里(通过嵌套任务,我的意思是等待时每个异步方法将返回什么)?在一个专用的线程池线程中(让我们说T11 T21 T31和T41)?
* 3和* 4的相同问题
* 4似乎是我最好的一击。很容易理解发生了什么,我将能够创建新的块并在需要时链接它们。它似乎完全异步。但我想了解DownLoadThenWriteThenReturnResult中所有Async / Await代码中嵌套任务的执行位置以及最佳方法。
感谢任何提示!
答案 0 :(得分:1)
我会尽力回答你的所有问题。
首先,这就是我要做的。我试图最小化任务数量并保持代码简单。
您的问题看起来像某种生产者/消费者案例。我会选择这样简单的东西:
public async Task Work(ConcurrentQueue<string> input, ConcurrentQueue<bool> output)
{
string current;
while (input.TryDequeue(out current))
{
output.Enqueue(await DownloadThenWriteThenReturnResult(current));
}
}
var nbThread = 4;
var input = new ConcurrentQueue<string>(_myStrings);
var output = new ConcurrentQueue<bool>();
var workers = new List<Task>(nbThread);
for (int i = 0; i < nbThread; i++)
{
workers.Add(Task.Run(async () => await this.Work(input, output)));
}
await Task.WhenAll(workers);
我不确定线程的数量与处理器的数量有关。如果您正在处理CPU绑定操作,则会出现这种情况。在这种情况下,您应该尽可能同步运行,因为系统引入的从一个上下文切换到另一个上下文的重载很重。所以在那种情况下,通过线程进行一次操作就是这样。
但在您的情况下,由于您大部分时间都在等待I / O(http调用的网络,写入的磁盘等),您可能会并行启动更多任务。每次任务等待I / O时,系统都可以暂停它并切换到另一个任务。这里的重载不会浪费,因为另一方面线程会等待什么都不做。
您应该使用4,5,6等任务进行基准测试,并找出哪一项效率更高。
我在这里可以看到的一个问题是你不知道产生了哪些输入。您可以使用ConcurrentDictionary
代替ConcurrentQueue
,但_myStrings
不能重复。
以下是我对您的解决方案的看法。
正如你所说,它将创造10 000个任务。据我所知(但我不是该领域的专家),系统将在任务之间共享ThreadPool线程,应用一些Round Robin算法。我认为同样的任务甚至可以在第一个线程上开始执行,由系统暂停,并在第二个线程上完成执行。这将引入比必要更多的开销,并导致整体运行时间变慢。
我认为绝对应该避免这种情况!
我读到Parallel API与异步操作不兼容。除非绝对需要,否则我还会多次阅读you don't want to call .Result
任务。
所以我也会避免这个解决方案。
老实说,我无法想象这会做什么^^。这可能是一个很好的解决方案,因为您不是一次创建所有任务。无论如何,你仍然会创造10 000个任务,所以我会避免它。
老实说,我甚至不知道这个API,所以我不能真正评论它。但由于它涉及第三方图书馆,如果可能的话我会避免使用它。