关于线程安全的一些问题,我认为我理解,但是如果你能这么好的话,想澄清一下。我编写的特定语言是C ++,C#和Java。希望在描述特定语言关键字/功能时牢记这些。
1)1位作家,n位读者的案例。在诸如n个线程读取变量的情况下,例如在轮询循环中,并且1个写入器更新此变量,是否需要显式锁定?
考虑:
// thread 1.
volatile bool bWorking = true;
void stopWork() { bWorking = false; }
// thread n
while (bWorking) {...}
在这里,只要有一个内存屏障就足够了,用volatile做到这一点吗?根据我的理解,在上面提到的语言中,对原语的简单读写不会交错,因此不需要显式锁定,但是如果没有一些显式锁定或volatile,则无法保证内存一致性。我的假设在这里是否正确?
2)假设我的上述假设是正确的,那么它只适用于简单的读写。那就是bWorking = x ...而x = bWorking;是唯一安全的操作? IE复杂的赋值,例如一元运算符(++, - )在这里是不安全的,+ =,* =等等......?
3)我假设如果案例1是正确的,那么当只涉及分配和阅读时,扩展该语句对于n个作者和n个读者来说是否安全是不安全的?
答案 0 :(得分:2)
对于Java:
1)volatile
变量在每次读写时从/向“主存储器”更新,这意味着更新者线程的更改将在下次读取时被所有读取线程看到。此外,更新是原子的(独立于变量类型)。
2)是的,如果你有多个编写器,像++
这样的组合操作不是线程安全的。对于单个写入线程,没有问题。 (volatile
关键字确保其他线程可以看到更新。)
3)只要您只分配和读取,volatile就足够了 - 但如果您有多个编写器,则无法确定哪个值是“最终”值,或哪个值将由哪个线程读取。即使是写线程本身也无法可靠地知道它们自己的值被设置。 (如果您只有boolean
且仅从true
设置为false
,则此处没有问题。)
如果您想要更多控制,请查看java.util.concurrent.atomic包中的类。
答案 1 :(得分:1)
对于C ++
1)这很容易尝试,通常会起作用。但是,请记住以下几点:
你用布尔值来做这件事,所以这似乎最安全。其他POD类型可能也不太安全。例如。在32位计算机上设置64位双精度可能需要两条指令。所以这显然不是线程安全的。
如果布尔是唯一关心线程共享的东西,那么这可能有用。如果您将其用作双重检查锁范例的变体,则会遇到其中的所有陷阱。考虑:
std::string failure_message; // shared across threads
// some thread triggers the stop, and also reports why
failure_message = "File not found";
stopWork();
// all the other threads
while (bWorking) {...}
log << "Stopped work: " << failure_message;
首先看起来没问题,因为在failure_message
设置为false之前设置了bWorking
。但是,实际情况可能并非如此。编译器可以重新排列语句,并首先设置bWorking,导致failure_message的线程不安全访问。即使编译器没有,硬件也可能。多核cpu有自己的缓存,因此事情并非如此简单。
如果它只是一个布尔值,它可能还可以。如果不止于此,它可能会偶尔出现问题。你写的代码有多重要,你能冒这个风险吗?
2)正确,++ / - ,+ =,其他运算符将采用多个cpu指令并且线程不安全。根据您的平台和编译器,您可以编写非可移植代码来进行原子增量。
3)正确,在一般情况下这是不安全的。当你有一个线程,写一个布尔一次时,你可以发出吱吱声。一旦引入多个写入,您最好有一些真正的线程同步。
关于cpu说明的注意事项
如果一个操作需要多个指令,那么你的线程可以在它们之间被抢占 - 并且操作将部分完成。这对于线程安全来说显然是不好的,这就是为什么++,+ =等不是线程安全的原因之一。
但是,即使操作只需要一条指令,也不一定意味着它的线程安全。使用多核和多CPU时,您必须担心更改的可见性 - 何时将CPU缓存刷新到主内存。
因此,虽然多条指令确实暗示不是线程安全的,但假设单条指令暗示线程是错误的安全
答案 2 :(得分:1)
锁定。如果您正在编写多线程代码,则无论如何都需要锁定。 C#和Java使它变得相当简单。 C ++有点复杂,但你应该能够使用boost或自己创建RAII类。鉴于你将要锁定所有地方,不要试图看看是否有一些地方你可以避免它。所有这些都可以正常工作,直到你在一个关键的客户系统的星期二使用新的INtel微码在64路处理器上运行代码。然后爆炸。
人们认为锁很贵;他们真的不是。内核开发人员花了很多时间来优化它们,与一个磁盘读取相比,它们完全是微不足道的;然而似乎没有人花费这么多精力来分析每一个最后的磁盘读取
添加关于性能调优邪恶的常用陈述,来自Knuth,Spolsky ......等的明智说法,
答案 3 :(得分:0)
使用1字节的bool,您可以在不使用锁定的情况下逃脱,但由于您无法保证处理器的内部结构,因此它仍然是个坏主意。当然,除了1字节之外的任何东西,例如整数,你都不能。一个处理器可以更新它,而另一个处理器在另一个线程上读取它,你可能会得到不一致的结果。在C#中,我将围绕访问(读取或写入)bWorking使用lock {}语句。如果它更复杂,例如IO访问大内存缓冲区,我会使用ReaderWriterLock或它的一些变体。在C ++中,volatile不会有多大帮助,因为这只会阻止某些类型的优化,例如寄存器变量,这会在多线程中引起问题。您仍然需要使用锁定构造。
总而言之,我绝不会在多线程程序中读取和写入任何内容而不以某种方式锁定它。
答案 4 :(得分:0)
更新bool对任何合理的现存系统都是原子的。但是,一旦你的作者写完了,就不知道你的读者读了多久,特别是考虑到多个核心,缓存,调度程序奇怪等等之后。
增量和减量(++, - )和复合赋值(+ =,* =)的部分问题在于它们具有误导性。它们意味着某些事情正在以原子方式发生,实际上正在几次运作中发生。但即使是简单的分配也可能是不安全的,你已经放弃了布尔变量的纯度。保证像x=foo
一样简单的写入是原子的,取决于平台的细节。
我假设线程安全,你的意思是无论作者做什么,读者都会看到一致的对象。在您的示例中,这将始终是这种情况,因为布尔值只能计算两个值,两个值都有效,并且值只从true转换为false。在更复杂的情况下,线程安全将变得更加困难。