Async/Await:为什么await后面的代码也是在后台线程执行,而不是在原来的主线程执行?
Async/ Await: why does the code that follows await is also executed on the background thread and not on the original primary thread?
下面是我的代码:
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
string message = await DoWorkAsync();
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(message);
}
static async Task<string> DoWorkAsync()
{
return await Task.Run(() =>
{
Thread.Sleep(3_000);
return "Done with work!";
});
}
}
输出为
1
// 3 秒后
3
Done with work!
所以可以看到主线程(id为1)变成了工作线程(id为3),怎么主线程就这么消失了?
异步入口点只是一个编译器技巧。在幕后,编译器生成了这个真正的入口点:
private static void <Main>(string[] args)
{
_Main(args).GetAwaiter().GetResult();
}
如果你把代码改成这样:
class Program
{
private static void Main(string[] args)
{
MainAsync(args).GetAwaiter().GetResult();
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
}
static async Task MainAsync(string[] args)
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
string message = await DoWorkAsync();
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(message);
}
static async Task<string> DoWorkAsync()
{
await Task.Delay(3_000);
return "Done with work!";
}
}
你会得到这个:
1
4
Done with work!
1
不出所料,主线程正在等待工作完成。
在您的代码中,您的主线程在此处调用 await
后结束:
string message = await DoWorkAsync();
由于DoWorkAsync()
创建了一个任务,所以执行会出现分歧,并且该调用之后的所有代码都将在新创建的任务中执行,然后主线程在调用await DoWorkAsync();
后无事可做所以它会完成。
这是您选择的应用程序类型的结果。控制台应用程序和 GUI 应用程序在 SynchronizationContext
方面的行为不同。当您使用 await
时,当前 SynchronizationContext
将被捕获并传递给后台线程。
这个想法不是通过等待后台线程完成来阻塞主线程。剩余代码被排队,当前上下文存储在背景线程将捕获的 SynchronizationContext
中。当后台线程完成时,它 return 捕获捕获的 SynchronizationContext
以便排队的剩余代码可以恢复执行。您可以通过访问 SynchronizationContext.Current
属性 来获取当前上下文。等待 await
完成的代码(await
之后的剩余代码)将作为延续入队并在捕获的 SynchronizationContext
上执行。
SynchronizationContext.Current
的默认值是 GUI 应用程序的 UI 线程,例如 WPF 或控制台应用程序的 NULL。控制台应用程序没有 SynchronizationContext
,因此为了能够使用 async
,框架使用 ThreadPool
SynchronizationContext
。 SynchronizationContext
行为的规则是
- 如果
SynchronizationContext.Current
returns为NULL,则
延续线程将默认为线程池线程
- 如果
SynchronizationContext.Current
不为NULL,继续
将在捕获的上下文中执行。
- 并且:如果
await
在后台线程上使用(因此一个新的
后台线程是从后台线程启动的),然后
SynchronizationContext
永远是线程池线程。
场景一,控制台应用程序:
规则 1) 适用:线程 1 调用 await
将尝试捕获当前上下文。 await
将使用来自 ThreadPool
的后台线程 线程 3 来执行异步委托。
一旦委托完成,调用线程的剩余代码将在捕获的上下文中执行。由于此上下文在控制台应用程序中为 NULL,因此默认 SynchronizationContext
将生效(第一条规则)。因此,调度程序决定继续在 ThreadPool
线程 线程 3 上执行(为了提高效率。上下文切换很昂贵)。
场景二,一个GUI应用:
规则 2) 适用:线程 1 调用 await
,它将尝试捕获当前上下文(UI SynchronizationContext
)。 await
将使用来自 ThreadPool
的后台线程 线程 3 来执行异步委托。
委托完成后,调用线程的剩余代码将在捕获的上下文 UI SynchronizationContext
thread 1 上执行。
场景3,一个GUI应用和Task.ContinueWith
:
规则 2) 和规则 3) 适用:线程 1 调用 await
,它将尝试捕获当前上下文(UI SynchronizationContext
)。 await
将使用来自 ThreadPool
的后台线程 线程 3 来执行异步委托。委托完成后,继续TaskContinueWith
。由于我们还在后台线程,一个新的TreadPool
线程线程4与线程3[=178捕获的SynchronizationContext
一起使用=].一旦继续完成上下文 returns 到 thread 3,它将在捕获的 SynchronizationContext
上执行调用者的剩余代码,即 UI线程 线程 1.
场景4,一个GUI应用和Task.ConfigureAwait(false)
(await DoWorkAsync().ConfigureAwait(false);
):
规则 1) 适用:线程 1 调用 await
并在 ThreadPool
后台线程 线程 3 上执行异步委托。但是因为任务配置了 Task.ConfigureAwait(false)
thread 3 没有捕获调用者的 SynchronizationContext
(UI SynchronizationContext
)。因此,线程 3 的 SynchronizationContext.Current
属性 将为 NULL,并且应用默认值 SynchronizationContext
:上下文将是一个 ThreadPool
线程。由于性能优化(上下文切换很昂贵),上下文将是 线程 3 的当前 SynchronizationContext
。这意味着一旦 thread 3 完成,hte caller 的剩余代码将在默认的 SynchronizationContext
thread 3 上执行。默认的 Task.ConfigureAwait
值为 true
,它可以捕获调用者 SynchronizationContext
.
场景 5,一个 GUI 应用程序和 Task.Wait
、Task.Result
或 Task.GetAwaiter.GetResult
:
规则 2 适用,但应用程序将死锁。 线程 1 的当前 SynchronizationContext
被捕获。但是因为异步delegate是同步执行的(Task.Wait
、Task.Result
或者Task.GetAwaiter.GetResult
都会把异步操作变成delegate的同步执行),thread 1 将阻塞,直到现在的同步委托完成。
由于代码是同步执行的,因此 线程 1 的剩余代码未作为 线程 3 的延续加入队列,因此将在 上执行线程 1 一旦委托完成。现在 thread 3 上的委托已完成,它不能 return thread 1 的 SynchronizationContext
线程 1,因为 线程 1 仍在阻塞(因此锁定了 SynchronizationContext
)。 线程 3 将无限等待 线程 1 释放 SynchronizationContext
上的锁,这反过来又使 线程 1 无限等待 线程 3 到 return --> 死锁。
场景 6,控制台应用程序和 Task.Wait
、Task.Result
或 Task.GetAwaiter.GetResult
:
规则 1 适用。 线程 1 的当前 SynchronizationContext
被捕获。但是因为这是一个控制台应用程序,所以上下文为 NULL 并且应用默认值 SynchronizationContext
。异步委托在ThreadPool
后台线程线程3上同步执行(Task.Wait
、Task.Result
或Task.GetAwaiter.GetResult
会将异步操作变成同步操作) 和 线程 1 将阻塞,直到 线程 3 上的委托完成。由于代码是同步执行的,因此剩余代码不会作为 thread 3 的延续进行排队,因此一旦委托完成,将在 thread 1 上执行。在控制台应用程序的情况下没有死锁情况,因为 thread 1 的 SynchronizationContext
为 NULL 并且 thread 3 必须使用默认值上下文。
您的示例代码与场景 1 匹配。它#s 因为您是 运行 控制台应用程序和应用的默认 SynchronizationContext
因为控制台应用程序的 SynchronizationContext
总是无效的。当捕获的 SynchronizationContext
为 NULL 时,Task
使用默认上下文,即 ThreadPool
的线程。由于异步委托已经在 ThreadPool
线程上执行,因此 TaskScheduler
决定留在该线程上并因此执行调用者线程 thread 1[=178= 的排队剩余代码] 在 线程 3.
在 GUI 应用程序中,最佳做法是始终在任何地方使用 Task.ConfigureAwait(false)
,除非您明确想要捕获调用者的 SynchronizationContext
。这将防止您的应用程序出现意外死锁。
下面是我的代码:
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
string message = await DoWorkAsync();
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(message);
}
static async Task<string> DoWorkAsync()
{
return await Task.Run(() =>
{
Thread.Sleep(3_000);
return "Done with work!";
});
}
}
输出为
1
// 3 秒后
3
Done with work!
所以可以看到主线程(id为1)变成了工作线程(id为3),怎么主线程就这么消失了?
异步入口点只是一个编译器技巧。在幕后,编译器生成了这个真正的入口点:
private static void <Main>(string[] args)
{
_Main(args).GetAwaiter().GetResult();
}
如果你把代码改成这样:
class Program
{
private static void Main(string[] args)
{
MainAsync(args).GetAwaiter().GetResult();
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
}
static async Task MainAsync(string[] args)
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
string message = await DoWorkAsync();
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(message);
}
static async Task<string> DoWorkAsync()
{
await Task.Delay(3_000);
return "Done with work!";
}
}
你会得到这个:
1
4
Done with work!
1
不出所料,主线程正在等待工作完成。
在您的代码中,您的主线程在此处调用 await
后结束:
string message = await DoWorkAsync();
由于DoWorkAsync()
创建了一个任务,所以执行会出现分歧,并且该调用之后的所有代码都将在新创建的任务中执行,然后主线程在调用await DoWorkAsync();
后无事可做所以它会完成。
这是您选择的应用程序类型的结果。控制台应用程序和 GUI 应用程序在 SynchronizationContext
方面的行为不同。当您使用 await
时,当前 SynchronizationContext
将被捕获并传递给后台线程。
这个想法不是通过等待后台线程完成来阻塞主线程。剩余代码被排队,当前上下文存储在背景线程将捕获的 SynchronizationContext
中。当后台线程完成时,它 return 捕获捕获的 SynchronizationContext
以便排队的剩余代码可以恢复执行。您可以通过访问 SynchronizationContext.Current
属性 来获取当前上下文。等待 await
完成的代码(await
之后的剩余代码)将作为延续入队并在捕获的 SynchronizationContext
上执行。
SynchronizationContext.Current
的默认值是 GUI 应用程序的 UI 线程,例如 WPF 或控制台应用程序的 NULL。控制台应用程序没有 SynchronizationContext
,因此为了能够使用 async
,框架使用 ThreadPool
SynchronizationContext
。 SynchronizationContext
行为的规则是
- 如果
SynchronizationContext.Current
returns为NULL,则 延续线程将默认为线程池线程 - 如果
SynchronizationContext.Current
不为NULL,继续 将在捕获的上下文中执行。 - 并且:如果
await
在后台线程上使用(因此一个新的 后台线程是从后台线程启动的),然后SynchronizationContext
永远是线程池线程。
场景一,控制台应用程序:
规则 1) 适用:线程 1 调用 await
将尝试捕获当前上下文。 await
将使用来自 ThreadPool
的后台线程 线程 3 来执行异步委托。
一旦委托完成,调用线程的剩余代码将在捕获的上下文中执行。由于此上下文在控制台应用程序中为 NULL,因此默认 SynchronizationContext
将生效(第一条规则)。因此,调度程序决定继续在 ThreadPool
线程 线程 3 上执行(为了提高效率。上下文切换很昂贵)。
场景二,一个GUI应用:
规则 2) 适用:线程 1 调用 await
,它将尝试捕获当前上下文(UI SynchronizationContext
)。 await
将使用来自 ThreadPool
的后台线程 线程 3 来执行异步委托。
委托完成后,调用线程的剩余代码将在捕获的上下文 UI SynchronizationContext
thread 1 上执行。
场景3,一个GUI应用和Task.ContinueWith
:
规则 2) 和规则 3) 适用:线程 1 调用 await
,它将尝试捕获当前上下文(UI SynchronizationContext
)。 await
将使用来自 ThreadPool
的后台线程 线程 3 来执行异步委托。委托完成后,继续TaskContinueWith
。由于我们还在后台线程,一个新的TreadPool
线程线程4与线程3[=178捕获的SynchronizationContext
一起使用=].一旦继续完成上下文 returns 到 thread 3,它将在捕获的 SynchronizationContext
上执行调用者的剩余代码,即 UI线程 线程 1.
场景4,一个GUI应用和Task.ConfigureAwait(false)
(await DoWorkAsync().ConfigureAwait(false);
):
规则 1) 适用:线程 1 调用 await
并在 ThreadPool
后台线程 线程 3 上执行异步委托。但是因为任务配置了 Task.ConfigureAwait(false)
thread 3 没有捕获调用者的 SynchronizationContext
(UI SynchronizationContext
)。因此,线程 3 的 SynchronizationContext.Current
属性 将为 NULL,并且应用默认值 SynchronizationContext
:上下文将是一个 ThreadPool
线程。由于性能优化(上下文切换很昂贵),上下文将是 线程 3 的当前 SynchronizationContext
。这意味着一旦 thread 3 完成,hte caller 的剩余代码将在默认的 SynchronizationContext
thread 3 上执行。默认的 Task.ConfigureAwait
值为 true
,它可以捕获调用者 SynchronizationContext
.
场景 5,一个 GUI 应用程序和 Task.Wait
、Task.Result
或 Task.GetAwaiter.GetResult
:
规则 2 适用,但应用程序将死锁。 线程 1 的当前 SynchronizationContext
被捕获。但是因为异步delegate是同步执行的(Task.Wait
、Task.Result
或者Task.GetAwaiter.GetResult
都会把异步操作变成delegate的同步执行),thread 1 将阻塞,直到现在的同步委托完成。
由于代码是同步执行的,因此 线程 1 的剩余代码未作为 线程 3 的延续加入队列,因此将在 上执行线程 1 一旦委托完成。现在 thread 3 上的委托已完成,它不能 return thread 1 的 SynchronizationContext
线程 1,因为 线程 1 仍在阻塞(因此锁定了 SynchronizationContext
)。 线程 3 将无限等待 线程 1 释放 SynchronizationContext
上的锁,这反过来又使 线程 1 无限等待 线程 3 到 return --> 死锁。
场景 6,控制台应用程序和 Task.Wait
、Task.Result
或 Task.GetAwaiter.GetResult
:
规则 1 适用。 线程 1 的当前 SynchronizationContext
被捕获。但是因为这是一个控制台应用程序,所以上下文为 NULL 并且应用默认值 SynchronizationContext
。异步委托在ThreadPool
后台线程线程3上同步执行(Task.Wait
、Task.Result
或Task.GetAwaiter.GetResult
会将异步操作变成同步操作) 和 线程 1 将阻塞,直到 线程 3 上的委托完成。由于代码是同步执行的,因此剩余代码不会作为 thread 3 的延续进行排队,因此一旦委托完成,将在 thread 1 上执行。在控制台应用程序的情况下没有死锁情况,因为 thread 1 的 SynchronizationContext
为 NULL 并且 thread 3 必须使用默认值上下文。
您的示例代码与场景 1 匹配。它#s 因为您是 运行 控制台应用程序和应用的默认 SynchronizationContext
因为控制台应用程序的 SynchronizationContext
总是无效的。当捕获的 SynchronizationContext
为 NULL 时,Task
使用默认上下文,即 ThreadPool
的线程。由于异步委托已经在 ThreadPool
线程上执行,因此 TaskScheduler
决定留在该线程上并因此执行调用者线程 thread 1[=178= 的排队剩余代码] 在 线程 3.
在 GUI 应用程序中,最佳做法是始终在任何地方使用 Task.ConfigureAwait(false)
,除非您明确想要捕获调用者的 SynchronizationContext
。这将防止您的应用程序出现意外死锁。