首页 > 解决方案 > 为什么直接等待异步函数而不将其分配给Task变量就无法工作

问题描述

通常,我会执行以下操作

public static async Task dosth()
{
    List<Task> job = new List<Task>();
    for (int i = 0; i < 3; i++)
    {
        job.Add(sleep());
    }
    Task.WhenAll(job.ToArray());
}

static async Task sleep()
{
    await Task.Delay(1000);
    Console.WriteLine("Finish new");
}

它运行顺利,没有问题。但是当我对自己的代码进行审查时(尝试使用其他语法来完成相同的工作),我突然发现以下两个是不同的。

public static async Task dosthA()
{
    //This will be working synchronously, take 3 seconds.
    await sleep();
    await sleep();
    await sleep();

    //This will be working asynchronously, take 1 second only.
    Task A = sleep();
    Task B = sleep();
    Task C = sleep();
    await A;
    await B;
    await C;
}

为什么将异步函数分配给新变量会有所不同?我最初认为它们是相同的。

更新

为什么让我感到困惑的是,实际上在Microsoft doc on Async-await中,他们在代码中声明了以下内容。

// Calls to TaskOfTResult_MethodAsync  
Task<int> returnedTaskTResult = TaskOfTResult_MethodAsync();  
int intResult = await returnedTaskTResult;  
// or, in a single statement  
int intResult = await TaskOfTResult_MethodAsync();  

他们实际上是不同的,为什么他们使用//or , in a single statement,只是因为在他们自己的例子中没有什么不同?

标签: c#async-awaittask

解决方案


这是因为当您Task在调用时返回运行时,Sleep()即使您正在分配给变量。

令人困惑的是,如果在调用之前将其分配给变量(、、或),则Task不会开始,但事实并非如此。一旦你分配给,就会被调用;因此该方法正在运行。调用方法时是否将其分配给变量;因为在你启动的方法中。 ABCawait A; sleep();Asleep()Tasksleep()TaskTask

知道这一点;你打电话时:

await A;
await B;
await C;

A、B 和 C 已经同时开始......在等待 A 之后,很可能是 B,而 C 也已经完成或者距离完成还有几毫秒。

在某些情况下,您可以引用Task尚未开始的 a,但您必须故意返回一个未运行的对象Task才能执行此操作。

还要回答对您问题的编辑。

Tasks 有一个被调用的方法GetAwaiter(),它返回一个TaskAwaiter. 在 C# 中,当您编写时,您var task = sleep();将实际分配给Task任务变量。当您编写await sleep();编译器时,所有这些都是一样的,它会做一些很酷的事情并且它实际上会调用该Task.GetAwaiter()方法;这是订阅的。将Task运行,并在完成时TaskAwaiter触发继续操作。这无法用简单的答案来解释,但了解外部逻辑会有所帮助。

除其他事项外,TaskAwaiter工具ICriticalNotifyCompletion依次实施INotifyCompletion。两者都有一个方法,OnCompleted(Action)并且UnsafeOnCompleted(Action) (您可以通过命名约定猜出哪个是哪个)。

需要注意的另一件事是Task.GetAwaiter()返回 aTaskAwaiterTask<TResult>.GetAwaiter()返回 a TaskAwaiter<TResult>GetResult()两者没有太大区别,但两个任务的方法有区别;这就是编组回正确的线程上下文时所调用的内容。TaskAwaiter.GetResult()返回 void和TaskAwaiter<TResult>.GetResult()返回TResult

我觉得如果我进一步深入,我将不得不写几页来详细解释这一切......希望只是解释你的问题并拉开窗帘一点点将提供足够的光线来帮助你理解和深入挖掘如果你更好奇。

好的,所以根据下面的评论,我想进一步描述我的答案。

我将从这个简单的开始;让我们做一个Task;一个没有运行的,先看看它。

public Task GetTask()
{
    var task = new Task(() => { /*some work to be done*/ });
    //Now we have a reference to a non-running task.
    return task;
}

我们现在可以调用如下代码:

public async void DoWork()
{
    await GetTask();
}

……但我们将永远等待;直到应用程序结束,因为Task从未启动。然而; 我们可以这样做:

public async void DoWork()
{
    var task = GetTask();
    task.Start();
    await task;
}

…它会等待运行Task并在Task完成后继续。

知道这一点后,您可以根据需要进行尽可能多的调用,GetTask()并且您只会引用Task尚未开始的 s。

在您的代码中正好相反,这很好,因为这是最常用的方式。我鼓励您确保您的方法名称通知用户您如何返回Task. 如果Task已经在运行,最常见的约定是方法名称以 结尾Async。为了清楚起见,这是另一个运行运行的示例Task

public Task DoTaskAsync()
{
    var task = Task.Run(() => { /*some work to be done*/ });
    //Now we have a reference to a task that's already running.
    return task;
}

现在我们很可能会这样称呼这个方法:

public async void DoWork()
{
    await DoTaskAsync();
}

然而; 请注意,如果我们只是想像Task之前那样引用,我们可以,唯一的区别是这Task是在没有先验的地方运行。所以这段代码是有效的。

public async void DoWork()
{
    var task = DoTaskAsync();
    await task;
}

最大的收获是 C# 如何处理 async / await 关键字。 async告诉编译器该方法将成为Task. 简而言之; 编译器知道查找所有await调用并将方法的其余部分放在一个延续中。

关键字告诉编译器await调用( 并且基本上订阅and ) 上的Task.GetAwaiter()方法来表示方法的继续。TaskINotifyCompletionICriticalNotifyCompletion


我想补充一下,以防你不知道。如果您确实有多个要等待的任务,但宁愿等待一项任务,就好像它们都是一个,那么您可以使用Task.WhenAll()So 而不是:

var taskA = DoTaskAsync();
var taskB = DoTaskAsync();
var taskC = DoTaskAsync();

await taskA;
await taskB;
await taskC;

你可以像这样写得更干净一点:

var taskA = DoTaskAsync(); 
var taskB = DoTaskAsync(); 
var taskC = DoTaskAsync();

await Task.WhenAll(taskA, taskB, taskC);

而且还有更多内置的方法可以做这种事情;只是探索它。


推荐阅读