C# - 解决潜在的死锁解决方案

C# - Solving potential deadlock solution

我有一个 LongOperationHelper,我会在每次可能的长时间操作时激活它。 它显示一个半透明层,在操作结束之前不允许任何点击,并有一个旋转控件来指示进度。

看起来像这样(缺少一些业务逻辑,但我认为这个想法很清楚):

编辑:(添加了实际需要锁定的常见状态的缺失代码 - 这更像是有问题的代码)

(我的解决方案发布在下面的答案中)

public static class LongOperationHelper
{
    private static object _synchObject = new object();
    private static Dictionary<string, int> _calls = new Dictionary<string, int>();

    private static Action<string> DisplayLongOperationRequested;
    private static Action<string> StopLongOperationRequested;

    public static void Begin(string messageKey)
    {
        lock (_synchObject)
        {
            if (_calls.ContainsKey(messageKey))
            {
                _calls[messageKey]++;
            }
            else
            {
                _calls.Add(messageKey, 1);

                DispatcherHelper.InvokeIfNecesary(() =>
                {
                    //Raise event for the MainViewModel to display the long operation layer
                    DisplayLongOperationRequested?.Invoke(messageKey);
                });
            }
        }
    }

    public static void End(string messageKey)
    {
        lock (_synchObject)
        {
            if (_calls.ContainsKey(messageKey))
            {
                if (_calls[messageKey] > 1)
                {
                    _calls[messageKey]--;
                }
                else
                {
                    _calls.Remove(messageKey);

                    DispatcherHelper.InvokeIfNecesary(() =>
                    {
                        //Raise event for the MainViewModel to stop displaying the long operation layer
                        StopLongOperationRequested?.Invoke(messageKey);
                    });
                }
            }
            else
            {
                throw new Exception("Cannot End long operation that has not began");
            }
        }
    }
}

正如您可能看到的那样,如果:

  1. 有人从非 UI 线程调用 Begin。
  2. 入锁
  3. 有人从 UI 线程调用 Begin 或 End 并被锁定
  4. 第一个 Begin 调用尝试调度到 UI 线程。

结果:死锁!

我想让这个 Helper 线程安全,这样任何线程都可以在任何给定时间调用 Begin 或 End,有兴趣看看是否有任何已知模式,任何想法?

谢谢!

不要锁定整个方法。仅在您触摸需要它的字段时锁定,并在您完成后立即解锁。每次触摸这些字段时锁定和解锁。否则,你最终会遇到这样的死锁。

也可以考虑使用ReaderWriterLockSlim,区分读锁和写锁。它允许多个线程同时读取,但在获取写锁时将所有人锁在外面。该文档中有一个关于如何使用它的示例。

"UI thread" 的全部目的就是避免像这样的同步。事实上,所有 UI 代码都需要在单个线程上 运行,这意味着根据定义,它不能同时 运行。您无需使用锁来使您的 UI 代码 运行 原子地 因为它全部 运行 在单个线程上 .

编写 UI 要求程序员自己锁定的代码非常困难且容易出错,因此整个框架的设计理念是期望人们(正确地)这样做是不合理的,并且简单地强制所有 UI 代码进入单个线程要容易得多,这样就不需要其他同步机制了。

这里是 "Deadlock free" 代码: 我已将 UI 线程的调度重新定位到锁外。

(有人还能看出这里存在潜在的死锁吗?)

public static class LongOperationHelper
{
    private static object _synchObject = new object();
    private static Dictionary<string, int> _calls = new Dictionary<string, int>();

    private static Action<string> DisplayLongOperationRequested;
    private static Action<string> StopLongOperationRequested;

    public static void Begin(string messageKey)
    {
        bool isRaiseEvent = false;

        lock (_synchObject)
        {
            if (_calls.ContainsKey(messageKey))
            {
                _calls[messageKey]++;
            }
            else
            {
                _calls.Add(messageKey, 1);

                isRaiseEvent = true;
            }
        }

        //This code got out of the lock, therefore cannot create a deadlock
        if (isRaiseEvent)
        {
            DispatcherHelper.InvokeIfNecesary(() =>
            {
                //Raise event for the MainViewModel to display the long operation layer
                DisplayLongOperationRequested?.Invoke(messageKey);
            });
        }
    }

    public static void End(string messageKey)
    {
        bool isRaiseEvent = false;

        lock (_synchObject)
        {
            if (_calls.ContainsKey(messageKey))
            {
                if (_calls[messageKey] > 1)
                {
                    _calls[messageKey]--;
                }
                else
                {
                    _calls.Remove(messageKey);

                    isRaiseEvent = true;
                }
            }
            else
            {
                throw new Exception("Cannot End long operation that has not began");
            }
        }

        //This code got out of the lock, therefore cannot create a deadlock
        if (isRaiseEvent)
        {
            DispatcherHelper.InvokeIfNecesary(() =>
            {
                StopLongOperationRequested?.Invoke(messageKey);
            });
        }
    }
}