ConcurrentBag <T>和锁定(List <T>)哪个更快或者删除?

qak*_*mak 5 .net c# concurrency

我需要在List上使用一些线程安全操作.

通常我只是简单地使用:

lock(List<T>)
{
   List<T>.Add();
   List<T>.Remove();
}
Run Code Online (Sandbox Code Playgroud)

我也知道还有另一种方法,使用List<T>.但我不知道哪个更快或任何其他不同.

UPDATE1:

有些人建议我使用ConcurrentBag,因为这样更安全.但我担心它会让我的操作更慢.

我只是有很多线程需要在List中添加或删除一些对象,我只是想知道更好的性能方法.这就是我问的原因.

Zer*_*er0 8

除非您确定线程的访问模式,否则不要ConcurrentBag<T>用于替换锁定List<T>,因为它在幕后使用线程本地存储。

MSDN 谈到首选用法:

"ConcurrentBag<T>是一个线程安全的 bag 实现,针对同一个线程将生产和消费存储在 bag 中的数据的场景进行了优化。"

这也是要注意,重要的List<T>有序的,并ConcurrentBag<T>无序的。如果您不关心收藏中的顺序,我会使用ConcurrentQueue<T>.

关于性能,下面是一些来自ConcurrentBag<T>. 但要考虑的主要事情是,如果您执行 aTake并且您的线程本地存储为空,它将从其他线程中窃取,这是昂贵的。

当它需要窃取时,注意它是锁定的。另请注意,它可以多次锁定一个,Take因为它TrySteal可能会失败并被多次调用Steal(未显示)。

private bool TrySteal(ConcurrentBag<T>.ThreadLocalList list, out T result, bool take)
{
    lock (list)
    {
        if (this.CanSteal(list))
        {
            list.Steal(out result, take);
            return true;
        }
        result = default (T);
        return false;
    }
}
Run Code Online (Sandbox Code Playgroud)

在 期间也可能有旋转等待CanSteal

private bool CanSteal(ConcurrentBag<T>.ThreadLocalList list)
{
    if (list.Count <= 2 && list.m_currentOp != 0)
    {
        SpinWait spinWait = new SpinWait();
        while (list.m_currentOp != 0)
            spinWait.SpinOnce();
    }
    return list.Count > 0;
} 
Run Code Online (Sandbox Code Playgroud)

最后,即使添加也会导致锁定。

private void AddInternal(ConcurrentBag<T>.ThreadLocalList list, T item)
{
    bool lockTaken = false;
    try
    {
        Interlocked.Exchange(ref list.m_currentOp, 1);
        if (list.Count < 2 || this.m_needSync)
        {
            list.m_currentOp = 0;
            Monitor.Enter((object) list, ref lockTaken);
        }
        list.Add(item, lockTaken);
    }
    finally
    {
        list.m_currentOp = 0;
        if (lockTaken)
            Monitor.Exit((object) list);
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 更一般的建议可能是:除非您知道自己在做什么,否则不要尝试多线程代码。不完全了解幕后发生的事情的人不太可能偶然发现可以提高性能的解决方案。 (3认同)
  • 为什么使用 TLS 是一个问题? (2认同)

Eug*_*kov 6

只需尝试一下,您就可以轻松测量不同方法的性能!这就是我刚才得到的:

lock list: 2,162s
ConcurrentBag: 7,264s
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;

public class Test
{
    public const int NumOfTasks = 4;
    public const int Cycles = 1000 * 1000 * 4;

    public static void Main()
    {
        var list = new List<int>();
        var bag = new ConcurrentBag<int>();

        Profile("lock list", () => { lock (list) list.Add(1); });
        Profile("ConcurrentBag", () => bag.Add(1));
    }

    public static void Profile(string label, Action work)
    {
        var s = new Stopwatch();
        s.Start();

        List<Task> tasks = new List<Task>();

        for (int i = 0; i < NumOfTasks; ++i)
        {
            tasks.Add(Task.Factory.StartNew(() =>
            {
                for (int j = 0; j < Cycles; ++j)
                {
                    work();
                }
            }));
        }

        Task.WaitAll(tasks.ToArray());

        Console.WriteLine(string.Format("{0}: {1:F3}s", label, s.Elapsed.TotalSeconds));
    }
}
Run Code Online (Sandbox Code Playgroud)

  • +1 对于实际进行科学研究来说,在 2021 年在 dotnet core 5.0 上尝试这个示例并得到以下完全不同的结果很有趣:锁定列表:1.681s ConcurrentBag:0.199s!看来 dotnet 团队一直在忙着优化 ConcurrentBag (9认同)