++运算符线程安全吗?

Vac*_*ano 56 c# multithreading

注意:我真的不是很擅长多线程编程,但我目前的项目让我这样做,所以我试图了解线程安全和什么不是.

我正在阅读Eric Lippert 关于++我所做的那些令人敬畏的答案之一.他说这才是真正发生的事情:

  1. 评估x以生成变量
  2. 将变量的值复制到临时位置
  3. 临时值递增以产生新值(不覆盖临时值!)
  4. 新值存储在变量中
  5. 操作的结果是新值

这让我思考,如果调用++ i的两个线程怎么办?如果第一个线程在步骤3,当第二个线程在步骤2时.(这意味着如果第二个线程在第一个线程将新值存储在变量中之前将值复制到临时位置,该怎么办?)

如果发生这种情况,那么两个线程似乎只会增加i一次而不是两次.(除非整件事情都在lock.)

Eri*_*ert 77

正如其他答案所指出的那样,不,++不是"线程安全".

当你了解多线程及其危害时,我认为有助于你开始非常精确地理解你所谓的"线程安全",因为不同的人通过它来表达不同的东西.基本上,您在此关注的线程安全方面是操作是否是原子操作."原子"操作是在被另一个线程中断时保证不会完成一半的操作.

(还有很多其他的线程问题与原子性无关,但可能仍然属于某些人对线程安全的定义.例如,给定两个线程,每个变量变量,两个线程每个读取变量,是两个读者保证同意其他两个线程发生突变的顺序?如果你的逻辑依赖于那个,那么即使每个读写都是原子的,你也有一个非常困难的线程安全问题需要处理.)

在C#中,几乎没有任何东西可以保证是原子的.简述:

  • 读取32位整数或浮点数
  • 阅读参考文献
  • 写一个32位整数或浮点数
  • 写一个参考

保证是原子的(详见具体细节.)

特别是,读取和写入64位整数或浮点数并不保证是原子的.如果你说:

C.x = 0xDEADBEEF00000000;
Run Code Online (Sandbox Code Playgroud)

在一个线程上,和

C.x = 0x000000000BADF00D;
Run Code Online (Sandbox Code Playgroud)

在另一个线程上,然后可以在第三个线程上:

Console.WriteLine(C.x);
Run Code Online (Sandbox Code Playgroud)

有写出0xDEADBEEF0BADF00D,即使逻辑上变量从未保持该值.C#语言保留写入相当于写入两个整数的长期写入的权利,一个接一个,实际上一些芯片确实以这种方式实现.第一次写入后的线程切换可能会导致读取器读取意外的内容.

它的长短是:不要在没有锁定东西的情况下在两个线程之间共享任何东西.锁定只有在满足时才会变慢; 如果由于争用锁而出现性能问题,则修复导致争用锁的任何架构缺陷.如果锁没有争用且仍然太慢,那么只有你应该考虑使用危险的低锁技术.

这里使用的常见低锁技术当然是调用Threading.Interlocked.Increment,它以保证原子的方式执行整数的增量.(但请注意,如果两个线程在不同时间分别执行两个不同变量的互锁增量,并且其他线程正在尝试确定哪个增量"首先"发生,那么它仍然无法保证会发生什么.C#不保证所有线程都可以看到单个一致的事件排序.)

  • @Joan:语言保证的原子操作正是硬件保证为原子的操作.CLR仅在具有32位原子性的硬件上运行. (6认同)
  • @binarycoder:本机具有64位指针的平台显然在硬件中具有64位原子性.CLR仅在具有*至少*32位原子性的平台上运行,而不是*最多*32位原子性.我认为这很清楚,但显然不是.抱歉. (5认同)
  • @Joan:Locks将非原子操作转化为原子操作.这就是锁定的重点.我不确定你说"一切"是原子的意思.假设您想要一个名为"read-this-queue-and-then-send-an-email"的原子操作.你可以通过锁定读取队列并发送电子邮件的呼叫来实现这一点. (4认同)
  • 重要的是要注意,这种情况不好的真正原因是死牛肉实际上被认为是好食物,至少大多数食肉动物. (2认同)

Ode*_*ded 32

不,不是.

您提出的分析非常正确 - 如果++(和--)运算符没有使用适当的锁定语义,则它们容易受到竞争条件的影响.

这些很难做到,但幸运的是,BCL为这些特定情况提供了现成的解决方案:

Interlocked.Increment如果您想要原子增量操作,则应该使用.还有一个Decrement和定义的几个其他有用的原子操作Interlocked类.

增加指定的变量并将结果存储为原子操作.

  • @DemetrisLeptos - 没关系。`++` 和 `--` 在任何可变大小下都不是原子的。它们被翻译成许多指令,因此不是原子的。 (2认同)

Hen*_*man 8

不,i++看起来很紧凑,但它确实只是简写i = i + 1,在这种形式下,更容易看到它涉及读取和写入'i'.没有锁定它根本不是线程安全的.

另外两个论点是:

  • `++'未定义为线程安全的
  • 的存在 Interlocked.Increment (ref int x)

线程安全是罕见且昂贵的,它将在可用时明确地公布.