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 之后,所以不会执行,于是死锁。

更多死锁问题

死锁问题:

解决方法:

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

  1. Java线程状态及同步锁

    线程的生命历程 线程的五大状态 创建状态:简而言之,当创建线程对象的代码出现的时候,此时线程就进入了创建状态.这时候的线程只是行代码而已.只有调用线程的start()方法时,线程的状态才会改变,进入就 ...

  2. Java基础学习笔记: 多线程,线程池,同步锁(Lock,synchronized )(Thread类,ExecutorService ,Future类)(卖火车票案例)

    多线程介绍 学习多线程之前,我们先要了解几个关于多线程有关的概念.进程:进程指正在运行的程序.确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能. 线 ...

  3. java线程中的同步锁和互斥锁有什么区别?

    两者都包括对资源的独占. 区别是 1:互斥是通过竞争对资源的独占使用,彼此没有什么关系,也没有固定的执行顺序. 2:同步是线程通过一定的逻辑顺序占有资源,有一定的合作关系去完成任务.

  4. 线程同步 synchronized 同步代码块 同步方法 同步锁

    一 同步代码块 1.为了解决并发操作可能造成的异常,java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块.其语法如下: synchronized(obj){ // ...

  5. 0037 Java学习笔记-多线程-同步代码块、同步方法、同步锁

    什么是同步 在上一篇0036 Java学习笔记-多线程-创建线程的三种方式示例代码中,实现Runnable创建多条线程,输出中的结果中会有错误,比如一张票卖了两次,有的票没卖的情况,因为线程对象被多条 ...

  6. Java同步锁全息详解

    一 同步代码块 1.为了解决并发操作可能造成的异常,java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块.其语法如下: synchronized(obj){ // ...

  7. Java并发之线程间的同步协作与通信协作

    1,Monitor监视器与syncrhoized实现原理 1.1:Monitor Monitor是一个同步工具,相当于操作系统中的互斥量(mutex),即值为1的信号量. 它内置与每一个Object对 ...

  8. java同步锁的正确使用

    同步锁分类 对象锁(this) 类锁(类的字节码文件对象即类名.class) 字符串锁(比较特别) 应用场景 在多线程下对共享资源的安全操作. 需求:启动5个线程对共享资源total进行安全操作. 同 ...

  9. 非UI线程更新UI界面的各种方法小结

    转载:https://www.cnblogs.com/xiashengwang/archive/2012/08/18/2645541.html 我们知道只有UI线程才能更新UI界面,其他线程访问UI控 ...

随机推荐

  1. Android提高第九篇之GridView和SQLite实现分页表格

    实现并封装一个SQL分页表格控件,不仅支持分页还是以表格的形式展示数据.先来看看本文程序运行的动画: 这个SQL分页表格控件主要分为“表格区”和“分页栏”这两部分,这两部分都是基于GridView实现 ...

  2. Codeforces 895C - Square Subsets

    895C - Square Subsets 思路:状压dp. 每个数最大到70,1到70有19个质数,给这19个质数标号,与状态中的每一位对应. 状压:一个数含有这个质因子奇数个,那么他状态的这一位是 ...

  3. 51nod 1682 中位数计数(前缀和)

    51nod 1682 中位数计数 思路: sum[i]表示到i为止的前缀和(比a[i]小的记为-1,相等的记为0,比a[i]大的记为1,然后求这些-1,0,1的前缀和): hash[sum[i]+N] ...

  4. Pyhon 日志管理 -- logging

    Pyhon 日志管理 -- logging 一直觉得运行程序是能打印日志是一个神奇的事情,不懂日志产生的原理,后来听说Pyhton 有一个logging模块,So,要好好研究一下. 先贴出代码,看看她 ...

  5. angular5 组件之间监听传值变化

    http://www.cnblogs.com/SLchuck/p/5904000.html https://i.cnblogs.com/EditPosts.aspx?postid=7995179&am ...

  6. 【备档】客户端自动化(主Android Appium + python

    之前做分享写的文档,备档~ 0.移动客户端自动化简介 客户端自动化测试的本质 定位对象 · 操作对象 · 校验对象 对象的定位应该是自动化测试的核心,要想操作.校验一个对象,首先应该识别这个对象. 一 ...

  7. android--------热修复介绍

    热修复技术在近年来飞速发展,尤其是在InstantRun方案推出之后,各种热修复技术竞相涌现.国内大部分成熟的主流APP都拥有自己的热修复技术,像手淘.支付宝.QQ.饿了么.美团等等. 代码热修复是最 ...

  8. docker添加国内仓库安装iredmail

    centos 7: 1.yum install docker or yum update docker sudo tee /etc/docker/daemon.json <<-'EOF'{ ...

  9. Java中/r和/n的区别

    /n换行符,效果是新换一行,光标在原有位置下一行 /r回车符,效果是光标来到下一行行首

  10. Black Widow CodeForces - 704C (dp)

    大意: 给定一个m个bool变量的方程, 求方程解的个数 给定方程的形式类似于这样 每个括号是一个子式, 每个子式里变量数不超过2, 每个变量出现次数不超过2, 方程右侧一直是1 对每个变量出现的式子 ...