2

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

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

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

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

我认为使用无锁代码的可能解决方案是拥有多个集合。然后有必要制定一种策略,使并行进程的每个线程都在单个集合上工作。例如,集合可以在以线程 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

4 回答 4

1

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

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

我没用过ConcurrentBag,但是我发现BlockingCollection的性能非常好。

不过,我怀疑您的瓶颈在其他地方。你做过剖析吗?

于 2011-02-21T17:58:24.590 回答
1

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

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

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

让我们以一个链表为例。

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

  1. 锁定集合。
  2. A = 获取最后一项。
  3. 将最后一个项目引用设置为新项目(或新列表中的最后一个项目)。
  4. 锁定最后一项 (A)。
  5. 解锁收藏。
  6. 将新项目/列表添加到 A 的末尾。
  7. 解锁锁定的项目。

这将在添加时锁定整个集合以执行 3 个简单任务。

然后在遍历列表时,只需trylock()对每个对象执行 a 即可。如果它被锁定,等待锁被释放(这样你就确定add()完成了)。
在 C# 中,您可以在对象上做一个空lock()块作为trylock(). 因此,现在您可以安全地添加并且仍然可以同时迭代列表。

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

于 2011-02-21T18:00:53.773 回答
1

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

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

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

有关详细信息,请参阅我关于使用 Parallel.For/ForEach 聚合数据的博客文章。它演示了重载并更详细地解释了它们是如何工作的。

于 2011-02-21T17:42:15.637 回答
0

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

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

从概念的角度来看,我会同时尝试两件事来加速这个算法:

  • 使用 Thread 类自己创建线程。这使您从线程池的调度减速中解脱出来;当您告诉线程开始时,线程开始处理(或等待 CPU 时间),而不是线程池以自己的速度将线程请求馈送到其内部工作中。您应该知道一次要执行的线程数;经验法则是,当活动线程的数量是可用于执行线程的“执行单元”的两倍以上时,开销会克服多线程的好处。但是,您应该能够构建一个相对简单地考虑到这一点的系统。
  • 通过创建结果集合字典来隔离结果集合。每个结果集合都与执行处理的线程携带的某个令牌作为键,并传递给回调。字典可以在不锁定的情况下一次读取多个元素,并且由于每个线程都在向字典中的不同集合写入,因此不需要锁定这些列表(即使你确实锁定了它们,你也不会阻塞其他线程)。结果是,当一个新线程的新集合被添加到主字典时,唯一必须被锁定以阻塞线程的集合是主字典。如果您对回收令牌很聪明,那不应该经常发生。
于 2011-02-21T17:50:07.523 回答