在 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 应用程序中锁定字典访问的主要内容,如果未能解决你的问题,请参考以下文章