为什么在没有将异步功能分配给Task变量的情况下直接等待异步功能不起作用

时间:2018-08-09 02:14:27

标签: c# async-await 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,仅仅是因为它们在自己的示例中没有什么不同?

1 个答案:

答案 0 :(得分:2)

这是因为当您返回Task时正在运行的Sleep()时,即使您已分配给变量,也是如此。

混乱之处在于,如果您将Task分配给变量(ABC),直到调用{{1} } ,但这不是真的。将await A;分配给sleep();后,就会立即调用A;因此sleep()方法中的Task正在运行。当您调用方法时,便开始将其分配给变量sleep()。因为在方法中您启动了Task

知道这一点;致电时:

Task

A,B和C已经同时开始...等待A之后,很可能B和C也已经完成或距离完成还有毫秒的时间。

在某些情况下,您可以引用尚未开始的await A; await B; await C; ,但是您必须有目的地返回未运行的Task来实现。

也要回答您的问题。

Task具有称为Task的方法,该方法返回GetAwaiter()。在C#中,当您编写TaskAwaiter时,您将实际的var task = sleep();分配给任务变量。当您编写Task时,编译器所做的一切都很棒,实际上它调用了await sleep();方法;已订阅。 Task.GetAwaiter()将运行,完成后,Task将触发继续操作。这不能用简单的答案来解释,但要知道外部逻辑会有所帮助。

除其他事项外,TaskAwaiter实现了TaskAwaiter,后者又实现了ICriticalNotifyCompletion。两者都有INotifyCompletionOnCompleted(Action)两种方法(通过命名约定,您可以猜测是哪种方法)。

要注意的另一件事是UnsafeOnCompleted(Action)返回一个Task.GetAwaiter(),但是TaskAwaiter返回一个Task<TResult>.GetAwaiter()。两者之间没有很大的区别,但是两项任务的TaskAwaiter<TResult>方法是不同的。当编组回到适当的线程上下文时,这就是所谓的。 GetResult()返回void,TaskAwaiter.GetResult()返回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。如果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

最重要的是C#如何处理async / await关键字。 public async void DoWork() { var task = DoTaskAsync(); await task; } 告诉编译器该方法将成为async的延续。简而言之;编译器知道要查找所有Task调用,并将其余的方法继续。

await关键字告诉编译器在await上调用Task.GetAwaiter()方法(并基本上订阅TaskINotifyCompletion)以发出信号。方法的延续。


我想补充一下,以防万一您不知道。如果您确实有一个以上要等待的任务,而宁愿等待一个任务都好像是一个任务,则可以使用ICriticalNotifyCompletion来完成,而不是:

Task.WhenAll()

您可以这样写得更干净:

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

await taskA;
await taskB;
await taskC;

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