线程同步。 完全锁定如何使内存访问“正确”?

首先,我知道lock{}Monitor类的合成糖。 (哦, 语法糖)

我正在玩简单的multithreading问题,并发现无法完全理解锁定一些任意WORD的内存如何保护整个其他内存不被缓存的寄存器/ CPU缓存等。使用代码示例来解释我所说的内容更容易:

 for (int i = 0; i < 100 * 1000 * 1000; ++i) { ms_Sum += 1; } 

最后, ms_Sum将包含100000000 ,这当然是预期的。

现在我们年龄将执行相同的周期,但在2个不同的线程上,上限减半。

 for (int i = 0; i < 50 * 1000 * 1000; ++i) { ms_Sum += 1; } 

由于没有同步,我们得到不正确的结果 – 在我的4核机器上,它是随机数近52 388 219 ,略大于100 000 000一半。 如果我们包含ms_Sum += 1;lock {} ,我们原因会得到绝对正确的结果100 000 000 。 但是对于我来说(真的说我期待相似的行为) ms_Sum += 1;是在ms_Sum += 1;之后添加lock ms_Sum += 1; 线让回答几乎正确:

 for (int i = 0; i < 50 * 1000 * 1000; ++i) { lock (ms_Lock) {}; // Note curly brackets ms_Sum += 1; } 

对于这种情况,我通常得到ms_Sum = 99 999 920 ,这非常接近。

问题:为什么要完全lock(ms_Lock) { ms_Counter += 1; } 使程序完全正确但lock(ms_Lock) {}; ms_Counter += 1; lock(ms_Lock) {}; ms_Counter += 1; 只是几乎正确; 锁定任意ms_Lock变量如何使整个内存稳定?

非常感谢!

PS已经阅读有关multithreading的书籍了。

类似问题(S)

lock语句如何确保内部处理器同步?

线程同步。 为什么这个锁不足以同步线程

为什么lock(ms_Lock) { ms_Counter += 1; } 使程序完全正确但lock(ms_Lock) {}; ms_Counter += 1; lock(ms_Lock) {}; ms_Counter += 1; 只是几乎正确?

好问题! 理解这一点的关键是锁做两件事:

  • 它会导致任何争用锁的线程暂停,直到可以执行锁定
  • 它会造成记忆障碍 ,有时也称为“全围栏”

我不完全理解锁定一些任意对象如何阻止其他内存缓存在寄存器/ CPU缓存等中

如您所知,在寄存器或CPU缓存中缓存内存可能会导致multithreading代码中发生奇怪的事情。 ( 请参阅我关于波动性的文章,以便对相关主题进行温和解释 。)简要说明:如果一个线程另一个线程更改该内存之前在CPU缓存中复制了一页内存,那么第一个线程会从缓存,然后有效地第一个线程已经及时向后移动读取 。 同样,对内存的写入似乎可以及时向前移动

内存屏障就像是时间的栅栏,它告诉CPU“做你需要做的事情,以确保随时间移动的读写操作不能越过栅栏”。

一个有趣的实验是,而不是一个空锁,在那里调用Thread.MemoryBarrier(),看看会发生什么。 你得到相同或不同的结果吗? 如果你得到相同的结果,那么它就是帮助的内存障碍。 如果你不这样做,那么线程几乎正确同步的事实就是减慢它们的速度以防止大多数比赛。

我的猜测是后者:空锁正在减慢线程,使得他们不会将大部分时间花在具有竞争条件的代码上。 强内存模型处理器通常不需要内存屏障。 (你是在x86机器上,还是在Itanium上,或者什么?x86机器有一个非常强大的内存模型,Itaniums有一个需要内存障碍的弱模型。)

你没有说你使用了多少个线程,但我猜两个 – 如果你用四个线程运行,我希望解锁版本能够得到合理接近1/4单线程版本的结果’正确’的结果。

当你不使用lock ,你的四进程机器会为每个CPU分配一个线程(为了简单起见,这个声明会对其他应用程序的存在进行折扣,这些应用程序也将依次进行调度)并且它们全速运行,不受干扰彼此。 每个线程从内存中获取值,递增它并将其存储回内存。 结果会覆盖那里的内容,这意味着,由于您有2个(或3个或4个)线程同时以全速运行,因此其他内核上的线程所产生的某些增量会被有效地丢弃。 因此,您的最终结果低于从单个线程获得的结果。

当你添加lock语句时,这会告诉CLR(这看起来像C#?),以确保任何可用内核上只有一个线程可以执行该代码。 这是从上面的情况发生的重大变化,因为multithreading现在互相干扰,即使你意识到这个代码不是线程安全的(只是足够接近它是危险的)。 这种不正确的序列化结果(作为副作用)在随后的增量中同时执行的次数较少 – 因为隐含的解锁需要昂贵的,就此代码和您的多核CPU而言,至少唤醒任何线程等待锁定。 由于这种开销,这个multithreading版本也将比单线程版本运行得慢。 线程并不总是使代码更快。

当任何等待的线程从等待状态唤醒时,锁定释放线程可以继续在其时间片中运行,并且通常会在唤醒线程获得获取变量副本的机会之前获取,增加和存储变量从记忆中为自己的增量操作。 因此,您最终会得到一个接近单线程版本的最终值,或者如果您lock循环内的增量,您将得到的结果。

查看Interlocked类,了解以primefaces方式处理某种类型变量的硬件级方法。

如果您没有锁定共享变量ms_Sum,那么两个线程都能够访问ms_Sum变量并无限制地增加该值。 在双核机器上并行运行的2个线程将同时对变量进行操作。

 Memory: ms_Sum = 5 Thread1: ms_Sum += 1: ms_Sum = 5+1 = 6 Thread2: ms_Sum += 1: ms_Sum = 5+1 = 6 (running in parallel). 

以下是我可以解释的事情正在发生的粗略细分:

 1: ms_sum = 5. 2: (Thread 1) ms_Sum += 1; 3: (Thread 2) ms_Sum += 1; 4: (Thread 1) "read value of ms_Sum" -> 5 5: (Thread 2) "read value of ms_Sum" -> 5 6: (Thread 1) ms_Sum = 5+1 = 6 6: (Thread 2) ms_Sum = 5+1 = 6 

有意义的是,没有同步/锁定,你得到的结果大约是预期总数的一半,因为2个线程可以“差不多”两倍的速度做事。

通过适当的同步,即lock(ms_Lock) { ms_Counter += 1; } lock(ms_Lock) { ms_Counter += 1; } ,订单更改为更像这样:

  1: ms_sum = 5. 2: (Thread 1) OBTAIN LOCK. ms_Sum += 1; 3: (Thread 2) WAIT FOR LOCK. 4: (Thread 1) "read value of ms_Sum" -> 5 5: (Thread 1) ms_Sum = 5+1 = 6 6. (Thread 1) RELEASE LOCK. 7. (Thread 2) OBTAIN LOCK. ms_Sum += 1; 8: (Thread 2) "read value of ms_Sum" -> 6 9: (Thread 2) ms_Sum = 6+1 = 7 10. (Thread 2) RELEASE LOCK. 

至于为何lock(ms_Lock) {}; ms_Counter += 1; lock(ms_Lock) {}; ms_Counter += 1; “几乎”是正确的,我觉得你很幸运。 锁会强制每个线程减速并“等待轮到”获取并释放锁定。 算术运算ms_Sum += 1;的事实ms_Sum += 1; 是如此微不足道(它运行得非常快)可能是为什么结果“几乎”可以。 当线程2执行获取和释放锁的开销时,线程1可能已经完成了简单的算术,因此您接近期望的结果。 如果你做了一些更复杂的事情(花费更多的处理时间),你会发现它不会接近你想要的结果。

我们一直在与聋人讨论这个问题 ,我们当前的想法可以表示为以下模式

在此处输入图像描述

时间从左到右运行,2个线程由两行表示。

哪里

  • 黑匣子代表获取,持有和释放锁的过程
  • plus表示添加操作(架构表示我的PC上的比例,锁定大约比添加的时间长20倍)
  • 白框表示包含尝试获取锁定并进一步等待其变为可用的时间段

黑匣子的顺序总是这样,它们不能重叠,它们应该始终非常紧密地相互跟随。 因此,它变得非常符合逻辑,即从不重叠,我们应该精确地达到预期的总和。

在这个问题中探讨了现有错误的来源:

这是答案。

我没有完全阅读所有其他答案,因为它们太长了,我看到了不正确的东西,答案不需要那么久。 也许Sedat的答案是最接近的。 它与锁定语句“减慢”程序的速度没有任何关系。

它与2个线程之间的ms_sum缓存同步有关。 每个线程都有自己的ms_sum缓存副本。

在你的第一个例子中 ,因为你没有使用’lock’,所以你要把它留给操作系统来决定何时进行同步(何时将更新的缓存值复制回主存储器或何时将其从主存储器读入缓存)。 因此,每个线程基本上都在更新它自己的ms_sum副本 。 现在,同步确实不时发生,但不是每个线程上下文切换,这导致结果略高于50,000,000。 如果它发生在每个线程上下文切换上,您将获得10,000,000。

第二个示例中 ,每次迭代都会同步ms_sum。 这使得ms_sum#1和ms_sum#2很好地同步。 所以,你将获得近10,000,000。 但它不会一直到10,000,000,因为每次线程上下文切换时,ms_sum可以关闭1,因为你在锁外发生了+ =。

现在,一般来说,在调用锁时,各种线程缓存的哪些部分是同步的,对我来说有点不为人知。 但是由于你在第二个例子中的差不多10,000,000的结果,我可以看到你的锁定调用导致ms_sum被同步。