之前给同事处理一个bug时遇到了一个死锁问题。问题本身不难,但是通过这个可以说明我们系统设计时如何避免死锁的发生。
大概的伪代码如下。
// 最外层服务层
public class Service
{
private readonly object TaskInfo = new object();
// 用户API
public void LoadSystem()
{
lock(TaskInfo)
{
CSystem.UnloadExisting();
}
}
// 下一层的回调
private void CmdFeedNotify()
{
lock(TaskInfo)
{
// do something
}
}
private void AnswersReceivedNotify()
{
lock(TaskInfo)
{
// do something
}
}
}
// 服务层包裹的系统层
public class CSystem
{
// 同样下一层的回调
private void AnswersReceivedNotify()
{
lock(TaskInfo)
{
Service.AnswersReceivedNotify();
}
}
private void CmdFeedNotify()
{
lock(TaskInfo)
{
Service.CmdFeedNotify();
}
}
public void UnloadExisting()
{
Component.UnloadExisting();
}
}
// 系统层包裹的组件层
public class Component
{
private readonly object TaskState = new object();
// .ctor
public Component
{
InitNotifyThread();
}
protected void InitNotifyThread()
{
_NotifyExternalThread = new Thread(FuncNotifyThread);
_NotifyExternalThread.Start();
}
public void UnloadExisting()
{
_NotifyExternalThread.Join(); //必须等待已有系统的消息消费者处理完毕
}
// 外部信息通知线程函数
private void FuncNotifyThread()
{
while (_NotifyThreadIsRun)
{
if (_NotifyExternalQueue.GetAndWait() is Msg msg) // 解耦内外部消息,由内部回调一直往_NotifyExternalQueue内塞消息
{
_NotifyExternalQueue.Remove();
CSystem.AnswersReceivedNotify();
}
}
}
// 底层回调
private void CmdFeedBack()
{
lock(TaskState)
{
CSystem.CmdFeedNotify();
}
}
}
对应的图示(同事后来画的,还算比较清晰)如下:
死锁的处理
死锁的问题处理一起来一般都比较简单,不复杂的情况可以直接脑推。复杂一点的情况就需要通过excel或者其他方式,列出每个线程每一步做的事情,获取了哪些锁就能有个比较清晰的认识,这里不赘述。
思考
可能大家会觉得这个例子比较简单,但其实他能说明很多问题。
在软件工程中,分层是我们处理复杂问题的必要手段,沿着业务层到抽象层的不同层级的划分可以带来更好的代码复用,还可以大大降低开发的认知负担,分层甚至成了开发人员处理问题的银弹,但是分层后不同层的需要加锁资源很容易带来锁竞争的问题,这里和UI的死锁问题是一样的(很多道理总是互通的)。
推荐大家读一读这一篇非常著名的文章,这个文章也是别人推荐给我的(英文好的可以直接搜原文,这篇是相对来说翻译的说人话的一篇)。Multithreaded toolkits: A failed dream
在基于看过上面这篇文章的基础上,其实很容易理解,这其实是一个简单的锁顺序问题。仔细想一想我们的业务系统其实也是同样的道理(尤其是需要在两个相反方向获取锁的系统),那么如何避免死锁也显而易见的(甚至这里不需要我多解释什么)。
摘录一下链接中的原文:
我相信,如果该工具包经过精心设计,则可以成功使用多线程 GUI 工具包进行编程。如果工具包详细地公开了其锁定方法;如果您非常聪明,非常小心,并且对工具包的整体结构有全面的了解。如果你犯了一个小错误,事情就会变得严肃起来,你会偶尔出现挂机(由于死锁)或小故障(由于多线程资源竞争)。这种多线程方法最适合于与工具包设计密切相关的人。
对业务系统来说也一样:如果系统不够复杂或者总有很熟悉的维护的人员的话,那么你完全可以不用太担心死锁。否则的话,"events are always our friends"!