.net 大数组和多线程-自旋锁或本地缓冲区或其他? 我有一个包含250k个实体(大小为20字节)和4个线程的数组 每个线程修改约100k个随机实体(这意味着,您无法预测它将接触哪些实体)。它可以多次修改同一实体 每个实体最多修改10次 修改大约需要10-6秒 每个实体由多个线程修改

.net 大数组和多线程-自旋锁或本地缓冲区或其他? 我有一个包含250k个实体(大小为20字节)和4个线程的数组 每个线程修改约100k个随机实体(这意味着,您无法预测它将接触哪些实体)。它可以多次修改同一实体 每个实体最多修改10次 修改大约需要10-6秒 每个实体由多个线程修改,.net,concurrency,locking,.net,Concurrency,Locking,最后两点是最重要的事实。第五点意味着我需要一种机制来保护我的实体不因并发访问/修改而损坏。第四点让我担心,考虑到获得锁的时间跨度很短,诸如阻塞线程的互斥锁之类的经典锁定机制是否会产生太多开销 我有两个想法: 使用自旋锁来克服开销(首先假设我关于开销的假设是正确的) 为每个线程提供一个它可以不中断地修改的数组的本地副本。所有线程完成后,将所有数组合并为一个。这是可能的,因为如果一个实体有多个副本,我可以选择一个赢家 你推荐什么?你同意我的一个想法还是推荐别的?如果我将数字更改为,您的建议是否会

最后两点是最重要的事实。第五点意味着我需要一种机制来保护我的实体不因并发访问/修改而损坏。第四点让我担心,考虑到获得锁的时间跨度很短,诸如阻塞线程的互斥锁之类的经典锁定机制是否会产生太多开销

我有两个想法:

  • 使用自旋锁来克服开销(首先假设我关于开销的假设是正确的)
  • 为每个线程提供一个它可以不中断地修改的数组的本地副本。所有线程完成后,将所有数组合并为一个。这是可能的,因为如果一个实体有多个副本,我可以选择一个赢家
你推荐什么?你同意我的一个想法还是推荐别的?如果我将数字更改为,您的建议是否会更改

  • 100万实体
  • 8线程
  • ~500k随机访问
  • 每个实体约100次修改
还请向我介绍C#/.Net中的实现。提前谢谢

其他信息

实体是值类型(结构)。我负担不起为每个写操作创建新对象——只修改现有的原语。

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

我把它建立在一个有锁和无锁的简单执行上

此锁定实例的运行大约需要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);
const int numOfPersons=250000;
var人员=新名单(NUMOF人员);
对于(int i=0;i
由于元素与线程的比率足够大,因此每个线程等待实例所需的时间可以忽略不计


从示例中可以看出,锁定实例的时间开销约为1秒。此代码在250000个项目的集合中执行100次100000次修改。不管修改是什么,1秒的时间基本上是恒定的。

看起来您的实体是结构(每个结构有20个字节)。这是一个大胆的猜测,因为我不知道您实际上在尝试做什么,但是您不能使这些实体成为不可变的引用类型吗


当您创建不可变的引用类型时,数组将只包含引用,其大小为4字节(或64位上的8字节),更改引用将始终是一个原子操作(当然,除非您显式更改对齐方式)。更改实体意味着创建一个新实体,并将数组中的引用从旧替换为新。这样,变化是原子的。然而,当两个线程在彼此之后不久写入同一个插槽时,您仍然可以放松更改(但您似乎并不担心这一点,因为您正在谈论“选择一个赢家”)


我不知道这会对性能产生什么影响,因为您可能会显式地选择值类型数组而不是引用类型数组。然而,有时让解决方案更简单是好的,而不是更难。此解决方案还可能改进缓存位置,因为您正在讨论对大型阵列的随机访问。因此,此阵列将不适合CPU的缓存,并且您将有很多缓存未命中。

正如他们所说,剥猫皮的方法不止一种(尽管为什么有人想要剥猫皮是另一个问题):-)

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

创建250K关键路段是否可行?也许,我不确定。您可以使用以下功能创建非常轻量级的自旋锁:

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

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

只有当OP提到的数组包含引用类型时,这才有效。@chibacity,你说得对。如果元素是值类型,锁将毫无意义。@Elisha thx,请回答。元素实际上是值类型。但是扩展数组来为每个实体保存一个额外的可锁定对象是没有问题的。@Elisha您在代码中花了多少时间?6秒似乎是处理250k元素的最佳时间。也许您的代码是修改绑定的,而不是锁定绑定的。现在占0.45%的部分在我的场景中可能会更多