async await 理解
异步编程之Async,Await和ConfigureAwait的关系 - Leon_Chaunce - 博客园 (cnblogs.com)
理解C#中的ConfigureAwait - xiaoxiaotank - 博客园 (cnblogs.com)
必死代码
async/await Task 被调用时会在调用方的上下文中(相同线程),await 在代码空闲时让出当前线程给其它代码运行(减少线程切换的开销)。
所以,async/await task 不是并行/多核编程。 async/await 不引发线程切换
async/await 调用完成后会尝试返回当前调用上下文 (调用 SynchronizationContext.Current.post )。这就是同步调用异步方法时产生死锁的原因
因为 .net core 已经取消了 SynchronizationContext, 所以没有死锁问题
private async Task<int> TaskCoreAsync(int trytimes = 5)
{
// 3.
for (var idx = 0; idx < trytimes; idx++)
{
await Task.Delay(TimeSpan.FromSeconds(0.5)); // 4,5
}
}
void OnFormOnLoad(object sender, EventArgs args) // 1.
{
var task = this.TaskCoreAsync(10); // 2.
task.Wait();
}
## 解释一下,
## 1. OnFormOnLoad 中为同步方法,比如线程编号为 14,
## 2. 这时以同步方式调用 TaskCoreAsync 并在线程 14 上等待
## 3. TaskCoreAsync 在调用者线程下运行 , 线程 14
## 4. 在调用 14 下调用 Task.Delay
## 5. Task.Delay 调用完成尝试返回当前线程 14 ,但是因为 2 已经在线程 14 上等待,线程卡死
不会死锁的方案
## 方法 1
// 从事件开始至结束都使用 async/await 就不会死锁,而且上下文都在主线程上
async Task OnFormOnLoad(object sender, EventArgs args) + var result = await this.TaskCoreAsync(10); // 这样调用不会死锁
## 方法 2
// 调用 ConfigureAwait(false), 不强制返回当前上下文
// 1. 线程 11
await this.SayHelloAsync().ConfigureAwait(false); // 2. SayHelloAsync 内部 线程11
// 3. 如果有其它在线程 11 等待, 则当前上下文可能在线程 13
#### 特别注意
如果 SayHelloAsync 中调用 async/await ,那也要 ConfigureAwait(false), 因为该函数也会要求返回当前线程
简单说就是所有 async/await 调用最好都调用 ConfigureAwait(false)
## 方法 3
var task = Task.Run(() => this.TaskCoreAsync(10)); // 这样调用不会死锁
var task = Task.Run(async () => await this.TaskCoreAsync(10)); // 这样调用不会死锁
## 注意 4
// 这样会死锁,因为 TaskCoreAsync 还是在当前上下文中工作的
async Task OnFormOnLoad(object sender, EventArgs args) + var task = this.TaskCoreAsync(10) + task.Wait(); // 死锁
.net core
听说 ~~~ 已经取消 SynchronizationContext 所以没有死锁问题 ~~~ , 假的。某一些类库可能没有,不要做这个假设
处理细节
TASK 处理
SynchronizationContext
提供一个同步上下文。在需要的时候,可以使用该上下文进行一些调用,比如在最初始的上下文中执行命令。使用方法就是调用 SynchronizationContext.Post
默认实现是调用 ThreadPool.QueueUserWorkItem , 丢到线程池中处理
winform 实现 WindowsFormsSynchronizationContext 调用 Control.BeginInvoke, 丢到 UI 消息队列中处理 (即 UI 线程中处理)
WPF 实现 DispatcherSynchronizationContext 调用 Dispatcher.BeginInvoke 送到 UI 线程处理
WinRT 也是一样,调用 CoreDispatcher 送到 UI 线程处理
获取当前上下文 SynchronizationContext.Current
设置当前上下文 SynchronizationContext.SetSynchronizationContext
一个简单例子
public void DoWork(Action worker, Action completion)
{
SynchronizationContext sc = SynchronizationContext.Current; // 在调用前获取上下文
ThreadPool.QueueUserWorkItem(_ =>
{
try
{
worker();
}
finally
{
sc.Post(_ => completion(), null); // 调用完成后,简单调用上下文的 Post
}
});
}
TaskScheduler
提供一个任务排序工作机制,即调度工作。使用方法是调用 TaskScheduler.QueueTask
- 默认 TaskScheduler.Default 返回 ThreadPoolTaskScheduler, 丢到线程池中处理
- System.Threading.Tasks.ConcurrentExclusiveSchedulerPair.ExclusiveScheduler 排它调度,一次只运行一个 task
- System.Threading.Tasks.ConcurrentExclusiveSchedulerPair.ConcurrentScheduler 并发调度(初始化时要指定最大并发数)
- TaskScheduler.FromCurrentSynchronizationContext 返回 SynchronizationContextTaskScheduler, 内部调用 SynchronizationContext.Current.Post 将任务丢进去处理
获取当前调度器 TaskScheduler.Current
不提供设置功能
一个简单例子
var cesp = new ConcurrentExclusiveSchedulerPair(); // 1. 新建一个调度器
Task.Factory.StartNew(() =>
{
// 3. task 中 TaskScheduler.Current 即为传入的调度器
Console.WriteLine(TaskScheduler.Current == cesp.ExclusiveScheduler); // 这里为 true
}, default, TaskCreationOptions.None, cesp.ExclusiveScheduler) // 2. 创建 task 时提供调度器
.Wait();
async/await
返回当前上下文调用
当我们在 ui 线程中工作时(比如下载页面),希望工作完成后在 ui 线程中设置界面元素
实现有如下 3 个方法,async/await 最为优雅
## 方法 1
private void downloadBtn_Click(object sender, RoutedEventArgs e)
{
SynchronizationContext sc = SynchronizationContext.Current; // 1. 获取当前上下文,默认是 ui 线程上下文,
s_httpClient.GetStringAsync("http://example.com/currenttime").ContinueWith(downloadTask =>
{
// 2. 工作完成后使用保存的当前工作上下文进行 post, 本例中为 ui 线程中
sc.Post(delegate { downloadBtn.Content = downloadTask.Result;}, null);
});
}
## 方法 2
private void downloadBtn_Click(object sender, RoutedEventArgs e)
{
// 1. 创建一个 SynchronizationContext.Current 对应的调度器
var scheduler = TaskScheduler.FromCurrentSynchronizationContext();
s_httpClient.GetStringAsync("http://example.com/currenttime").ContinueWith(downloadTask =>
{
// 当前 task 运行在 ui 线程中,因为是 1 中的 SynchronizationContext.Current.post
downloadBtn.Content = downloadTask.Result;
}, scheduler); // 2. 指定 ContinueWith 的 task 的调度器
}
## 方法 3
见 《async/await 调用 》
async/await 调用 及 ConfigureAwait(false)
private async void downloadBtn_Click(object sender, RoutedEventArgs e)
{
// 1. await 调用前保存 SynchronizationContext.Current 与 TaskScheduler.Current
// 2. 调用 Task.GetAwaiter() 返回 TaskAwaiter<T>,在其中保存上下文与调度器
// 3. TaskAwaiter 挂起回调,在等待对象完成后在保存的上下文或调度器中运行
// 4. 大致理解,有上下文,在上下文中 post 运行,如果上下文在调度器中运行(线程池)
string text = await s_httpClient.GetStringAsync("http://example.com/currenttime");
downloadBtn.Content = text;
}
## 调用 ConfigureAwait(false) 改变如下行为
// 上面的 4 中放弃保存的上下文或调度器,这样就避免强制在原始上下文或调度程序中进行回调,
// await task().ConfigureAwait(false) , ConfigureAwait(false) 中返回的 TaskAwaiter 进行了特别处理
## 好处 1 不在查询当前上下文,不进行排队,直接调用回调,提升性能
## 好处 2 避免了死锁,因为运行上下文可能会有调用并发限制,当前线程中正在调用 TaskAwaiter.Wait(), 而 await 的代码调用完成后再次调用上下文 post 尝试再次返回当前线程中执行,这时,因为并发限制,产生死锁。就算并发数有多个,因为多次调用 await 会导致多次等待直到锁死
调用 ConfigureAwait(true)
一般不要主动调用,它是默认方式
调用 ConfigureAwait(false)
- 在应用程序中一般不使用 ConfigureAwait(false) ,因为大多数时候希望回到当前上下文中。比如在事件处理中的 ui 线程中,调用完成后,一般也希望再次回到 ui 线程中进行后续代码(比如设置界面元素),如果调用 ConfigureAwait(false) ,那么可能会回到其它线程中,设置界面元素会出错
csharp
private async void downloadBtn_Click(object sender, RoutedEventArgs e)
{
// 这里就不要调用 ConfigureAwait(false) , 如果调用的话 downloadBtn.Content = text 会报异常
string text = await s_httpClient.GetStringAsync("http://example.com/currenttime");
downloadBtn.Content = text;
}
- 在通用库中一般要调用 ConfigureAwait(false)。因为通用库不希望与界面 UI 有关,所以 所有 await 中都要调用 ConfigureAwait(false) 。否则被 ui 线程调用 时地,一不小心就会死锁
markdown
也可能会被传入一些回调函数,而这些回调整函数可能是在 ui 上下文中运行的。这样就会有漏洞
-
ConfigureAwait(false) 并不总是不回到当前上下文中,比如 await taskAsync().ConfigureAwait(false), 而 taskAsync() 返回的 task 已经完成任务,则不会进行排队等待,代码直接同步运行下去。这时就在相同的上下文中运行了
-
方法第一个 await 使用 ConfigureAwait(false) ,其它不使用可以吗, 最好不要。
markdown
除非,第一个 await 后明确不会回到当前上下文,而且其它 await 也不会引用最初的上下文。所以最好不要,因为不小心就会回到最初的上下文中(UI 线程)
- 通过 Task.Run 防止死锁。 可以,就是会有性能损失。而且注意不要将当前上下文引用到 Task 中,比如指定 TaskScheduler.FromCurrentSynchronizationContext 为 Scheduler (这个我没有测试,应该是会死锁)
``` csharp Task.Run(async delegate { await SomethingAsync(); // await 后不会找到原始上下文,没必要 ConfigureAwait(false) });
```
- 调用 SynchronizationContext.SetSynchronizationContext(null) 是否可以防止死锁 。 最好不要这样。因为该代码不影响 TaskScheduler, 所以还是有可能会死锁。而且见如下代码
csharp
var old = SynchronizationContext.Current; ## 1. 保存当前上下文, 线程 1
SynchronizationContext.SetSynchronizationContext(null); ## 2. 将当前上下文置空, 线程 1
try
{
await t; ## 3. 等待后返回的可能不是当前上下文了, 线程 1
## 4. 线程就不是前面的上下文了,线程 14
}
finally
{
## 5. 希望重置回原始上下文,但该代码在线程 14 下运行,当前环境非最初始的上下文了,所以可能会有意外发生,所以不要这么调用
## 被 set 的上下文已经不是在 1 那里 current 的上下文了
SynchronizationContext.SetSynchronizationContext(old);
}
- awaiter 的一些细节
markdown
awaiters 公开IsCompleted属性、GetResult方法和OnCompleted方法(可选使用UnsafeOnCompleted方法), 其中 ConfigureAwait只会影响OnCompleted/UnsafeOnCompleted 所以调用 GetResult 不需要 ConfigureAwait, 所以 task.ConfigureAwait(false).GetAwaiter().GetResult(),则可以将其替换为task.GetAwaiter().GetResult(), 两者等价
- 如何 await using一个IAsyncDisposable
``` csharp // 错误, 这里 c 是 ConfiguredAsyncDisposable await using (var c = new MyAsyncDisposableClass().ConfigureAwait(false)) { ... }
// 要这么写 var c = new MyAsyncDisposableClass(); await using (c.ConfigureAwait(false)) { ... } ```