多线程编程时,如果涉及同时读写共享数据,就要格外小心。如果共享数据是独占资源,则要对共享数据的读写进行排它访问,最简单的方式就是加锁。锁也不能随便用,否则可能会造成死锁和活锁。本文将通过示例详细讲解死锁和活锁是如何发生的,以及如何避免它们。

避免多线程同时读写共享数据

在实际开发中,难免会遇到多线程读写共享数据的需求。比如在某个业务处理时,先获取共享数据(比如是一个计数),再利用共享数据进行某些计算和业务处理,最后把共享数据修改为一个新的值。由于是多个线程同时操作,某个线程取得共享数据后,紧接着共享数据可能又被其它线程修改了,那么这个线程取得的数据就是错误的旧数据。我们来看一个具体代码示例:

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 代码块,虽然都在运行,但却没有真正地干活。

我们把这种线程一直处于运行状态但其任务却一直无法进展的现象称为活锁。活锁和死锁的区别在于,处于活锁的线程是运行状态,而处于死锁的线程表现为等待;活锁有可能自行解开,死锁则不能。

要避免活锁,就要合理预估各线程对独占资源的占用时间,并合理安排任务调用时间间隔,要格外小心。现实中,这种业务场景很少见。示例中这种复杂的资源占用逻辑,很容易把人搞蒙,而且极不容易维护。推荐的做法是使用信号量机制代替锁,这是另外一个话题,后面单独写文章讲。

总结

我们应该避免多线程同时读写共享数据,避免的方式,最简单的就是加锁,把共享数据作为独占资源来进行排它使用。

多个线程在一次任务中需要对多个独占资源加锁时,就可能因相互循环等待而出现死锁。要避免死锁,就至少得有一个线程作出让步。即,在发现自己需要的资源得不到满足时,就要主动释放已占有的资源,以让别的线程可以顺利执行完成。

大部分情况安排一个线程让步便可避免死锁,但在复杂业务中可能会有多个线程互相让步的情况造成活锁。为了避免活锁,需要合理安排线程任务调用的时间间隔,而这会使得业务代码变得非常复杂。更好的做法是放弃使用锁,而换成使用信号量机制来实现对资源的独占访问。

-

精致码农

带你洞悉编程与架构

↑长按图片识别二维码关注,不要错过网海相遇的缘分

[C#.NET 拾遗补漏]12:死锁和活锁的发生及避免相关推荐

  1. 死锁和活锁的发生及避免

    死锁和活锁的发生及避免 多线程编程时,如果涉及同时读写共享数据,就要格外小心.如果共享数据是独占资源,则要对共享数据的读写进行排它访问,最简单的方式就是加锁.锁也不能随便用,否则可能会造成死锁和活锁. ...

  2. pg多线程更新会发生死锁_[C#.NET 拾遗补漏]12:死锁和活锁的发生及避免

    多线程编程时,如果涉及同时读写共享数据,就要格外小心.如果共享数据是独占资源,则要对共享数据的读写进行排它访问,最简单的方式就是加锁.锁也不能随便用,否则可能会造成死锁和活锁.本文将通过示例详细讲解死 ...

  3. c# xml文件新增同级节点_[C#.NET 拾遗补漏]08:强大的LINQ

    大家好,这是 [C#.NET 拾遗补漏] 系列的第 08 篇文章,今天讲 C# 强大的 LINQ 查询.LINQ 是我最喜欢的 C# 语言特性之一. LINQ 是 Language INtegrate ...

  4. [C#.NET 拾遗补漏]08:强大的LINQ

    阅读本文大概需要 13 分钟. 大家好,这是 [C#.NET 拾遗补漏] 系列的第 08 篇文章,今天讲 C# 强大的 LINQ 查询.LINQ 是我最喜欢的 C# 语言特性之一. LINQ 是 La ...

  5. .net 集合分成几个等数量集合_[C#.NET 拾遗补漏]08:强大的LINQ

    大家好,这是 [C#.NET 拾遗补漏] 系列的第 08 篇文章,今天讲 C# 强大的 LINQ 查询.LINQ 是我最喜欢的 C# 语言特性之一. LINQ 是 Language INtegrate ...

  6. 进程、线程知识点总结和同步(消费者生产者,读者写者三类问题)、互斥、异步、并发、并行、死锁、活锁的总结

    转自:http://www.cnblogs.com/kubixuesheng/p/4355786.html 进程:是个动态的概念,指的是一个静态的程序对某个数据集的一次运行活动,而程序是静态的概念,是 ...

  7. 多线程二死锁,活锁,饿死,阻塞的理解

    假如你现在还在为自己的技术担忧,假如你现在想提升自己的工资,假如你想在职场上获得更多的话语权,假如你想顺利的度过35岁这个魔咒,假如你想体验BAT的工作环境,那么现在请我们一起开启提升技术之旅吧,详情 ...

  8. 死锁和活锁有什么区别?

    本文翻译自:What's the difference between deadlock and livelock? 有人可以举例说明(代码) 死锁和活锁有什么区别吗? #1楼 参考:https:// ...

  9. 重入锁、死锁、活锁、公平非公平锁……一下子都给你屡清楚了

    目录 写在前面 重入锁 线程饥饿死锁 死锁 活锁(Livelock) 公平锁非公平锁 互斥锁 读-写锁 写在前面 每当听公司大佬提起来,死锁.活锁.公平锁.非公平锁--自己也是知其然而不知其所以然. ...

最新文章

  1. 全面支持三大主流环境 |百度PaddlePaddle新增Windows环境支持
  2. markdown希腊字母
  3. P4983-忘情【wqs二分,斜率优化】
  4. CPU位数、操作系统位数、应用程序位数浅析
  5. unity 关闭自己脚本_太可了!这些领域可以把Unity玩的那么好,带你开启新世界...
  6. 【Hadoop Summit Tokyo 2016】数据流与Apache NiFi
  7. 欧式二元期权的定价公式及实现
  8. seqkit根据基因id_西番莲内参基因及其筛选方法和应用与流程
  9. win10拨号上网开启热点
  10. No matter how hard it is or no matter how bad it gets, I am going to make it!
  11. excel保存快捷键_Excel新手必备的5大技巧,看看你会几个?(附26个超实用快捷键)
  12. 腾讯AI Lab开放文本理解系统TexSmart,让AI想得更深更广
  13. 微信小程序 小星星样式
  14. 视频异常检测 综述(一)
  15. JavaScript基础知识快速预览
  16. 雨听 | 英语学习笔记(十)~作文范文:怎样提高学生体能?
  17. 六足机器人的实现原理
  18. Webstorm2016安装激活
  19. docker CPU限制参数
  20. VS2022无法打开Silverlight 项目的问题:改用VS2015

热门文章

  1. Jenkins忘记admin密码处理方法
  2. 使用qrcode类制作二维码
  3. DOM节点创建(jQuery)
  4. FastDFS 安装
  5. Python easy_install
  6. 笔记(2015-07-24)
  7. python将argv作为参数_在jupyter / ipython notebook中将命令行参数传递给argv
  8. 将Teams Template升级到dotnet core 3.1
  9. 从Firefox控制您喜欢的音乐播放器
  10. 禁用删除键退回历史记录_如何在Windows 8中删除或禁用搜索超级按钮历史记录