当前位置: 首页 > news >正文

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

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

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


本文内容

      • 一段正常的同步上下文的代码
      • 一个微调即会死锁
      • 此死锁的触发条件
      • 此死锁的原因
      • 更多死锁问题

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

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

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] 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 函数,采用了非常典型的防止方法重入的措施:

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

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

一个微调即会死锁

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

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 逗比");
    });
}

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

        {
--          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 逗比");
++      });
    }

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

[  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 线程调用,也会直接死锁。例如:

DoAsync();
DoAsync();

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

此死锁的原因

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

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

更多死锁问题

死锁问题:

  • 使用 Task.Wait()?立刻死锁(deadlock) - walterlv
  • 不要使用 Dispatcher.Invoke,因为它可能在你的延迟初始化 Lazy 中导致死锁 - walterlv
  • 在有 UI 线程参与的同步锁(如 AutoResetEvent)内部使用 await 可能导致死锁
  • .NET 中小心嵌套等待的 Task,它可能会耗尽你线程池的现有资源,出现类似死锁的情况 - walterlv

解决方法:

  • 在编写异步方法时,使用 ConfigureAwait(false) 避免使用者死锁 - walterlv
  • 将 async/await 异步代码转换为安全的不会死锁的同步代码(使用 PushFrame) - walterlv

相关文章:

  • 不要使用 Dispatcher.Invoke,因为它可能在你的延迟初始化 LazyT 中导致死锁
  • 定义一组抽象的 Awaiter 的实现接口,你下次写自己的 await 可等待对象时将更加方便
  • .NET 除了用 Task 之外,如何自己写一个可以 await 的对象?
  • .NET 中什么样的类是可使用 await 异步等待的?
  • Visual Studio 2017 以前的旧格式的 csproj Import 进来的 targets 文件有时不能正确计算属性(PropertyGroup)和集合(ItemGroup)
  • 使用 ReSharper,输入即遵循 StyleCop 的代码格式化规范
  • StyleCop 是什么,可以帮助团队带来什么价值?
  • 文件和文件夹不存在的时候,FileSystemWatcher 监听不到文件的改变?如果递归地监听就可以了
  • C#/.NET 使用 CommandLineParser 来标准化地解析命令行
  • .NET 中使用 TaskCompletionSource 作为线程同步互斥或异步操作的事件
  • 使用 WPF 开发一个 Windows 屏幕保护程序
  • 在 Windows 10 中开启移动 WLAN 热点
  • .NET/C# 项目如何优雅地设置条件编译符号?
  • 在 Roslyn 分析语法树时添加条件编译符号的支持
  • 自然码的形码
  • IE9 : DOM Exception: INVALID_CHARACTER_ERR (5)
  • android图片蒙层
  • Fastjson的基本使用方法大全
  • FastReport在线报表设计器工作原理
  • Javascript设计模式学习之Observer(观察者)模式
  • NLPIR语义挖掘平台推动行业大数据应用服务
  • PermissionScope Swift4 兼容问题
  • Quartz实现数据同步 | 从0开始构建SpringCloud微服务(3)
  • SQLServer之创建显式事务
  • webgl (原生)基础入门指南【一】
  • 数据科学 第 3 章 11 字符串处理
  • 腾讯大梁:DevOps最后一棒,有效构建海量运营的持续反馈能力
  • 一些css基础学习笔记
  • 阿里云ACE认证学习知识点梳理
  • ​Python 3 新特性:类型注解
  • # 飞书APP集成平台-数字化落地
  • #{} 和 ${}区别
  • (1)Android开发优化---------UI优化
  • (11)MSP430F5529 定时器B
  • (2021|NIPS,扩散,无条件分数估计,条件分数估计)无分类器引导扩散
  • (delphi11最新学习资料) Object Pascal 学习笔记---第2章第五节(日期和时间)
  • (八)光盘的挂载与解挂、挂载CentOS镜像、rpm安装软件详细学习笔记
  • (全注解开发)学习Spring-MVC的第三天
  • (十八)SpringBoot之发送QQ邮件
  • (四)Android布局类型(线性布局LinearLayout)
  • (四)鸿鹄云架构一服务注册中心
  • (四)七种元启发算法(DBO、LO、SWO、COA、LSO、KOA、GRO)求解无人机路径规划MATLAB
  • (一)【Jmeter】JDK及Jmeter的安装部署及简单配置
  • (译)2019年前端性能优化清单 — 下篇
  • .Net - 类的介绍
  • .NET Core WebAPI中使用swagger版本控制,添加注释
  • .net core 依赖注入的基本用发
  • .Net Remoting(分离服务程序实现) - Part.3
  • .NET Remoting学习笔记(三)信道
  • .net 提取注释生成API文档 帮助文档
  • .net 逐行读取大文本文件_如何使用 Java 灵活读取 Excel 内容 ?
  • .NET开源项目介绍及资源推荐:数据持久层
  • @private @protected @public
  • [20171106]配置客户端连接注意.txt
  • [AIGC 大数据基础]hive浅谈