C# 使用2个等待的I/O绑定任务运行,还是使用经典的Fork/Join方法?

C# 使用2个等待的I/O绑定任务运行,还是使用经典的Fork/Join方法?,c#,.net,asynchronous,.net-core,async-await,C#,.net,Asynchronous,.net Core,Async Await,假设我们有2个I/O绑定的任务需要处理,其中有N个元素。我们可以将这两个任务称为A和BB只能在A产生结果后运行 我们可以通过两种方式实现这一点。(请忽略访问修改关闭的情况。) 任务。运行方式: List<Task> workers = new List<Task>(); for (int i = 0; i < N; i++) { workers.Add(Task.Run(async () => { await A(i);

假设我们有2个I/O绑定的任务需要处理,其中有N个元素。我们可以将这两个任务称为ABB只能在A产生结果后运行

我们可以通过两种方式实现这一点。(请忽略访问修改关闭的情况。)

任务。运行方式:

List<Task> workers = new List<Task>();
for (int i = 0; i < N; i++)
{
    workers.Add(Task.Run(async () =>
    {
        await A(i);
        await B(i);
    }
}
await Task.WhenAll(workers);
List workers=new List();
对于(int i=0;i
{
等待A(i);
等待B(i);
}
}
等待任务。何时(工人);
经典分叉/连接:

List<Task> workersA = new List<Task>();
List<Task> workersB = new List<Task>();
for (int i = 0; i < N; i++)
{
    workersA.Add(A(i));
}

await Task.WhenAll(workersA);

for (int i = 0; i < N; i++)
{
    workersB.Add(B(i));
}

await Task.WhenAll(workersB);
List workersA=new List();
List WORKERB=新列表();
对于(int i=0;i
或者,也可以通过以下方式进行:

List<Task> workers = new List<Task>();

for (int i = 0; i < N; i++)
{
    workers.Add(A(i));
}

for (int i = 0; i < N; i++)
{
    await workers[i];
    workers[i] = B(i);
}

await Task.WhenAll(workers);
List workers=new List();
对于(int i=0;i
我担心的是MSDN文档声明我们永远不应该使用Task.Run进行I/O操作

考虑到这一点,处理这一案件的最佳方法是什么

如果我错了,请纠正我的错误,但我们希望避免使用Task.Run,因为我们可以有效地将线程排队来处理工作,如果我们只使用wait,那么就在那里(因为操作是I/O)

我真的很想沿着Task.Run的路线走下去,但是如果它最终没有明显的原因而使用了线程/增加了额外的开销,那么就不可能了

我真的很想完成这项任务。走这条路

为什么?

但是,如果它最终使用线程没有明显的原因,那么它是不可能的

它说:

对要在线程池上运行的指定工作进行排队

这并不一定意味着每次调用
任务时都会有一个全新的线程。运行
。可能会,但不一定。您可以保证的是,它将在一个不是当前线程的线程上运行

您无法控制创建多少线程来完成所有这些工作。但是建议不要使用
Task.Run
进行I/O操作是合理的。这是没有收益的不必要的开销。这样会降低效率

您的其他任何一个解决方案都可以正常工作。您的上一个解决方案可能会更快完成,因为您将更快地开始调用
B()
(您只需等待第一个
A()
完成,然后再开始调用
B()

根据Theodor的答案更新:我们都是对的:)重要的是要知道异步方法中的所有代码在第一次等待之前(以及之后的代码,除非您另有规定)将在启动它的相同上下文中运行。在桌面应用中,这是UI线程。等待是异步的。因此,UI线程在等待时被释放。但是,如果该方法中有任何CPU繁重的工作,它将锁定UI线程

所以Theodor说你可以使用
任务。尽快运行
以使其脱离UI线程,并保证它永远不会锁定UI线程。虽然这是真的,但你不能到处盲目地使用该建议。首先,在I/O操作后,你可能需要在UI中做一些事情,而这必须在UI线程上完成。如果你使用 运行
,然后您必须确保返回到UI线程以完成该工作

但是,如果您调用的异步方法有足够的CPU绑定工作来冻结UI,那么它就不是严格意义上的I/O操作,并且“使用
Task.Run
进行CPU绑定工作,
async
/
等待
进行I/O”的建议仍然适用

我能说的就是:试试看。如果你发现无论你做什么都会冻结UI,那么就使用
Task.Run
。如果你发现它没有冻结UI,那么
Task.Run
是不必要的开销(请注意,这并不多,但仍然是不必要的,但如果你像现在这样在循环中做,情况会变得更糟)

所有这些都适用于桌面应用程序。如果你在ASP.NET中,那么
Task.Run
不会为你做任何事情,除非你尝试并行执行某些操作。在ASP.NET中,没有“UI线程”,因此不管你在哪个线程上执行工作。你只想确保在等待时不会锁定线程(因为ASP.NET中的线程数量有限)

如果您的工作是I/O绑定的,请使用
异步
等待
,而不使用
任务。运行
。您不应使用任务并行库。原因如中所述

不过,这条建议有误导性。通过阻止I/O操作的
Task.Run
,作者可能想到了以下几点:

var data = await Task.Run(() =>
{
    return webClient.DownloadString(url); // Blocking call
});
…这确实很糟糕,因为它会阻止线程池线程。但是使用异步委托运行
Task.Run
是完全可以的:

var data = await Task.Run(async () =>
{
    return await webClient.DownloadStringTaskAsync(url); // Async call
});
实际上,这是从UI应用程序的事件处理程序启动异步操作的首选方法,因为它可以确保立即释放UI线程。如果您按照本文的建议,省略了
任务。请运行

private async void Button1_Click(object sender, EventArgs args)
{
    var data = await webClient.DownloadStringTaskAsync(url);
}
…然后,您可能会面临异步方法可能不是100%异步的风险,并且可能会阻塞UI线程。对于专家编写的内置异步方法,这是一个很小的问题,但对于第三方异步方法来说,这是一个更大的问题,对于开发人员自己编写的异步方法来说,这是一个更大的问题


因此,关于您问题的选项,我认为第一个(Task.Run方式)是最安全和最有效的。第二个将分别等待所有A和所有B任务,因此持续时间最多为Max(A)+Max(B)。从统计上看,它应该比Max(A+B)长。

还有一种方式:
异步任务