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

时间:2011-02-21 17:28:33

标签: c# multithreading collections concurrent-collections

我有一个使用并行化处理数据的应用程序。

主程序在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 x n的二维数组(short [] []数据)组成,带有n个变量。我以这种方式使用并行化:

Parallel.For(0, 10000, (int i) =>
{
    // wrapper for the external c++ dll
    ProcessData(data[i]);
}

现在,对于10000个阵列中的每一个,我估计可以触发0到4个回调。我面临一个瓶颈,并且考虑到我的CPU资源没有被过度使用,我认为锁(连同数千个回调)是问题(我是对的还是可能有其他东西?)。我已经尝试过ConcurrentBag集合,但性能仍然更差(与其他用户findings一致)。

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

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

// 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));
}

4 个答案:

答案 0 :(得分:1)

  

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

您可能希望使用ThreadLocal<T>来保存您的馆藏。这会自动为每个线程分配一个单独的集合。

话虽如此,有Parallel.For的重载与本地状态一起工作,并在最后有一个集合传递。这可能会允许您生成ProcessData包装器,其中每个循环体都在自己的集合上工作,然后在最后重新组合。这可能会消除锁定的需要(因为每个线程正在处理它自己的数据集),直到重组阶段,每个线程发生一次(而不是每个任务一次,即:10000次)。这可以减少你从大约25000(0-4 * 10000)到几个的锁(从系统和算法依赖,但在四核系统上,根据我的经验可能大约10)。

有关详细信息,请参阅aggregating data with Parallel.For/ForEach上的博文。它演示了重载并解释了它们如何更详细地工作。

答案 1 :(得分:1)

你没有说你遇到了多少“瓶颈”。但是让我们来看看锁。

在我的机器上(四核,2.4 GHz),如果没有争用,锁的成本约为70纳秒。我不知道将一个项目添加到列表需要多长时间,但我无法想象它需要超过几微秒。但考虑到锁争用,我们需要100微秒(我会非常惊讶地发现它甚至是10微秒)将项目添加到列表中。因此,如果您在列表中添加40,000个项目,则为4,000,000微秒或4秒。如果是这种情况,我希望有一个核心被挂起。

我没有使用ConcurrentBag,但我发现BlockingCollection的表现非常好。

但是,我怀疑你的瓶颈在其他地方。你做过任何剖析吗?

答案 2 :(得分:1)

C#中的基本集合不是线程安全的。

您遇到的问题是由于您锁定整个集合只是为了调用add()方法。

您可以创建一个线程安全的集合,只锁定集合中的单个元素,而不是整个集合。

让我们看一下linked list

实施执行以下操作的add(item (or list))方法:

  1. 锁定集合。
  2. A =获取最后一项。
  3. 将最后一项引用设置为新项目(或新列表中的最后一项)。
  4. 锁定最后一项(A)。
  5. unclock collection。
  6. 将新项目/列表添加到A。
  7. 的末尾
  8. 解锁已锁定的项目。
  9. 这将在添加时仅锁定3个简单任务的整个集合。

    然后在迭代列表时,只需对每个对象执行trylock()。如果它被锁定,等待锁定是免费的(这样你确定add()已完成)。
    在C#中,您可以在lock()上对对象执行空trylock()块。 所以现在你可以安全地添加并同时迭代列表。

    如果需要,可以为其他命令实施类似的解决方案。

答案 3 :(得分:0)

任何集合的内置解决方案都会涉及一些锁定。可能有办法避免它,可能是通过隔离正在读/写的实际数据结构,但你将不得不锁定SOMEWHERE。

另外,要了解Parallel.For()将使用线程池。虽然易于实现,但您在创建/销毁线程时失去了细粒度的控制,并且线程池在启动大型并行任务时会产生一些严重的开销。

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

  • 使用Thread类自己创建线程。这使您免于线程池的调度速度降低;当你告诉它开始时,线程开始处理(或等待CPU时间),而不是线程池按照自己的节奏将线程请求送入其内部工作。你应该知道你一次进行的线程数量;经验法则是,当执行线程的“执行单元”活动线程数超过两倍时,开销可以克服多线程的好处。但是,您应该能够构建一个相对简单地考虑到这一点的系统。
  • 通过创建结果集合的字典来隔离结果集合。每个结果集合都键入由执行处理并传递给回调的线程所携带的某些令牌。字典可以一次有多个元素READ而不锁定,并且当每个线程写入字典中的不同集合时,不应该需要锁定这些列表(即使你确实锁定了它们,你也不会阻止其他线程)。结果是,当添加新线程的新集合时,唯一必须被锁定以便它将阻止线程的集合是主字典。如果你对回收代币很聪明,那就不应该经常发生。