注意:我真的不太擅长多线程编程,但是我目前的项目让我这样做,所以我试图了解线程安全和什么不是。
我正在阅读Eric Lippert的awesome answers on what ++i does之一。他说这才是真正发生的事情:
这让我思考,如果调用++ i的两个线程怎么办?如果第一个线程在步骤3,当第二个线程在步骤2时。(意味着如果第二个线程在第一个线程将新值存储在变量中之前将值复制到临时位置,该怎么办?)
如果发生这种情况,那么两个线程似乎只会增加i
一次而不是两次。 (除非整件事都在lock
。)
答案 0 :(得分:75)
正如其他答案所指出的那样,不,++不是“线程安全”。
当你学习多线程及其危害时,我认为会有所帮助的东西是开始非常准确地说“线程安全”是什么意思,因为不同的人用它来表示不同的东西。基本上,您在此关注的线程安全方面是操作是否为 atomic 。 “原子”操作是在被另一个线程中断时保证不会中途完成的操作。
(还有很多其他的线程问题与原子性无关,但可能仍然属于某些人对线程安全性的定义。例如,给定两个线程,每个线程都在变量,两个线程各自读取变量,两个读者是否保证同意其他两个线程突变的 order ?如果你的逻辑依赖于那个,那么你就有一个非常困难的线程安全问题需要处理,即使每次读取和写是原子的。)
在C#中,几乎没有任何东西可以保证是原子的。简言之:
保证是原子的(详细信息请参阅规范。)
特别是,读取和写入64位整数或浮点数不保证是原子的。如果你说:
C.x = 0xDEADBEEF00000000;
在一个帖子上,
C.x = 0x000000000BADF00D;
在另一个线程上,然后可以在第三个线程上:
Console.WriteLine(C.x);
写出0xDEADBEEF0BADF00D,即使逻辑变量从未保存该值。 C#语言保留写入相当于写入两个整数的长期写入的权利,一个接一个,实际上一些芯片确实以这种方式实现。第一次写入后的线程切换可能会导致读者读取意外的内容。
它的长短是:不要在没有锁定东西的情况下在两个线程之间共享任何。锁定只有在满足时才会变慢;如果由于争用锁而出现性能问题,则修复导致争用锁的任何架构缺陷。如果锁没有争用并且仍然太慢,那么你是否应该考虑采用危险的低锁技术。
这里使用的常见低锁技术当然是调用Threading.Interlocked.Increment
,它以保证原子的方式执行整数的增量。 (但请注意,如果两个线程在不同时间分别执行两个不同变量的互锁增量,并且其他线程正在尝试确定哪个增量“首先”发生,那么它仍然无法保证会发生什么.C#不保证所有线程都可以看到单个一致的事件排序。)
答案 1 :(得分:32)
不,不是。
您提出的分析非常正确 - ++
(和--
)运算符如果没有使用适当的锁定语义,则容易受到竞争条件的影响。
这些很难做到,但幸运的是,BCL为这些具体案例提供了现成的解决方案:
如果您想要原子增量操作,则应使用Interlocked.Increment
。在Decrement
类上还定义了Interlocked
和其他几个有用的原子操作。
增加指定变量并将结果存储为原子操作。
答案 2 :(得分:8)
不,i++
看起来很紧凑,但它只是i = i + 1
的简写,在这种形式下,更容易看到它涉及读取和写入'i'。没有锁定它根本不是线程安全的。
另外两个论点是:
Interlocked.Increment (ref int x)
线程安全性很少且价格昂贵,如果可用的话,它会被明确地公布。