为了账号安全,请及时绑定邮箱和手机立即绑定

为什么在不将其分配给 Task 变量的情况下直接等待异步函数不起作用

为什么在不将其分配给 Task 变量的情况下直接等待异步函数不起作用

C#
素胚勾勒不出你 2021-11-21 14:32:18
通常,我会执行以下操作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;}为什么将异步函数分配给新变量会有所不同?我原本认为它们是一样的。更新为什么让我感到困惑的是,实际上在Async-await 的 Microsoft 文档中,他们在代码中声明了以下内容。// 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 回答

?
函数式编程

TA贡献1807条经验 获得超9个赞

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


令人困惑的是,Task如果您在调用之前将其分配给变量(、 或)A,则不会开始B,但事实并非如此。一旦你分配给,就被调用了;因此in方法正在运行。将其分配给变量与否在调用方法时开始;因为在你启动的方法中。 Cawait 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()返回 aTaskAwaiter但Task<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关键字告诉编译器调用Task.GetAwaiter()上的方法Task(和基本上订阅INotifyCompletion和ICriticalNotifyCompletion)转换成信号的方法中的延续。


我想补充一点,以防万一你不知道。如果您确实有多个任务要等待,但宁愿等待一项任务,就好像它们都是一项任务一样,那么您可以使用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);

并且内置了更多的方法来做这种事情;只是探索它。


查看完整回答
反对 回复 2021-11-21
  • 1 回答
  • 0 关注
  • 169 浏览

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信