如何避免 ConcurrentBag<T>.GetEnumerator() 上的争用
Posted
技术标签:
【中文标题】如何避免 ConcurrentBag<T>.GetEnumerator() 上的争用【英文标题】:How to avoid contention on ConcurrentBag<T>.GetEnumerator() 【发布时间】:2021-05-05 21:18:31 【问题描述】:我正在尝试解决在一个非常常用的 Log Writer 类上处理争用的性能问题。我看到的是 ConcurrentBag.GetEnumerator() 方法引起的竞争,当我们从多个线程迭代包项目时。查看 GetEnumerator() 的代码,我看到该方法调用 FreezeBag(),而后者又调用 AquireAllLocks(),这是争用的根源。我正在寻求帮助设计一种避免这种情况的方法。
下面是简化的类设计。该类注册了任意数量的日志记录“IPlugin”对象,我们将它们添加到 ConcurrentBag 实例中。 WriteLog() 方法被许多线程大规模调用。 WriteLog() 方法必须遍历插件列表以在每个项目上调用 IPlugin.WriteLog()。
public static class Monitor
internal static ConcurrentBag<IPlugin> Plugins = new ConcurrentBag<IPlugin>();
public static void WriteLog(int code, params object[] arguments)
foreach (IPlugin plugin in Plugins)
plugin.WriteLog(code, arguments);
public static void RegisterForEvents(IPlugin plugin)
if (plugin == null)
throw new ArgumentNullException("plugin");
Plugins.Add(plugin);
我的第一个想法是用列表替换 ConcurrentBag。我可以锁定 List.Add() 方法以使该线程安全,但随后我需要锁定迭代器周围的 WriteLog() 内部,因为在使用迭代器时我们无法更改集合。我可以在注册调用期间锁定/争用,因为我们只注册了几个 IPlugin。我无法承受 WriteLog() 方法中的任何争用。
有人帮我解决这个问题吗?
提前致谢!
【问题讨论】:
【参考方案1】:使用不可变集合找到答案。此处的信息和示例:System.Collections.Immutable
基本上,集合中的项目是不可变的,因此无需锁定 GetEnumerator() 调用。当我们需要向集合添加新插件时,将 ImmutableList 复制到新的 ImmutableList 并分配给共享静态变量。如果线程之前已经使用 GetEnumerator() 获得了枚举,那么它将具有“旧列表”。这样大规模的 WriteEvent 永远不会阻塞。
public static class Monitor
internal static ImmutableList<IPlugin> Plugins = ImmutableList.Create<IPlugin>();
public static void WriteLog(int code, params object[] arguments)
foreach (IPlugin plugin in Plugins)
plugin.WriteLog(code, arguments);
public static void RegisterForEvents(IPlugin plugin)
if (plugin == null)
throw new ArgumentNullException("plugin");
Plugins = Plugins.Add(plugin);
【讨论】:
以上是关于如何避免 ConcurrentBag<T>.GetEnumerator() 上的争用的主要内容,如果未能解决你的问题,请参考以下文章
Parallel ForEach For 多线程并行计算使用注意