启动和等待任务有什么区别?
What's the difference between starting and awaiting a Task?
开始和等待有什么区别?以下代码摘自 Stephen Cleary 的博客(包括评论)
public async Task DoOperationsConcurrentlyAsync()
{
Task[] tasks = new Task[3];
tasks[0] = DoOperation0Async();
tasks[1] = DoOperation1Async();
tasks[2] = DoOperation2Async();
// At this point, all three tasks are running at the same time.
// Now, we await them all.
await Task.WhenAll(tasks);
}
我认为任务会在您等待它们时开始 运行...但是代码中的注释似乎暗示并非如此。
另外,任务怎么会是运行,我只是把它们归于一个任务类型的数组。这不只是一种归因,本质上不涉及行动吗?
开始你就开始了一项任务。这意味着它可能会被任何已安装的多任务处理系统选中并执行。
等待,您等待一项任务实际完成,然后再继续。
没有即发即弃线程这样的东西。你总是需要回来,对异常做出反应或对异步操作的结果做一些事情(数据库查询或 WebQuery 结果,文件系统操作完成,文档发送到最近的打印机池)。
您可以并行启动和执行任意数量的任务 运行。但是迟早你会需要结果才能继续。
A Task
returns "hot"(即已经开始)。 await
异步等待 Task
完成。
在您的示例中,您实际执行 await
的位置将影响任务是一个接一个地 运行 还是同时完成所有任务:
await DoOperation0Async(); // start DoOperation0Async, wait for completion, then move on
await DoOperation1Async(); // start DoOperation1Async, wait for completion, then move on
await DoOperation2Async(); // start DoOperation2Async, wait for completion, then move on
相对于:
tasks[0] = DoOperation0Async(); // start DoOperation0Async, move on without waiting for completion
tasks[1] = DoOperation1Async(); // start DoOperation1Async, move on without waiting for completion
tasks[2] = DoOperation2Async(); // start DoOperation2Async, move on without waiting for completion
await Task.WhenAll(tasks); // wait for all of them to complete
更新
"doesn't await
make an async
operation... behave like sync, in this example (and not only)? Because we can't (!) run anything else in parallel with DoOperation0Async()
in the first case. By comparison, in the 2nd case DoOperation0Async()
and DoOperation1Async()
run in parallel (e.g. concurrency,the main benefits of async
?)"
这是一个很大的主题,也是一个值得提出的问题,因为它在 SO 上有自己的主题,因为它偏离了开始任务和等待任务之间的区别的原始问题 - 因此我会在向您推荐时保持简短的回答在适当的地方找到其他答案。
不,await
执行 async
操作不会使其表现得像同步;这些关键字的作用是让开发人员能够编写 类似于 同步工作流的异步代码(有关更多信息,请参阅 Eric Lippert 的 )。
调用 await DoOperation0Async()
不会阻塞 执行此代码流的线程,而 DoOperation0
的同步版本(或类似 DoOperation0Async.Result
) 将阻塞线程直到操作完成。
在网络环境中考虑这一点。假设请求到达服务器应用程序。作为生成对该请求的响应的一部分,您需要执行一个 long-运行 操作(例如,查询一个外部 API 以获得生成您的响应所需的一些值)。如果这个 long-运行 操作的执行是同步的,执行你的请求的线程将 block 因为它必须等待 long-运行 操作完全的。另一方面,如果这个 long-运行 操作的执行是异步的,请求线程可以被释放,这样它就可以在 long-运行 操作的同时做其他事情(比如服务其他请求)仍然是 运行。然后,当 long-运行 操作最终完成时,请求线程(或者可能是线程池中的另一个线程)可以从它停止的地方继续(因为 long-运行 操作将是完成,结果现在可用)并做剩下的任何工作来产生响应。
服务器应用程序示例还解决了您关于 async
- .
的主要优点的问题的第二部分
Isn't that just an attribution, by nature not involving action?
通过调用异步方法,您可以执行其中的代码。通常在链下,一种方法将创建一个任务,然后 return 通过使用 return 或等待 return 它。
开始任务
您可以使用 Task.Run(...)
启动任务。这会在任务线程池上安排一些工作。
等待任务
要获得任务,您通常会调用一些 return 作为任务的(异步)方法。 async
方法的行为类似于常规方法,直到您 await
(或使用 Task.Run()
)。请注意,如果您 await 一个方法链并且 "final" 方法仅执行 Thread.Sleep()
或同步操作 - 那么您将阻塞初始调用线程,因为没有任何方法使用过任务的线程池。
您可以通过多种方式进行一些实际异步操作:
- 使用Task.Run
- 使用Task.Delay
- 使用Task.Yield
- 调用提供异步操作的库
这些是我想到的,可能还有更多。
举例
让我们假设线程 ID 1 是您从中调用 MethodA()
的主线程。线程 ID 5 及以上是 运行 任务的线程(System.Threading.Tasks 为此提供了默认调度程序)。
public async Task MethodA()
{
// Thread ID 1, 0s passed total
var a = MethodB(); // takes 1s
// Thread ID 1, 1s passed total
await Task.WhenAll(a); // takes 2s
// Thread ID 5, 3s passed total
// When the method returns, the SynchronizationContext
// can change the Thread - see below
}
public async Task MethodB()
{
// Thread ID 1, 0s passed total
Thread.Sleep(1000); // simulate blocking operation for 1s
// Thread ID 1, 1s passed total
// the await makes MethodB return a Task to MethodA
// this task is run on the Task ThreadPool
await Task.Delay(2000); // simulate async call for 2s
// Thread ID 2 (Task's pool Thread), 3s passed total
}
我们可以看到 MethodA
在 MethodB
上被阻塞,直到我们遇到 await 语句。
Await、SynchronizationContext 和控制台应用程序
您应该了解 Tasks 的一项功能。如果存在(基本上是非控制台应用程序),他们确保调用回 SynchronizationContext
。如果调用的代码不采取措施,则在任务上使用 .Result
或 .Wait()
时很容易 运行 陷入死锁。参见 https://blogs.msdn.microsoft.com/pfxteam/2012/01/20/await-synchronizationcontext-and-console-apps/
async/await 作为语法糖
await
基本上只是在调用完成后将以下代码安排到 运行。让我来说明一下幕后发生的事情。
这是使用 async/await 的未转换代码。等待 Something
方法,因此在 Something
完成后所有后续代码 (Bye
) 将是 运行。
public async Task SomethingAsync()
{
Hello();
await Something();
Bye();
}
为了解释这一点,我添加了一个实用程序 class Worker
,它只是对 运行 执行一些操作,然后在完成时通知。
public class Worker
{
private Action _action;
public event DoneHandler Done;
// skipping defining DoneHandler delegate
// store the action
public Worker(Action action) => _action = action;
public void Run()
{
// execute the action
_action();
// notify so that following code is run
Done?.Invoke();
}
}
现在我们的转换代码,不使用async/await
public Task SomethingAsync()
{
Hello(); // this remains untouched
// create the worker to run the "awaited" method
var worker = new Worker(() => Something());
// register the rest of our method
worker.Done += () => Bye();
// execute it
worker.Run();
// I left out the part where we return something
// or run the action on a threadpool to keep it simple
}
这是简短的回答:
要回答这个问题,您只需要了解 async
/ await
关键字的作用即可。
我们知道单个线程一次只能做一件事,我们也知道单个线程在整个应用程序中反弹到各种方法调用和事件等。这意味着线程接下来需要去的地方很可能在幕后某个地方被安排或排队(这是但我不会在这里解释这部分。)当一个线程调用一个方法时,那个方法是 运行在任何其他方法可以 运行 之前完成,这就是为什么长 运行ning 方法更适合分派给其他线程以防止应用程序冻结的原因。为了将单个方法分解为单独的队列,我们需要进行一些花哨的编程,或者您可以将 async
签名放在方法上。这告诉编译器,在某些时候该方法可以分解为其他方法并放入队列中以待 运行 之后。
如果这说得通,那么您已经知道 await
做了什么... await
告诉编译器这是方法将被分解并安排到 运行 以后。这就是为什么可以不使用 await
关键字而使用 async
关键字的原因;尽管编译器知道这一点并警告您。 await
通过使用 Task
.
为您完成这一切
await
如何使用 Task
告诉编译器安排方法的其余部分?当您调用 await Task
时,编译器会为您调用 Task
上的 Task.GetAwaiter()
方法。 GetAwaiter()
return一个TaskAwaiter
。 TaskAwaiter
实现了两个接口 ICriticalNotifyCompletion, INotifyCompletion
。每个都有一种方法,UnsafeOnCompleted(Action continuation)
和 OnCompleted(Action continuation)
。然后编译器包装方法的其余部分(在 await
关键字之后)并将其放入 Action
中,然后调用 OnCompleted
和 UnsafeOnCompleted
方法并传递 Action
作为参数。现在,当 Task
完成时,如果成功,它会调用 OnCompleted
,如果不成功,它会调用 UnsafeOnCompleted
,并且它会在用于启动 Task
的同一线程上下文中调用它们。它使用 ThreadContext
将线程分派给原始线程。
现在你可以理解 async
或 await
都不会执行任何 Task
。他们只是告诉编译器使用一些预先编写的代码来为您安排所有这些。实际上;您可以 await
不是 运行ning 的 Task
并且它将 await
直到 Task
被执行并完成或直到应用程序结束。
知道这个;让我们通过手动执行 async await 的操作来深入了解它。
使用异步等待
using System;
using System.Threading.Tasks;
namespace Question_Answer_Console_App
{
class Program
{
static void Main(string[] args)
{
Test();
Console.ReadKey();
}
public static async void Test()
{
Console.WriteLine($"Before Task");
await DoWorkAsync();
Console.WriteLine($"After Task");
}
static public Task DoWorkAsync()
{
return Task.Run(() =>
{
Console.WriteLine($"{nameof(DoWorkAsync)} starting...");
Task.Delay(1000).Wait();
Console.WriteLine($"{nameof(DoWorkAsync)} ending...");
});
}
}
}
//OUTPUT
//Before Task
//DoWorkAsync starting...
//DoWorkAsync ending...
//After Task
执行编译器手动执行的操作(某种程度上)
注意:虽然此代码有效,但它旨在帮助您从自上而下的角度理解异步等待。它不包含或执行编译器逐字执行的方式。
using System;
using System.Threading.Tasks;
namespace Question_Answer_Console_App
{
class Program
{
static void Main(string[] args)
{
Test();
Console.ReadKey();
}
public static void Test()
{
Console.WriteLine($"Before Task");
var task = DoWorkAsync();
var taskAwaiter = task.GetAwaiter();
taskAwaiter.OnCompleted(() => Console.WriteLine($"After Task"));
}
static public Task DoWorkAsync()
{
return Task.Run(() =>
{
Console.WriteLine($"{nameof(DoWorkAsync)} starting...");
Task.Delay(1000).Wait();
Console.WriteLine($"{nameof(DoWorkAsync)} ending...");
});
}
}
}
//OUTPUT
//Before Task
//DoWorkAsync starting...
//DoWorkAsync ending...
//After Task
课程总结:
请注意,我示例中的方法 DoWorkAsync()
只是 return 一个 Task
的函数。在我的示例中,Task
是 运行ning,因为在我使用的方法中使用 return Task.Run(() =>…
。使用关键字 await
不会改变该逻辑。完全一样; await
只做我上面提到的。
如果您有任何问题,请尽管提问,我很乐意回答。
开始和等待有什么区别?以下代码摘自 Stephen Cleary 的博客(包括评论)
public async Task DoOperationsConcurrentlyAsync()
{
Task[] tasks = new Task[3];
tasks[0] = DoOperation0Async();
tasks[1] = DoOperation1Async();
tasks[2] = DoOperation2Async();
// At this point, all three tasks are running at the same time.
// Now, we await them all.
await Task.WhenAll(tasks);
}
我认为任务会在您等待它们时开始 运行...但是代码中的注释似乎暗示并非如此。 另外,任务怎么会是运行,我只是把它们归于一个任务类型的数组。这不只是一种归因,本质上不涉及行动吗?
开始你就开始了一项任务。这意味着它可能会被任何已安装的多任务处理系统选中并执行。
等待,您等待一项任务实际完成,然后再继续。
没有即发即弃线程这样的东西。你总是需要回来,对异常做出反应或对异步操作的结果做一些事情(数据库查询或 WebQuery 结果,文件系统操作完成,文档发送到最近的打印机池)。
您可以并行启动和执行任意数量的任务 运行。但是迟早你会需要结果才能继续。
A Task
returns "hot"(即已经开始)。 await
异步等待 Task
完成。
在您的示例中,您实际执行 await
的位置将影响任务是一个接一个地 运行 还是同时完成所有任务:
await DoOperation0Async(); // start DoOperation0Async, wait for completion, then move on
await DoOperation1Async(); // start DoOperation1Async, wait for completion, then move on
await DoOperation2Async(); // start DoOperation2Async, wait for completion, then move on
相对于:
tasks[0] = DoOperation0Async(); // start DoOperation0Async, move on without waiting for completion
tasks[1] = DoOperation1Async(); // start DoOperation1Async, move on without waiting for completion
tasks[2] = DoOperation2Async(); // start DoOperation2Async, move on without waiting for completion
await Task.WhenAll(tasks); // wait for all of them to complete
更新
"doesn't
await
make anasync
operation... behave like sync, in this example (and not only)? Because we can't (!) run anything else in parallel withDoOperation0Async()
in the first case. By comparison, in the 2nd caseDoOperation0Async()
andDoOperation1Async()
run in parallel (e.g. concurrency,the main benefits ofasync
?)"
这是一个很大的主题,也是一个值得提出的问题,因为它在 SO 上有自己的主题,因为它偏离了开始任务和等待任务之间的区别的原始问题 - 因此我会在向您推荐时保持简短的回答在适当的地方找到其他答案。
不,await
执行 async
操作不会使其表现得像同步;这些关键字的作用是让开发人员能够编写 类似于 同步工作流的异步代码(有关更多信息,请参阅 Eric Lippert 的
调用 await DoOperation0Async()
不会阻塞 执行此代码流的线程,而 DoOperation0
的同步版本(或类似 DoOperation0Async.Result
) 将阻塞线程直到操作完成。
在网络环境中考虑这一点。假设请求到达服务器应用程序。作为生成对该请求的响应的一部分,您需要执行一个 long-运行 操作(例如,查询一个外部 API 以获得生成您的响应所需的一些值)。如果这个 long-运行 操作的执行是同步的,执行你的请求的线程将 block 因为它必须等待 long-运行 操作完全的。另一方面,如果这个 long-运行 操作的执行是异步的,请求线程可以被释放,这样它就可以在 long-运行 操作的同时做其他事情(比如服务其他请求)仍然是 运行。然后,当 long-运行 操作最终完成时,请求线程(或者可能是线程池中的另一个线程)可以从它停止的地方继续(因为 long-运行 操作将是完成,结果现在可用)并做剩下的任何工作来产生响应。
服务器应用程序示例还解决了您关于 async
-
Isn't that just an attribution, by nature not involving action?
通过调用异步方法,您可以执行其中的代码。通常在链下,一种方法将创建一个任务,然后 return 通过使用 return 或等待 return 它。
开始任务
您可以使用 Task.Run(...)
启动任务。这会在任务线程池上安排一些工作。
等待任务
要获得任务,您通常会调用一些 return 作为任务的(异步)方法。 async
方法的行为类似于常规方法,直到您 await
(或使用 Task.Run()
)。请注意,如果您 await 一个方法链并且 "final" 方法仅执行 Thread.Sleep()
或同步操作 - 那么您将阻塞初始调用线程,因为没有任何方法使用过任务的线程池。
您可以通过多种方式进行一些实际异步操作:
- 使用Task.Run
- 使用Task.Delay
- 使用Task.Yield
- 调用提供异步操作的库
这些是我想到的,可能还有更多。
举例
让我们假设线程 ID 1 是您从中调用 MethodA()
的主线程。线程 ID 5 及以上是 运行 任务的线程(System.Threading.Tasks 为此提供了默认调度程序)。
public async Task MethodA()
{
// Thread ID 1, 0s passed total
var a = MethodB(); // takes 1s
// Thread ID 1, 1s passed total
await Task.WhenAll(a); // takes 2s
// Thread ID 5, 3s passed total
// When the method returns, the SynchronizationContext
// can change the Thread - see below
}
public async Task MethodB()
{
// Thread ID 1, 0s passed total
Thread.Sleep(1000); // simulate blocking operation for 1s
// Thread ID 1, 1s passed total
// the await makes MethodB return a Task to MethodA
// this task is run on the Task ThreadPool
await Task.Delay(2000); // simulate async call for 2s
// Thread ID 2 (Task's pool Thread), 3s passed total
}
我们可以看到 MethodA
在 MethodB
上被阻塞,直到我们遇到 await 语句。
Await、SynchronizationContext 和控制台应用程序
您应该了解 Tasks 的一项功能。如果存在(基本上是非控制台应用程序),他们确保调用回 SynchronizationContext
。如果调用的代码不采取措施,则在任务上使用 .Result
或 .Wait()
时很容易 运行 陷入死锁。参见 https://blogs.msdn.microsoft.com/pfxteam/2012/01/20/await-synchronizationcontext-and-console-apps/
async/await 作为语法糖
await
基本上只是在调用完成后将以下代码安排到 运行。让我来说明一下幕后发生的事情。
这是使用 async/await 的未转换代码。等待 Something
方法,因此在 Something
完成后所有后续代码 (Bye
) 将是 运行。
public async Task SomethingAsync()
{
Hello();
await Something();
Bye();
}
为了解释这一点,我添加了一个实用程序 class Worker
,它只是对 运行 执行一些操作,然后在完成时通知。
public class Worker
{
private Action _action;
public event DoneHandler Done;
// skipping defining DoneHandler delegate
// store the action
public Worker(Action action) => _action = action;
public void Run()
{
// execute the action
_action();
// notify so that following code is run
Done?.Invoke();
}
}
现在我们的转换代码,不使用async/await
public Task SomethingAsync()
{
Hello(); // this remains untouched
// create the worker to run the "awaited" method
var worker = new Worker(() => Something());
// register the rest of our method
worker.Done += () => Bye();
// execute it
worker.Run();
// I left out the part where we return something
// or run the action on a threadpool to keep it simple
}
这是简短的回答:
要回答这个问题,您只需要了解 async
/ await
关键字的作用即可。
我们知道单个线程一次只能做一件事,我们也知道单个线程在整个应用程序中反弹到各种方法调用和事件等。这意味着线程接下来需要去的地方很可能在幕后某个地方被安排或排队(这是但我不会在这里解释这部分。)当一个线程调用一个方法时,那个方法是 运行在任何其他方法可以 运行 之前完成,这就是为什么长 运行ning 方法更适合分派给其他线程以防止应用程序冻结的原因。为了将单个方法分解为单独的队列,我们需要进行一些花哨的编程,或者您可以将 async
签名放在方法上。这告诉编译器,在某些时候该方法可以分解为其他方法并放入队列中以待 运行 之后。
如果这说得通,那么您已经知道 await
做了什么... await
告诉编译器这是方法将被分解并安排到 运行 以后。这就是为什么可以不使用 await
关键字而使用 async
关键字的原因;尽管编译器知道这一点并警告您。 await
通过使用 Task
.
await
如何使用 Task
告诉编译器安排方法的其余部分?当您调用 await Task
时,编译器会为您调用 Task
上的 Task.GetAwaiter()
方法。 GetAwaiter()
return一个TaskAwaiter
。 TaskAwaiter
实现了两个接口 ICriticalNotifyCompletion, INotifyCompletion
。每个都有一种方法,UnsafeOnCompleted(Action continuation)
和 OnCompleted(Action continuation)
。然后编译器包装方法的其余部分(在 await
关键字之后)并将其放入 Action
中,然后调用 OnCompleted
和 UnsafeOnCompleted
方法并传递 Action
作为参数。现在,当 Task
完成时,如果成功,它会调用 OnCompleted
,如果不成功,它会调用 UnsafeOnCompleted
,并且它会在用于启动 Task
的同一线程上下文中调用它们。它使用 ThreadContext
将线程分派给原始线程。
现在你可以理解 async
或 await
都不会执行任何 Task
。他们只是告诉编译器使用一些预先编写的代码来为您安排所有这些。实际上;您可以 await
不是 运行ning 的 Task
并且它将 await
直到 Task
被执行并完成或直到应用程序结束。
知道这个;让我们通过手动执行 async await 的操作来深入了解它。
使用异步等待
using System;
using System.Threading.Tasks;
namespace Question_Answer_Console_App
{
class Program
{
static void Main(string[] args)
{
Test();
Console.ReadKey();
}
public static async void Test()
{
Console.WriteLine($"Before Task");
await DoWorkAsync();
Console.WriteLine($"After Task");
}
static public Task DoWorkAsync()
{
return Task.Run(() =>
{
Console.WriteLine($"{nameof(DoWorkAsync)} starting...");
Task.Delay(1000).Wait();
Console.WriteLine($"{nameof(DoWorkAsync)} ending...");
});
}
}
}
//OUTPUT
//Before Task
//DoWorkAsync starting...
//DoWorkAsync ending...
//After Task
执行编译器手动执行的操作(某种程度上)
注意:虽然此代码有效,但它旨在帮助您从自上而下的角度理解异步等待。它不包含或执行编译器逐字执行的方式。
using System;
using System.Threading.Tasks;
namespace Question_Answer_Console_App
{
class Program
{
static void Main(string[] args)
{
Test();
Console.ReadKey();
}
public static void Test()
{
Console.WriteLine($"Before Task");
var task = DoWorkAsync();
var taskAwaiter = task.GetAwaiter();
taskAwaiter.OnCompleted(() => Console.WriteLine($"After Task"));
}
static public Task DoWorkAsync()
{
return Task.Run(() =>
{
Console.WriteLine($"{nameof(DoWorkAsync)} starting...");
Task.Delay(1000).Wait();
Console.WriteLine($"{nameof(DoWorkAsync)} ending...");
});
}
}
}
//OUTPUT
//Before Task
//DoWorkAsync starting...
//DoWorkAsync ending...
//After Task
课程总结:
请注意,我示例中的方法 DoWorkAsync()
只是 return 一个 Task
的函数。在我的示例中,Task
是 运行ning,因为在我使用的方法中使用 return Task.Run(() =>…
。使用关键字 await
不会改变该逻辑。完全一样; await
只做我上面提到的。
如果您有任何问题,请尽管提问,我很乐意回答。