假设我有一个类似SaveAsync(Item item)的方法,我需要在10个Item上调用它,并且调用彼此独立。我想关于线程的理想方式是

Thread A | Run `SaveAsync(item1)` until we hit the `await` | ---- ... ---- | Run `SaveAsync(item10)` until we hit the `await` | ---------------------------------------|
Thread B | --------------------------------------------------- | Run the stuff after the `await` in `SaveAsync(item1)` | ------------------ ... -----------------------|
Thread C | ------------------------------------------------------ | Run the stuff after the `await` in `SaveAsync(item2)` | ------------------ ... --------------------|
.
.
.

(有可能在多个项目的await之后的某些内容在同一线程中运行,甚至可能在线程A中运行)

我想知道如何用C#编写代码?它是并行的foreach还是带有await SaveAsync(item)的循环或什么?

最佳答案

这是人们使用async/await处理大多数错误的地方:

1)有人认为,在调用async方法之后,有/没有等待的所有内容都可以转换为ThreadPool线程。

2)或者人们认为异步确实是同步运行的。

真相之间,@ Iqon关于下一个代码块的声明实际上是不正确的:“这里唯一的主要区别是,所有任务都在同一线程中执行。”

var tasks = new List<Task>();
foreach(var item in items)
{
    tasks.Add(SaveAsync(item)); // No await here
}

await Task.WhenAll(tasks); // will only continue when all tasks are finished (or cancelled or failed)

进行这样的声明将表明异步方法SaveAsync(item)实际上能够完全和完全同步地执行。

以下是示例:
async Task SaveAsync1(Item item)
{
    //no awaiting at all
}

async Task SaveAsync2(Item item)
{
    //awaiting already completed task
    int i = await Task.FromResult(0);
}

此类方法实际上将在执行异步任务的线程上同步运行。但是这些种类的异步方法是特殊的雪花。此处没有等待的操作,即使在方法内部,也没有问题,因为在第一种情况下它不会等待,而在第二种情况下它会在完成的任务上等待,因此它在等待之后同步继续,这两个调用将是相同的:
var taskA = SaveAsync2(item);//it would return task runned to completion

//same here, await wont happen as returned task was runned to completion
await SaveAsync2(item);

因此,在执行语句时,仅在以下特殊情况下,在此处同步执行异步方法才是正确的:
var tasks = new List<Task>();
foreach(var item in items)
{
    tasks.Add(SaveAsync2(item));
}

await Task.WhenAll(tasks); // will only continue when all tasks are finished (or cancelled or failed)

并且无需存储任务并等待Task.WhenAll(tasks),这一切都已经完成,这就足够了:
foreach(var item in items)
{
    SaveAsync2(item);
    //it will execute synchronously because there is
    //nothing to await for in the method
}

现在让我们探索实际情况,一个实际上等待内部事物或引发等待操作的异步方法:
async Task SaveAsync3(Item item)
{
    //awaiting already completed task
    int i = await Task.FromResult(0);

    await Task.Delay(1000);
    Console.WriteLine(i);
}

现在这会怎么办?
var tasks = new List<Task>();
foreach(var item in items)
{
    tasks.Add(SaveAsync3(item));
}

await Task.WhenAll(tasks); // will only continue when all tasks are finished (or cancelled or failed)

它会同步运行吗?不!

它可以并行运行吗?不!

就像我在开始时所说的,事实是异步方法之间是有一定距离的,除非它们是像SaveAsync1和SaveAsync2这样的特殊雪花。

那么代码做了什么呢?它同步执行每个SaveAsync3直到等待Task.Delay,在该延迟中发现返回的任务不完整,然后返回给调用方,并提供了不完整的任务,该任务存储到任务中,下一个SaveAsync也以相同的方式执行。

现在等待Task.WhenAll(tasks);具有真正的意义,因为它正在等待一些不完整的操作,这些操作将在此线程上下文之外并行运行。
等待Task.Delay之后SaveAsync3方法的所有这些部分都将安排到ThreadPool并并行运行,除非有特殊情况,例如UI线程上下文,在这种情况下,需要TaskDelay之后的ConfigureAwait(false)。

希望你们明白我想说的。除非您有更多有关异步方法或代码的信息,否则您无法真正说出异步方法将如何运行。

此练习还打开了何时在异步方法上运行Task.Run的问题。
它经常被误用,我认为实际上只有两种主要情况:

1)当您想脱离当前线程上下文时,例如UI,ASP.NET等

2)当异步方法具有同步部分(直到第一次未完成等待)时,这是计算密集型的,并且您还希望卸载它,而不仅仅是不完整等待部分。情况就是SaveAsync3长时间计算变量i,比如说斐波那契(Fibonacci):)。

例如,您不必在SaveAsync之类的文件上使用Task.Run即可打开文件并以异步方式将其保存到其中,除非在SaveAsync中首次等待之前的同步部分会浪费时间。然后Task.Run的顺序是情况2)的一部分。

10-04 13:00
查看更多