C# 如何在多线程场景中加速使用集合的例程

C# 如何在多线程场景中加速使用集合的例程,c#,multithreading,collections,concurrent-collections,C#,Multithreading,Collections,Concurrent Collections,我有一个应用程序,它利用并行化来处理数据 主程序是C语言,而分析数据的程序之一是外部C++ DLL。该库扫描数据并在每次数据中发现某个信号时调用回调。数据应收集、分类,然后存储到HD中 下面是回调调用的方法以及排序和存储数据的方法的第一个简单实现: // collection where saving found signals List<MySignal> mySignalList = new List<MySignal>(); // method invoked b

我有一个应用程序,它利用并行化来处理数据

主程序是C语言,而分析数据的程序之一是外部C++ DLL。该库扫描数据并在每次数据中发现某个信号时调用回调。数据应收集、分类,然后存储到HD中

下面是回调调用的方法以及排序和存储数据的方法的第一个简单实现:

// collection where saving found signals
List<MySignal> mySignalList = new List<MySignal>();

// method invoked by the callback
private void Collect(int type, long time)
{
    lock(locker) { mySignalList.Add(new MySignal(type, time)); }
}

// store signals to disk
private void Store()
{
    // sort the signals
    mySignalList.Sort();
    // file is a object that manages the writing of data to a FileStream
    file.Write(mySignalList.ToArray());
}
现在,对于10000个数组中的每一个,我估计可能会触发0到4次回调。我正面临一个瓶颈,考虑到我的CPU资源没有被过度利用,我认为锁(加上数千次回调)是问题所在(我是对的还是可能有其他问题?)。我尝试过ConcurrentBag集合,但性能仍然较差(与其他用户一致)

我认为使用无锁代码的一个可能解决方案是拥有多个集合。然后,有必要制定一种策略,使并行进程的每个线程在单个集合上工作。例如,集合可以位于一个以线程ID为键的字典中,但我不知道有任何.NET工具可以实现这一点(在启动并行化之前,我应该知道初始化字典的线程ID)。这个想法可行吗?如果可行的话,是否有一些.NET工具可以实现这个目的?或者,有没有其他加快这一进程的想法

[编辑] 我遵循了Reed Copsey的建议,使用了以下解决方案(根据VS2010的探查器,在锁定和添加到列表的负担占用了15%的资源之前,而现在只有1%):

//保存找到的信号的主集合
List mySignalList=新列表();
//线程本地存储数据(每个线程正在处理其列表)
ThreadLocal ThreadLocal;
//分析数据
私有无效分析数据()
{
使用(threadLocal=新threadLocal(()=>
{返回新列表();}))
{
对于(0,10000,
() =>
{返回0;},
(i,loopState,localState)=>
{
//用于外部C++ DLL的包装器
ProcessData(数据[i]);
返回0;
},
(localState)=>
{
锁(这个)
{
//将线程本地列表添加到主集合
mySignalList.AddRange(local.Value);
local.Value.Clear();
}
});
}
}
//回调调用的方法
私有void Collect(整型,长时间)
{
local.Value.Add(newmysignal(type,time));
}
我认为使用无锁代码的一个可能解决方案是拥有多个集合。然后,有必要制定一种策略,使并行进程的每个线程在单个集合上工作。例如,集合可以位于一个以线程ID为键的字典中,但我不知道有任何.NET工具可以实现这一点(在启动并行化之前,我应该知道初始化字典的线程ID)。这个想法可行吗?如果可行的话,是否有一些.NET工具可以实现这个目的?或者,有没有其他加快这一进程的想法

您可能想看看如何使用来保存您的收藏。这会自动为每个线程分配一个单独的集合

也就是说,
Parallel.For的重载与本地状态一起工作,并且在末尾有一个收集过程。这可能会允许您生成
ProcessData
包装器,其中每个循环体处理自己的集合,然后在最后重新组合。这将潜在地消除锁定的需要(因为每个线程都在处理自己的数据集),直到重组阶段,每个线程发生一次(而不是每个任务一次,即10000次)。这可以将锁的数量从25000(0-4*10000)减少到少数(取决于系统和算法,但取决于四核系统,根据我的经验,可能是10个左右)


有关详细信息,请参阅我在上的博客文章。它演示了重载并更详细地解释了它们是如何工作的。

集合的任何内置解决方案都将涉及一些锁定。可能有一些方法可以避免这种情况,可能是通过分离正在读/写的实际数据结构,但是您必须锁定在某个地方

另外,请理解Parallel.For()将使用线程池。虽然实现起来很简单,但您会失去对线程创建/销毁的细粒度控制,并且在启动大型并行任务时,线程池会带来一些严重的开销

从概念的角度来看,我会尝试两种方法来加速这个算法:

  • 使用Thread类自己创建线程。这将使您从线程池的调度缓慢中解脱出来;当您告诉线程开始时,线程开始处理(或等待CPU时间),而不是线程池以自己的速度将线程请求送入其内部工作。您应该知道一次运行的线程数;经验法则是,当活动线程的数量是可用于执行线程的“执行单元”的两倍以上时,多线程的好处会被开销所克服。然而,您应该能够构建一个相对简单地考虑到这一点的系统
  • 通过创建结果集合字典来隔离结果集合。每个结果集合都被键入执行处理的线程携带的某个令牌,并传递给回调。字典可以一次读取多个元素而无需锁定,而且由于每个线程都在向字典中的不同集合写入,因此不需要锁定这些列表(即使您锁定了它们,也不会阻止其他线程)。结果是唯一的
    Parallel.For(0, 10000, (int i) =>
    {
        // wrapper for the external c++ dll
        ProcessData(data[i]);
    }
    
    // master collection where saving found signals
    List<MySignal> mySignalList = new List<MySignal>();
    // thread-local storage of data (each thread is working on its List<MySignal>)
    ThreadLocal<List<MySignal>> threadLocal;
    
    // analyze data
    private void AnalizeData()
    {
        using(threadLocal = new ThreadLocal<List<MySignal>>(() => 
            { return new List<MySignal>(); }))
        {
            Parallel.For<int>(0, 10000,
            () =>
            { return 0;},
            (i, loopState, localState) =>
            {
                // wrapper for the external c++ dll
                ProcessData(data[i]);
                return 0;
            },
            (localState) =>
            {
                lock(this)
                {
                    // add thread-local lists to the master collection
                    mySignalList.AddRange(local.Value);
                    local.Value.Clear();
                }
            });
        }
    }
    
    // method invoked by the callback
    private void Collect(int type, long time)
    {
        local.Value.Add(new MySignal(type, time));
    }