我通常会执行以下操作
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
,仅仅是因为它们在自己的示例中没有什么不同?
答案 0 :(得分:2)
这是因为当您返回Task
时正在运行的Sleep()
时,即使您已分配给变量,也是如此。
混乱之处在于,如果您将Task
分配给变量(A
,B
或C
),直到调用{{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
。两者都有INotifyCompletion
和OnCompleted(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()
方法(并基本上订阅Task
和INotifyCompletion
)以发出信号。方法的延续。
我想补充一下,以防万一您不知道。如果您确实有一个以上要等待的任务,而宁愿等待一个任务都好像是一个任务,则可以使用ICriticalNotifyCompletion
来完成,而不是:
Task.WhenAll()
您可以这样写得更干净:
var taskA = DoTaskAsync();
var taskB = DoTaskAsync();
var taskC = DoTaskAsync();
await taskA;
await taskB;
await taskC;
还有更多的内置方法可以用来做这种事情。只是探索它。