循环异步方法的最佳方法是什么?

时间:2017-11-23 09:21:20

标签: c# .net multithreading mono async-await

我想知道循环异步方法的最佳方法是什么。 假设我有一个方法:

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代码中嵌套任务的执行位置以及最佳方法。

感谢任何提示!

1 个答案:

答案 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不能重复。

您的解决方案

以下是我对您的解决方案的看法。

解决方案* 1

正如你所说,它将创造10 000个任务。据我所知(但我不是该领域的专家),系统将在任务之间共享ThreadPool线程,应用一些Round Robin算法。我认为同样的任务甚至可以在第一个线程上开始执行,由系统暂停,并在第二个线程上完成执行。这将引入比必要更多的开销,并导致整体运行时间变慢。

我认为绝对应该避免这种情况!

解决方案* 2

我读到Parallel API与异步操作不兼容。除非绝对需要,否则我还会多次阅读you don't want to call .Result任务。

所以我也会避免这个解决方案。

解决方案* 3

老实说,我无法想象这会做什么^^。这可能是一个很好的解决方案,因为您不是一次创建所有任务。无论如何,你仍然会创造10 000个任务,所以我会避免它。

解决方案* 4

老实说,我甚至不知道这个API,所以我不能真正评论它。但由于它涉及第三方图书馆,如果可能的话我会避免使用它。

相关问题