如何避免 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() 上的争用的主要内容,如果未能解决你的问题,请参考以下文章

C# ConcurrentBag与list

Parallel ForEach For 多线程并行计算使用注意

线程安全ConcurrentBag

C#ConcurrentBag - 如何安全地清除每个添加的N个对象

c# 并行运算

Parallel.For 和 ConcurrentBag 给出不可预测的行为