内容简介:本文将说一个在同步上下文中非常常见的一种用法,换成异步上下文中会产生死锁的问题。先看看一段非常简单的代码:
AutoResetEvent
、 ManualResetEvent
、 Monitor
、 lock
等等这些用来做同步的类,如果在异步上下文(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
之后,所以不会执行,于是死锁。
更多死锁问题
死锁问题:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Java 多线程(二)—— 线程的同步
- 线程的三个同步器
- java synchronize - 线程同步原理
- 多线程六 同步容器&并发容器
- C++ 线程同步的四种方式
- 【译】JVM 进行线程同步背后的原理
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
复盘+:把经验转化为能力(第2版)
邱昭良 / 机械工业出版社 / 39.00
随着环境日趋多变、不确定、复杂、模糊,无论是个人还是组织,都需要更快更有效地进行创新应变、提升能力。复盘作为一种从经验中学习的结构化方法,满足了快速学习的需求,也是有效进行知识萃取与共享的机制。在第1版基础上,《复盘+:把经验转化为能力》(第2版)做了六方面修订: ·提炼复盘的关键词,让大家更精准地理解复盘的精髓; ·基于实际操作经验,梳理、明确了复盘的"底层逻辑"; ·明确了复......一起来看看 《复盘+:把经验转化为能力(第2版)》 这本书的介绍吧!
随机密码生成器
多种字符组合密码
HEX HSV 转换工具
HEX HSV 互换工具