复杂业务系统如何避免死锁

98 阅读3分钟

之前给同事处理一个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();
        }
    }
}

对应的图示(同事后来画的,还算比较清晰)如下:

image.png

死锁的处理

死锁的问题处理一起来一般都比较简单,不复杂的情况可以直接脑推。复杂一点的情况就需要通过excel或者其他方式,列出每个线程每一步做的事情,获取了哪些锁就能有个比较清晰的认识,这里不赘述。

思考

可能大家会觉得这个例子比较简单,但其实他能说明很多问题。

在软件工程中,分层是我们处理复杂问题的必要手段,沿着业务层到抽象层的不同层级的划分可以带来更好的代码复用,还可以大大降低开发的认知负担,分层甚至成了开发人员处理问题的银弹,但是分层后不同层的需要加锁资源很容易带来锁竞争的问题,这里和UI的死锁问题是一样的(很多道理总是互通的)。

推荐大家读一读这一篇非常著名的文章,这个文章也是别人推荐给我的(英文好的可以直接搜原文,这篇是相对来说翻译的说人话的一篇)。Multithreaded toolkits: A failed dream

在基于看过上面这篇文章的基础上,其实很容易理解,这其实是一个简单的锁顺序问题。仔细想一想我们的业务系统其实也是同样的道理(尤其是需要在两个相反方向获取锁的系统),那么如何避免死锁也显而易见的(甚至这里不需要我多解释什么)。

摘录一下链接中的原文:

我相信,如果该工具包经过精心设计,则可以成功使用多线程 GUI 工具包进行编程。如果工具包详细地公开了其锁定方法;如果您非常聪明,非常小心,并且对工具包的整体结构有全面的了解。如果你犯了一个小错误,事情就会变得严肃起来,你会偶尔出现挂机(由于死锁)或小故障(由于多线程资源竞争)。这种多线程方法最适合于与工具包设计密切相关的人。

对业务系统来说也一样:如果系统不够复杂或者总有很熟悉的维护的人员的话,那么你完全可以不用太担心死锁。否则的话,"events are always our friends"!