c# 死锁和活锁的发生及避免
作者:精致码农 • 王亮 发布时间:2023-05-28 00:45:49
避免多线程同时读写共享数据
在实际开发中,难免会遇到多线程读写共享数据的需求。比如在某个业务处理时,先获取共享数据(比如是一个计数),再利用共享数据进行某些计算和业务处理,最后把共享数据修改为一个新的值。由于是多个线程同时操作,某个线程取得共享数据后,紧接着共享数据可能又被其它线程修改了,那么这个线程取得的数据就是错误的旧数据。我们来看一个具体代码示例:
static int count { get; set; }
static void Main(string[] args)
{
for (int i = 1; i <= 2; i++)
{
var thread = new Thread(ThreadMethod);
thread.Start(i);
Thread.Sleep(500);
}
}
static void ThreadMethod(object threadNo)
{
while (true)
{
var temp = count;
Console.WriteLine("线程 " + threadNo + " 读取计数");
Thread.Sleep(1000); // 模拟耗时工作
count = temp + 1;
Console.WriteLine("线程 " + threadNo + " 已将计数增加至: " + count);
Thread.Sleep(1000);
}
}
示例中开启了两个独立的线程开始工作并计数,假使当 ThreadMethod
被执行第 4 次的时候(即此刻 count
值应为 4),count
值的变化过程应该是:1、2、3、4,而实际运行时计数的的变化却是:1、1、2、2...。也就是说,除了第一次,后面每次,两个线程读取到的计数都是旧的错误数据,这个错误数据我们把它叫作脏数据。
因此,对共享数据进行读写时,应视其为独占资源,进行排它访问,避免同时读写。在一个线程对其进行读写时,其它线程必须等待。避免同时读写共享数据最简单的方法就是加锁。
修改一下示例,对 count
加锁:
static int count { get; set; }
static readonly object key = new object();
static void Main(string[] args)
{
...
}
static void ThreadMethod(object threadNumber)
{
while (true)
{
lock(key)
{
var temp = count;
...
count = temp + 1;
...
}
Thread.Sleep(1000);
}
}
这样就保证了同时只能有一个线程对共享数据进行读写,避免出现脏数据。
死锁的发生
上面为了解决多线程同时读写共享数据问题,引入了锁。但如果同一个线程需要在一个任务内占用多个独占资源,这又会带来新的问题:死锁。简单来说,当线程在请求独占资源得不到满足而等待时,又不释放已占有资源,就会出现死锁。死锁就是多个线程同时彼此循环等待,都等着另一方释放其占有的资源给自己用,你等我,我待你,你我永远都处在彼此等待的状态,陷入僵局。下面用示例演示死锁是如何发生的:
class Program
{
static void Main(string[] args)
{
var workers = new Workers();
workers.StartThreads();
var output = workers.GetResult();
Console.WriteLine(output);
}
}
class Workers
{
Thread thread1, thread2;
object resourceA = new object();
object resourceB = new object();
string output;
public void StartThreads()
{
thread1 = new Thread(Thread1DoWork);
thread2 = new Thread(Thread2DoWork);
thread1.Start();
thread2.Start();
}
public string GetResult()
{
thread1.Join();
thread2.Join();
return output;
}
public void Thread1DoWork()
{
lock (resourceA)
{
Thread.Sleep(100);
lock (resourceB)
{
output += "T1#";
}
}
}
public void Thread2DoWork()
{
lock (resourceB)
{
Thread.Sleep(100);
lock (resourceA)
{
output += "T2#";
}
}
}
}
示例运行后永远没有输出结果,发生了死锁。线程 1 工作时锁定了资源 A,期间需要锁定使用资源 B;但此时资源 B 被线程 2 独占,恰巧资线程 2 此时又在待资源 A 被释放;而资源 A 又被线程 1 占用......,如此,双方陷入了永远的循环等待中。
死锁的避免
针对以上出现死锁的情况,要避免死锁,可以使用 Monitor.TryEnter(obj, timeout)
方法来检查某个对象是否被占用。这个方法尝试获取指定对象的独占权限,如果 timeout
时间内依然不能获得该对象的访问权,则主动“屈服”,调用 Thread.Yield()
方法把该线程已占用的其它资源交还给 CUP,这样其它等待该资源的线程就可以继续执行了。即,线程在请求独占资源得不到满足时,主动作出让步,避免造成死锁。
把上面示例代码的 Workers
类的 Thread1DoWork
方法使用 Monitor.TryEnter
修改一下:
// ...(省略相同代码)
public void Thread1DoWork()
{
bool mustDoWork = true;
while (mustDoWork)
{
lock (resourceA)
{
Thread.Sleep(100);
if (Monitor.TryEnter(resourceB, 0))
{
output += "T1#";
mustDoWork = false;
Monitor.Exit(resourceB);
}
}
if (mustDoWork) Thread.Yield();
}
}
public void Thread2DoWork()
{
lock (resourceB)
{
Thread.Sleep(100);
lock (resourceA)
{
output += "T2#";
}
}
}
再次运行示例,程序正常输出 T2#T1#
并正常结束,解决了死锁问题。
注意,这个解决方法依赖于线程 2 对其所需的独占资源的固执占有和线程 1 愿意“屈服”作出让步,让线程 2 总是优先执行。同时注意,线程 1 在锁定 resourceA
后,由于争夺不到 resourceB
,作出了让步,把已占有的 resourceA
释放掉后,就必须等线程 2 使用完 resourceA
重新锁定 resourceA
再重做工作。
正因为线程 2 总是优先,所以,如果线程 2 占用 resourceA
或 resourceB
的频率非常高(比如外面再嵌套一个类似 while(true) 的循环 ),那么就可能导致线程 1 一直无法获得所需要的资源,这种现象叫线程饥饿,是由高优先级线程吞噬低优先级线程 CPU 执行时间的原因造成的。线程饥饿除了这种的原因,还有可能是线程在等待一个本身也处于永久等待完成的任务。
我们可以继续开个脑洞,上面示例中,如果线程 2 也愿意让步,会出现什么情况呢?
活锁的发生和避免
我们把上面示例改造一下,使线程 2 也愿意让步:
public void Thread1DoWork()
{
bool mustDoWork = true;
Thread.Sleep(100);
while (mustDoWork)
{
lock (resourceA)
{
Console.WriteLine("T1 重做");
Thread.Sleep(1000);
if (Monitor.TryEnter(resourceB, 0))
{
output += "T1#";
mustDoWork = false;
Monitor.Exit(resourceB);
}
}
if (mustDoWork) Thread.Yield();
}
}
public void Thread2DoWork()
{
bool mustDoWork = true;
Thread.Sleep(100);
while (mustDoWork)
{
lock (resourceB)
{
Console.WriteLine("T2 重做");
Thread.Sleep(1100);
if (Monitor.TryEnter(resourceA, 0))
{
output += "T2#";
mustDoWork = false;
Monitor.Exit(resourceB);
}
}
if (mustDoWork) Thread.Yield();
}
}
注意,为了使我要演示的效果更明显,我把两个线程的 Thread.Sleep 时间拉开了一点点。运行后的效果如下:
通过观察运行效果,我们发现线程 1 和线程 2 一直在相互让步,然后不断重新开始。两个线程都无法进入 Monitor.TryEnter
代码块,虽然都在运行,但却没有真正地干活。
我们把这种线程一直处于运行状态但其任务却一直无法进展的现象称为活锁。活锁和死锁的区别在于,处于活锁的线程是运行状态,而处于死锁的线程表现为等待;活锁有可能自行解开,死锁则不能。
要避免活锁,就要合理预估各线程对独占资源的占用时间,并合理安排任务调用时间间隔,要格外小心。现实中,这种业务场景很少见。示例中这种复杂的资源占用逻辑,很容易把人搞蒙,而且极不容易维护。推荐的做法是使用信号量机制代替锁,这是另外一个话题,后面单独写文章讲。
总结
我们应该避免多线程同时读写共享数据,避免的方式,最简单的就是加锁,把共享数据作为独占资源来进行排它使用。
多个线程在一次任务中需要对多个独占资源加锁时,就可能因相互循环等待而出现死锁。要避免死锁,就至少得有一个线程作出让步。即,在发现自己需要的资源得不到满足时,就要主动释放已占有的资源,以让别的线程可以顺利执行完成。
大部分情况安排一个线程让步便可避免死锁,但在复杂业务中可能会有多个线程互相让步的情况造成活锁。为了避免活锁,需要合理安排线程任务调用的时间间隔,而这会使得业务代码变得非常复杂。更好的做法是放弃使用锁,而换成使用信号量机制来实现对资源的独占访问。
作者:精致码农
出处:http://cnblogs.com/willick
来源:https://www.cnblogs.com/willick/p/13967346.html?utm_source=tuicool&utm_medium=referral


猜你喜欢
- 一:回顾反射机制这种重点回顾一下反射Method!为什么要回顾反射机制呢?因为我们后面要手写一个简易的Spring框架,便于我们理解其中的核
- 本文实例为大家分享了Unity使用射线实现贴花系统,供大家参考,具体内容如下老规矩,直接上代码:using System.Collectio
- 本文实例讲述了Java使用备忘录模式实现过关类游戏功能。分享给大家供大家参考,具体如下:一.模式定义备忘录模式,在不破坏封闭的前提下,捕获一
- 本文实例讲述了C#实现合并多个word文档的方法,是非常具有实用价值的技巧。分享给大家供大家参考。具体实现方法如下:using System
- C#实现的获取路由器MAC地址,路由器外网地址。对于要获取路由器MAC地址,一定需要知道路由器web管理系统的用户名和密码。至于获取路由器的
- C++中一个重要的特性就是指针,指针不仅具有获得地址的能力,还具有操作地址的能力。指针可以用于数组、或作为函数的参数,用来访问内存和对内存的
- 1.如下图中的检测值存到数据库是科学计数法2 经过代码处理后3 只需要对你读到的字符串进行如下操作BigDecimal bd = new B
- 可能也有其他方法,比如用 WGet 等等,但是 推荐用 PowerShell ,为什么呢,因为 PowerShell 太强大呗PowerSh
- SpringBoot的web项目,在每一次修改了java文件或者是resource的时候,都必须去重启一下项目,这样的话浪费了很多的时间,实
- 本文介绍了浅谈Java的两种多线程实现方式,分享给大家。具有如下:一、创建多线程的两种方式Java中,有两种方式可以创建多线程:1 通过继承
- PagerBottomTabStrip 是一个基本按谷歌Material Design规范完成的安卓底部导航栏控件官方设计规范:https:
- 常量池Java中我们创建String对象有两种基本方法。String str1 = "zxhtom";String st
- 本文以实例详细分析了Java中重载与重写的区别,感兴趣的朋友可以参考一下。一、重载(Overloading):(1) 方法重载是让类以统一的
- 需求背景接到这样一个需求,需要在 WebView 的所有网络请求中,在请求的url中,加上一个xxx=1的标志位。例如 http:
- Jenkins 关闭和重启我们用jar -jar jenkins.war来启动jenkins服务器,那么我们如何关闭或者重启jenkins服
- HashTable和HashMap区别第一,继承的父类不同。Hashtable继承自Dictionary类,而HashMap继承自Abstr
- 本文实例讲述了C#获取文件创建时间的方法。分享给大家供大家参考。具体如下:C#获取文件创建时间,主要用到了FileInfo的Creattio
- 目录1.C 语言包含的数据类型2.C语言的基本数据类型3.示例代码1.C 语言包含的数据类型如下图所示:2.C语言的基本数据类型short、
- C# Linq延迟查询在定义linq查询表达式时,查询是不会执行,查询会在迭代数据项时运行。它使用yield return 语句返回谓词为t
- 如果没有安装过maven,是用的idea自带的maven,那就是idea的安装目录下 /plugins/maven/lib/maven3这个