大型数组和多个线程 - 自旋锁或本地缓冲区或其他东西?

时间:2010-07-31 13:01:23

标签: .net concurrency locking

  • 我有一个包含250k实体(大小为20字节)和4个线程的数组。
  • 每个线程修改~10万个随机实体(这意味着,您无法预测它将触摸哪些实体)。它可以多次修改同一个实体。
  • 每个实体最多修改10次。
  • 修改需要~10 -6 秒。
  • 每个实体都被多个线程修改

最后两点是最重要的事实。第五点意味着我需要一种机制来保护我的实体免于因并发访问/修改而被破坏。第四点让我担心,考虑到获取锁定的短时间跨度,阻塞线程的经典锁定机制是否会产生很大的开销。

我提出了两个想法:

  • 使用自旋锁来克服开销(首先假设我对开销的假设是正确的)。
  • 为每个线程提供数组的本地副本,它可以在不中断的情况下进行修改。所有线程完成后,将所有数组合并为一个。这是可能的,因为如果有一个实体的多个副本,我可以选择一个胜利者。

你推荐什么?你同意我的一个想法还是你推荐别的东西?如果我将数字更改为?:

,您的建议是否会更改
  • 1M实体
  • 8个帖子
  • ~500k随机访问
  • 每个实体
  • ~100次修改

还请指出我在C#/ .Net中的实现。提前谢谢。

其他信息
实体是值类型(结构)。我不能为每个写操作创建新对象 - 只修改现有的基元。

3 个答案:

答案 0 :(得分:1)

似乎最简单的解决方案可以适合这里。最简单的解决方案是锁定线程当前操作的实例。

我基于带锁和不带锁的简单执行。

这个锁定实例的运行需要大约10.09秒。同样的运行,没有锁定需要大约9.03秒:

const int numOfPersons = 250000;
var persons = new List<Person>(numOfPersons);
for (int i = 0; i < numOfPersons; i++)
{
    persons.Add(new Person());
}

var rand = new Random();

var sw = Stopwatch.StartNew();

for (int j = 0; j < 100; j++)
{
    for (int i = 0; i < 100000; i++)
    {
        int index = rand.Next(0, numOfPersons);

        Person person = persons[index];
        lock (person)
        {
            person.Name += "a";
        }
    }
}

Console.WriteLine(sw.Elapsed);

由于元素与线程的比例足够大,每个线程需要等待一个实例的时间期望可以忽略不计。

从示例中可以看出,锁定实例的时间开销约为1秒。此代码在250,000个项目的大小集合中进行100次100,000次修改。无论修改是什么,1秒的时间大致是恒定的。

答案 1 :(得分:1)

正如他们所说,有一种方法可以给猫皮肤(不过为什么有人想剥皮猫是另一个问题):-)

使用250K对象和4个线程,您必须猜测冲突将是(相对)罕见的。这并不意味着我们可以忽略它们,但它可能会影响我们寻找它们的方式。除非实际存在冲突,否则测试关键部分的速度非常快。这意味着可能可以检查每个事务的关键部分,因为知道相对较少的检查将占用更多的CPU。

创建250K关键部分是否可行?也许,我不确定。您可以使用以下命令创建一个非常轻量级的自旋锁:

while (0 != ::InterlockedExchange(&nFlag, 1)) {};
DoStuff();
nFlag = 0;

另一种方法可能是对数据集进行分区,并让每个线程都在一组唯一的对象上工作。这使得冲突不可能,因此不需要锁定。根据问题的性质,您可以通过让每个线程对一系列数据进行操作,或者可能通过为每个工作线程操作队列并使一个或多个扫描线程识别需要处理的对象并将其推送到适当的上来实现此目的。处理队列。

答案 2 :(得分:0)

您的实体似乎是结构(每个20字节)。这是一个疯狂的猜测,因为我不知道你真正想做什么,但是你不能让那些实体成为不可变的引用类型吗?

当你创建不可变的引用类型时,你的数组将只包含引用,它的大小为4个字节(或64位的8个字节),更改引用将始终是原子操作(除非你明确地改变了对齐方式)课程)。更改实体意味着创建一个新实体并将数组中的引用从旧更换为新。这种变化是原子的。但是,当两个线程在不久之后写入同一个插槽时,您仍然可以松开更改(但您似乎并不担心,因为您正在谈论'挑选获胜者')。

我不知道这会对性能产生什么影响,因为您可能会明确选择值类型数组而不是引用类型数组。但是,有时最好使解决方案更简单,而不是更难。此解决方案还可能会改善缓存局部性,因为您正在讨论对大型阵列的随机访问。因此,这个数组不适合CPU的缓存,你会有很多缓存未命中。