【问题标题】:Lock around dictionary access in a WPF application在 WPF 应用程序中锁定字典访问
【发布时间】:2021-09-05 20:34:30
【问题描述】:

我正在开发一个旧的大型 WPF 应用程序。客户报告了一个错误,他们能够重现,但我不能。应用程序中有一个类如下所示:

public static class PermissionProvider
{
    private static Dictionary<string, bool> Permissions;

    public static void Init()
    {
        Permissions = new Dictionary<string, bool>();
    }
    
    private static object _lock = new object();
    public static bool HasPermission(string permission)
    {
        if (string.IsNullOrEmpty(permission)) return false;

        lock (_lock)
        {
            if (Permissions.ContainsKey(permission)) return Permissions[permission];
            var hasPermission = true; // Expensive call a third party module to check user permissions.
            Permissions.Add(permission, hasPermission);
            return hasPermission;
        }
    }
}

根据客户提供的日志文件,Permissions.Add(permission, hasPermission)这一行抛出了ArgumentException(key已经存在)。这对我来说没有意义;代码检查同一锁内的钥匙。

根据测试运行,对HasPermission 的所有调用似乎都是从主线程进行的。该程序在某些地方使用Dispatcher.BeginInvoke,但我的理解是锁定甚至不是必需的。字典是私有的,不能从其他任何地方访问。

在什么情况下会发生这种异常?


我的第一个想法是客户运行的是旧版本的应用程序,结果发现这个类只是在最新版本中添加的。

只需将Permissions.Add(permission, hasPermission) 更改为Permissions[permission] = hasPermission 就应该很容易避免这种特殊异常,但我更愿意先了解它为什么会发生。

【问题讨论】:

  • 这是PermissionProvider类的全部代码吗?
  • 唯一缺少的是对第三方的调用。
  • Init 是否有可能被多次调用?您可以考虑将其替换为静态构造函数。
  • 附带说明,PermissionProvider 类看起来像权限缓存,可能效率很低。如果线程请求未缓存的权限"Gazelle",则在持有锁时将调用昂贵的检查。然后第二个线程将请求权限"Rhino",它可能已经被缓存,必须等到对"Gazelle" 的检查完成。
  • @JonasH 新的Permissions 对象如何影响案例“密钥已存在”。如果错误是“密钥不存在”,那么是的。

标签: c# wpf multithreading


【解决方案1】:

这是可能的,但没有完整的源代码很难说。

昂贵的电话

var hasPermission = true; // Expensive call a third party module to check user permissions.

可以再次调用HasPermission()。因此,同一个线程会进入

lock (_lock) { ... }

再次(这是允许的),可能添加许可,离开锁,离开方法并返回到它来自的HasPermission(),再次添加相同的密钥。

这可能需要您的客户进行生产调试。如果您对此不熟悉并且可以说服您的客户暂时替换受影响的 DLL(让他创建一个备份副本),您可以尝试以下操作:

lock (_lock) 
{
    var stack = Environment.StackTrace;
    if (stack.Split(new []{nameof(HasPermission)}, StringSplitOptions.None).Length> 2) throw new Exception("I should not be in here twice");
    ...
}

这应该会使应用程序崩溃(除非某个地方的通用 catch 块),其调用堆栈具有两次受影响的方法,因此您可以分析第二次调用的来源。在这种情况下做任何你想做的事:generate a crash dump,分析你的日志,...

生成堆栈跟踪非常昂贵,因此这会改变时间并因此可能使问题消失。不过,消失的问题并不是固定的问题。

【讨论】:

  • 代价高昂的调用是调用第三方模块,它不会回调我们的应用程序代码。
  • @jkiiski 你确定不可能在同一个线程上发生另一个调用吗?这可能会以意想不到的方式发生。如果涉及 UI 线程,第三方代码可能会进入某种阻塞状态,仍会泵送消息,从而可能允许任意代码在 UI 线程上运行。
  • @JonasH 我将不得不调查这种可能性。我们没有第三方代码的源代码,但我想我可以尝试检查每次调用 HasPermission 的调用堆栈,看看它们来自哪里。
  • @Rekshino 启动消息泵的最简单方法可能是在控件上调用.ShowDialog,或调用Application.DoEvents。但是对于在什么情况下发送什么消息有各种规则,我现在知道所有细节。但谨慎的做法是尽可能少地假设第三方代码。
  • @JonasH @ThomasWeller 只是为了您的兴趣,WPF 中的 Application 类型中没有 DoEvents 方法。请参阅 Where is the Application.DoEvents() in WPF? 以供参考。
【解决方案2】:

我同意 Thomas Weller 的观点,最可能的原因是同一个线程由于某种原因重新进入锁。但我想提出另一种解决这类问题的方法。

在调用任意代码时持有锁可能很危险,它可能会导致死锁和其他各种问题。为了限制此类风险,最好只为一小段代码持有锁,只调用您知道是安全的代码,并且不会引发事件或以其他方式运行任意代码。

一种选择是切换到线程安全的“仅发布”模型,在调用“昂贵的方法”时释放锁。这可能允许多个线程同时调用昂贵的方法,这在您的特定情况下可能是也可能不是问题。比如:

lock (_lock)
{
     if (Permissions.ContainsKey(permission)) return Permissions[permission];
}
var hasPermission = true; // Expensive call a third party module to check user permissions.
lock (_lock)
{
     if (Permissions.ContainsKey(permission)) return Permissions[permission];
     Permissions.Add(permission, hasPermission);
     return hasPermission;
}

或者使用 ConcurrentDictionary.GetOrAdd 或多或少做同样的事情。

我也会提醒大家不要使用可变的全局状态,因为这也会使代码难以阅读和预测。

【讨论】:

    【解决方案3】:

    正如 JonasH 在 a comment 中指出的那样,Init 方法看起来非常可疑。如果此方法未仅调用一次,您的程序可能会崩溃。如果你不确定它被调用了多少次,至少用相同的锁保护它包含的代码。

    public static void Init()
    {
        lock (_lock)
        {
            Permissions = new Dictionary<string, bool>();
        }
    }
    

    【讨论】:

    • Init 方法应该只在应用程序启动时调用一次,但我会添加这个以确保。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-10-27
    • 1970-01-01
    • 2011-04-02
    • 2011-10-07
    • 2017-12-03
    • 2023-03-12
    相关资源
    最近更新 更多