前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >在有 UI 线程参与的同步锁(如 AutoResetEvent)内部使用 await 可能导致死锁

在有 UI 线程参与的同步锁(如 AutoResetEvent)内部使用 await 可能导致死锁

作者头像
walterlv
发布2023-10-22 09:50:03
1880
发布2023-10-22 09:50:03
举报
文章被收录于专栏:walterlv - 吕毅的博客

AutoResetEventManualResetEventMonitorlock 等等这些用来做同步的类,如果在异步上下文(await)中使用,需要非常谨慎。

本文将说一个在同步上下文中非常常见的一种用法,换成异步上下文中会产生死锁的问题。

一段正常的同步上下文的代码

先看看一段非常简单的代码:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37

private void OnLoaded(object sender, RoutedEventArgs e) { ThreadPool.SetMinThreads(100, 100); // 全部在后台线程,不会死锁。 for (var i = 0; i < 100; i++) { Task.Run(() => Do()); } // 主线程执行与后台线程并发竞争,也不会死锁。 for (var i = 0; i < 100; i++) { Do(); } } private void Do() { _resetEvent.WaitOne(); try { // 这个 ++ 在安全的线程上下文中,所以不需要使用 Interlocked.Increment(ref _count); _count++; DoCore(); } finally { _resetEvent.Set(); } } private void DoCore() { Console.WriteLine($"{_count.ToString().PadLeft(3, ' ')} walterlv is a 逗比"); }

以上代码运行会输出 200 个 “walterlv is a 逗比”:

1 2 3 4 5 6 7 8 9 10 11 12

1 walterlv is a 逗比 2 walterlv is a 逗比 3 walterlv is a 逗比 4 walterlv is a 逗比 5 walterlv is a 逗比 6 walterlv is a 逗比 7 walterlv is a 逗比 8 walterlv is a 逗比 9 walterlv is a 逗比 10 walterlv is a 逗比 // 有 200 个,但是不需要再在这里占用行数了。197 walterlv is a 逗比 200 walterlv is a 逗比

以上代码最关键的使用锁进行同步的地方是 Do 函数,采用了非常典型的防止方法重入的措施:

1 2 3 4 5 6 7 8 9

// 获得锁 try { // 执行某个需要线程安全的操作。 } finally { // 释放锁 }

我们设置了线程池最小线程数为 100,这样在使用 Task.Run 进行并发的时候,一次能够开启 100 个线程来执行 Do 方法。同时 UI 线程也执行 100 次,与后台线程竞争输出。

一个微调即会死锁

现在我们微调一下刚刚的代码:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39

private void OnLoaded(object sender, RoutedEventArgs e) { ThreadPool.SetMinThreads(100, 100); // 全部在后台线程,不会死锁。 for (var i = 0; i < 100; i++) { Task.Run(() => DoAsync()); } // 主线程执行与后台线程并发竞争,也不会死锁。 for (var i = 0; i < 100; i++) { DoAsync(); } } private async Task DoAsync() { _resetEvent.WaitOne(); try { _count++; await DoCoreAsync(); } finally { _resetEvent.Set(); } } private async Task DoCoreAsync() { await Task.Run(async () => { Console.WriteLine($"{_count.ToString().PadLeft(3, ' ')} walterlv is a 逗比"); }); }

为了直观看出差别,我只贴出不同之处:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

{ -- Task.Run(() => Do()); ++ Task.Run(() => DoAsync()); } ... { -- Do(); ++ DoAsync(); } -- private void Do() ++ private async Task DoAsync() { ... _count++; -- await DoCore(); ++ await DoCoreAsync(); } ... } -- private void DoCore() ++ private async Task DoCoreAsync() { -- Console.WriteLine($"{_count.ToString().PadLeft(3, ' ')} walterlv is a 逗比"); ++ await Task.Run(async () => ++ { ++ Console.WriteLine($"{_count.ToString().PadLeft(3, ' ')} walterlv is a 逗比"); ++ }); }

现在再运行代码,只输出几次程序就停下来了:

1 2 3 4 5 6

0 walterlv is a 逗比 1 walterlv is a 逗比 2 walterlv is a 逗比 3 walterlv is a 逗比 4 walterlv is a 逗比 5 walterlv is a 逗比

每次运行时,停下来的次数都不相同,这也正符合多线程坑的特点。

此死锁的触发条件

实际上,以上这段代码如果没有 WPF / UWP 的 UI 线程的参与,是 不会出现死锁 的。

但是,如果有 UI 线程参与,即便只有 UI 线程调用,也会直接死锁。例如:

1 2

DoAsync(); DoAsync();

只是这样的调用,你会看到值输出一次 —— 这就已经死锁了!

此死锁的原因

WPF / UWP 等 UI 线程会使用 DispatcherSynchronizationContext 作为线程同步上下文,我在 出让执行权:Task.Yield, Dispatcher.Yield - walterlv 一问中有说到它的原理。

await 等待完成之后,会调用 BeginInvoke 回到 UI 线程。然而,此时 UI 线程正卡死在 _resetEvent.WaitOne();,于是根本没有办法执行 BeginInvoke 中的操作,也就是 await 之后的代码。然而释放锁的代码 _resetEvent.Set(); 就在 await 之后,所以不会执行,于是死锁。

更多死锁问题

死锁问题:

解决方法:

本文会经常更新,请阅读原文: https://blog.walterlv.com/post/deadlock-if-await-in-ui-lock-context.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://blog.walterlv.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 ([email protected])

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018-12-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一个微调即会死锁
  • 此死锁的触发条件
  • 此死锁的原因
  • 更多死锁问题
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档