在 WPF 应用程序中锁定字典访问

Posted

技术标签:

【中文标题】在 WPF 应用程序中锁定字典访问【英文标题】:Lock around dictionary access in a WPF application 【发布时间】: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 对象如何影响案例“密钥已存在”。如果错误是“密钥不存在”,那么是的。 【参考方案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 方法应该只在应用程序启动时调用一次,但我会添加这个以确保。

以上是关于在 WPF 应用程序中锁定字典访问的主要内容,如果未能解决你的问题,请参考以下文章

WPF数据绑定到图像文件永远锁定该文件[重复]

WPF 以编程方式锁定工作站

在运行时切换 wpf 资源字典

.NET - 字典锁定与 ConcurrentDictionary

WPF如何获取和设置应用程序范围的资源

WPF 在控件中添加自定义属性