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

不要使用 Dispatcher.Invoke,因为它可能在你的延迟初始化 LazyT 中导致死锁

WPF 中为了 UI 的跨线程访问,提供了 Dispatcher 线程模型。其 Invoke 方法,无论在哪个线程调用,都可以让传入的方法回到 UI 线程。

然而,如果你在 Lazy 上下文中使用了 Invoke,那么当这个 Lazy<T> 跨线程并发时,极有可能导致死锁。本文将具体说说这个例子。


本文内容

      • 一段死锁的代码
      • 此死锁的触发条件
      • 此死锁的原因
      • 此死锁的解决方法
      • 更多死锁问题

一段死锁的代码

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

private Lazy<Walterlv> _walterlvLazy = new Lazy<Walterlv>(() => new Walterlv());

private void OnLoaded(object sender, RoutedEventArgs e)
{
    Task.Run(() =>
    {
        // 在后台线程通过 Lazy 获取。
        var backgroundWalterlv = _walterlvLazy.Value;
    });

    // 等待一个时间,这样可以确保后台线程先访问到 Lazy,并且在完成之前,UI 线程也能访问到 Lazy。
    Thread.Sleep(50);

    // 在主线程通过 Lazy 获取。
    var walterlv = _walterlvLazy.Value;
}

而其中的 Walterlv 类的定义也是非常简单的:

class Walterlv
{
    public Walterlv()
    {
        // 等待一段时间,是为了给我么的测试程序一个准确的时机。
        Thread.Sleep(100);

        // Invoke 到主线程执行,里面什么都不做是为了证明绝不是里面代码带来的影响。
        Application.Current.Dispatcher.Invoke(() =>
        {
        });
    }
}

这里的 Application.Current.Dispatcher 并不一定必须是 Application.Current,只要是两个不同线程拿到的 Dispatcher 的实例是同一个,就会死锁。

此死锁的触发条件

  1. Lazy<T> 的线程安全参数设置为默认的,也就是 LazyThreadSafetyMode.ExecutionAndPublication
  2. 后台线程和主 UI 线程并发访问这个 Lazy<T>,且后台线程先于主 UI 线程访问这个 Lazy<T>
  3. Lazy<T> 内部的代码包含主线程的 Invoke

此死锁的原因

  1. 后台线程访问到 Lazy,于是 Lazy 内部获得同步锁;
  2. 主 UI 线程访问到 Lazy,于是主 UI 线程等待同步锁完成,并进入阻塞状态(以至于不能处理消息循环);
  3. 后台线程的初始化调用到 Invoke 需要到 UI 线程完成指定的任务后才会返回,但 UI 线程此时阻塞不能处理消息循环,以至于无法完成 Invoke 内的任务;

于是,后台线程在等待 UI 线程处理消息以便让 Invoke 完成,而主 UI 线程由于进入 Lazy 的等待,于是不能完成 Invoke 中的任务;于是发生死锁。

此死锁的解决方法

Invoke 改为 InvokeAsync 便能解锁。

这么做能解决的原因是:后台线程能够及时返回,这样 UI 线程便能够继续执行,包括执行 InvokeAsync 中传入的任务。

实际上,以上可能是最好的解决办法了。因为:

  1. 我们使用 Lazy 并且设置线程安全,一定是因为这个初始化过程会被多个线程访问;
  2. 我们会在 Lazy 的初始化代码中使用回到主线程的 Invoke,也是因为我们预料到这份初始化代码可能在后台线程执行。

所以,这段初始化代码既然不可避免地会并发,那么就应该阻止并发造成的死锁问题。也就是不要使用 Invoke 而是改用 InvokeAsync

如果需要使用 Invoke 的返回值,那么改为 InvokeAsync 之后,可以使用 await 异步等待返回值。

更多死锁问题

死锁问题:

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

解决方法:

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

相关文章:

  • 定义一组抽象的 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 分析语法树时添加条件编译符号的支持
  • 自然码的形码
  • 出于迁移项目的考虑,GitHub 中 Fork 出来的项目,如何与原项目断开 Fork 关系?
  • “Material Design”设计规范在 ComponentOne For WinForm 的全新尝试!
  • 【知识碎片】第三方登录弹窗效果
  • Android 初级面试者拾遗(前台界面篇)之 Activity 和 Fragment
  • Apache的基本使用
  • CentOS从零开始部署Nodejs项目
  • CSS 专业技巧
  • Fastjson的基本使用方法大全
  • gcc介绍及安装
  • JAVA并发编程--1.基础概念
  • Java基本数据类型之Number
  • Median of Two Sorted Arrays
  • node-sass 安装卡在 node scripts/install.js 解决办法
  • NSTimer学习笔记
  • PV统计优化设计
  • rabbitmq延迟消息示例
  • spring cloud gateway 源码解析(4)跨域问题处理
  • vue.js框架原理浅析
  • Yeoman_Bower_Grunt
  • 持续集成与持续部署宝典Part 2:创建持续集成流水线
  • 从0到1:PostCSS 插件开发最佳实践
  • 小程序、APP Store 需要的 SSL 证书是个什么东西?
  • 一道闭包题引发的思考
  • 一个JAVA程序员成长之路分享
  • const的用法,特别是用在函数前面与后面的区别
  • 没有任何编程基础可以直接学习python语言吗?学会后能够做什么? ...
  • 移动端高清、多屏适配方案
  • (day 2)JavaScript学习笔记(基础之变量、常量和注释)
  • (Matalb时序预测)PSO-BP粒子群算法优化BP神经网络的多维时序回归预测
  • (附源码)springboot宠物管理系统 毕业设计 121654
  • (附源码)基于ssm的模具配件账单管理系统 毕业设计 081848
  • (三)uboot源码分析
  • (转)iOS字体
  • (转载)虚幻引擎3--【UnrealScript教程】章节一:20.location和rotation
  • .net 程序发生了一个不可捕获的异常
  • .NET 的静态构造函数是否线程安全?答案是肯定的!
  • .NET 反射 Reflect
  • .net 简单实现MD5
  • .NET中 MVC 工厂模式浅析
  • [ 常用工具篇 ] POC-bomber 漏洞检测工具安装及使用详解
  • [1]-基于图搜索的路径规划基础