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

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

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

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

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://www.mzph.cn/news/306961.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

64岁Python之父加入微软 | 谁说大龄程序员无出路

喜欢就关注我们吧&#xff01;现年 64 岁的 Python 创始人 Guido van Rossum 退休一年后再度复出&#xff0c;今天宣布已加入微软开发者部门 (Developer Division).我觉得退休生活乏味又无趣&#xff0c;因此已加入微软开发者部门。做什么工作&#xff1f;选择太多了&#xff0…

JAVA中的GridView每一个赋值,在ASP.NET 2.0中操作数据之六十二:GridView批量更新数据...

导言&#xff1a;在前面的教程&#xff0c;我们对数据访问层进行扩展以支持数据库事务.数据库事务确保一系列的操作要么都成功&#xff0c;要么都失败。本文我们将注意力转到创建一个批更新数据界面.在本文&#xff0c;我们将创建一个GridView控件&#xff0c;里面的每一行记录…

微软发布VS Code Jupyter插件!不止Python!多语言的Jupyter Notebook支持来了!

北京时间 2020 年 11 月 12 日&#xff0c;微软发布了全新的 VS Code Jupyter 插件&#xff01;Jupyter 插件将 Jupyter Notebook 的功能引入 VS Code&#xff0c;并且将会支持更多语言和使用场景。Jupyter Notebook 支持创建和共享包含代码、方程式、文本和可视化内容的文档&a…

windows安全模式_鲁大师正式挂牌上市,使用鲁大师如何开启笔记本电脑全面节能模式...

10月10日消息&#xff0c;今天360旗下的鲁大师正式挂牌上市。上市之后&#xff0c;鲁大师的盘中涨幅一度扩大至100%&#xff0c;鲁大师的市值也一度达到了14亿港元。过去三个财年&#xff0c;鲁大师的营业收入分别为6981.2万、1.23亿和3.20亿人民币。简单介绍360&#xff0c;36…

跟我一起学Redis之Redis事务简单了解一下

前言关系数据库中的事务&#xff0c;小伙伴们应该是不陌生了&#xff0c;不管是在开发还是在面试过程中&#xff0c;总有两个问题逃不掉&#xff1a;•说说事务的特性&#xff1b;•事务隔离级别是怎么一回事&#xff1f;事务处理不好&#xff0c;数据就可能不准确&#xff0c;…

groovy 字符串截取最后一个_Python入门高级教程--Python 字符串

Python 字符串字符串是 Python 中最常用的数据类型。我们可以使用引号(或")来创建字符串。创建字符串很简单&#xff0c;只要为变量分配一个值即可。例如&#xff1a;var1 Hello World!var2 "Python Runoob"Python 访问字符串中的值Python 不支持单字符类型&a…

java面试题_阿里大厂流出的数百道 Java 经典面试题

BAT 常问的 Java基础39道常见面试题1.八种基本数据类型的大小&#xff0c;以及他们的封装类2.引用数据类型3.Switch能否用string做参数4.equals与的区别5.自动装箱&#xff0c;常量池6.Object有哪些公用方法7.Java的四种引用&#xff0c;强弱软虚&#xff0c;用到的场景8.Hashc…

​被冷落的运算符重载

基本类型可以使用运算符进行运算、比较、取反等操作。如果想使用运算符操作两个对象&#xff0c;我们就需要用到运算符重载。我们先看个例子&#xff0c;假如有个房子类&#xff0c;有长和宽两个属性。代码如下&#xff1a;接下来我们使用House类实例化两个对象&#xff1a;hou…

neo4j 查询同一节点的两个上级_WhatRoute for Mac(互联网流量诊断查询工具)

如果您想在不使用命令行的情况下执行流量诊断查询&#xff0c;那么WhatRoute是一个不错的选择。WhatRoute提供了一个干净且有条理的界面&#xff0c;主要提供Traceroute功能&#xff0c;但也可以执行Ping&#xff0c;域名服务查询&#xff0c;Whois查询以及监控进出计算机的流量…

快来参加学习.NET 挑战赛

今天访问dot.net 网站看到了一个学习.NET 挑战赛&#xff0c;发现已经赛程过半了&#xff0c;这是一个为那些想更多地了解 C# 和 .NET 的人举办的一个完全免费的课程活动&#xff0c;这些模块必须在 11 月底前完成。参加这个挑战赛&#xff0c;你必须从 .NET 学习挑战页面进入进…

excel怎么设置打印区域_别再浪费打印纸了!这样设置,Excel表格再大都能打印成一页!...

在打印Excel表格时&#xff0c;你是不是经常碰到过这种情况&#xff1a;明明排版好的表格&#xff0c;结果打印完却发现只显示一半......表格太宽导致无法打印在A4纸上......今天叨叨君就来分享几个有效的解决方法&#xff0c;教你轻轻松松将表格打印在一页纸上&#xff0c;一起…

xcode 修改 infodictionary_安卓系统修改复位键生效时间

文档说明本文档以SC806-CN-00(msm8909平台&#xff0c;Android 7)为例&#xff0c;说明如何修改复位键生效时间。应用背景默认情况下&#xff0c;按复位键&#xff0c;系统马上直接关机。在实际应用中&#xff0c;有可能由于干扰造成的抖动导致误关机行为。 为避免发生这种情况…

为什么曾经优秀的人突然变得平庸?

职场&认知洞察 丨 作者 / findyi这是findyi公众号分享的第95篇原创文章一个读者的提问&#xff1a;洋哥&#xff0c;我从小都是学霸&#xff0c;本硕都是985&#xff0c;计算机科班出身&#xff0c;但进入职场后却始终无法取得突破。工作5年还是基层员工&#xff0c;我该怎…

java编程_Java编程和C语言的比较

很多人都拿Java编程和c语言相比较&#xff0c;那么今天小编就来先说说个人理解吧&#xff0c;新手学习Java很简单&#xff0c;上手也很容易&#xff0c;只需要会拼音就可以&#xff0c;简单而且没有门槛&#xff0c;而c语言学习成本高&#xff0c;更需要投入较大的精力&#xf…

.NET Core3.1升级.NET5,坑还真不少...

11月11号是电商狂欢的日子&#xff0c;也是.NET5正式发布的日子&#xff0c;媳妇儿等着零点秒杀&#xff0c;我却在刷新着微软官网等更新&#xff0c;然后第一时间开始折腾。此前Scott Hunter在博客信誓旦旦.NET Core3.1平滑迁移.NET5&#xff0c;于是当天就去升级我的宝藏项目…

requestPermissions读写手机存储权限_泛圈云盘可为企业建立高效安全的云办公在线协同文档存储?...

泛圈企业云盘结合智能手机和无线网络&#xff0c;实现对任何办公地点和办公时间的无缝访问&#xff0c;提高办公效率。它可以连接客户原有的各种IT系统&#xff0c;包括OA、邮件、ERP等各种个人业务系统&#xff0c;使手机也可以用来操作、浏览、管理公司的所有工作事务&#x…

怎么将SVG转成PNG(.NET工具包编写)

序一天&#xff0c;作者在深圳湾吹风时突然想到自己还有 20 多位粉丝&#xff0c;所以决定每周至少要水一篇文章。众所周知&#xff0c;一篇文章要有封面&#xff0c;正痛苦时&#xff0c;.NET 官方网站更新了一大波质量上乘的插图&#xff1b;高兴之余&#xff0c;发觉平台不支…

ghelper怎么在手机上用_当长时间不用手机玩《崩坏3》、《战双》

崩坏3、战双&#xff0c;虽然是手机游戏&#xff0c;作为一个PC党&#xff0c;我还是热衷于用电脑玩游戏。用电脑玩游戏可以把画质全部开到最大&#xff0c;依旧可以顺畅地进行游戏。手机就不行&#xff0c;即使是苹果&#xff0c;把画质拉满&#xff0c;依旧会有卡顿。特别是崩…

System.Text.Json中时间格式化

转自&#xff1a;Rayomcnblogs.com/Rayom/p/13967415.html简介.Net Core 3.0开始全新推出了一个名为System.Text.Json的Json解析库&#xff0c;用于序列化和反序列化Json&#xff0c;此库的设计是为了取代Json.Net(Newtonsoft.Json)时间格式化的不足System.Text.Json的优点就不…

windows server 启用 vss_windows服务器常用的安全加固方法

Windows操作系统本身已经提供了多种安全机制&#xff0c;如标识与鉴别、访问控制、用户账户控制、安全审计、文件系统。但是&#xff0c;这并不意味着操作系统就固若金汤。事实上&#xff0c;任何一个Windows版本都或多或少的存在着漏洞&#xff0c;而且在不断的被挖掘出来。Wi…