繁体   English   中英

线程安全通则

[英]Thread Safety General Rules

关于线程安全的一些问题,我认为我理解,但是如果你能这么好的话,想澄清一下。 我编写的特定语言是C ++,C#和Java。 希望在描述特定语言关键字/功能时牢记这些。

1)1位作家,n位读者的案例。 在诸如n个线程读取变量的情况下,例如在轮询循环中,并且1个写入器更新此变量,是否需要显式锁定?

考虑:

// thread 1.
volatile bool bWorking = true;

void stopWork() { bWorking = false; }

// thread n
while (bWorking) {...}

在这里,只要有一个内存屏障就足够了,用volatile来实现这个目标吗? 根据我的理解,在我上面提到的语言中,对原语的简单读写不会交错,因此不需要显式锁定,但是如果没有一些显式锁定或volatile,则无法保证内存一致性。 我的假设在这里是否正确?

reads and writes. 2)假设我的上述假设是正确的,那么它只适用于读写操作。 那就是bWorking = x ...而x = bWorking; 是唯一安全的操作? IE复杂的赋值如一元运算符(++, - )在这里是不安全的,+ =,* =等......?

3)我假设如果案例1是正确的,那么当只涉及分配和阅读时,扩展该语句对于n个作者和n个读者来说是否安全是不安全的?

对于Java:

1)在每次读取写入时,从/向“主存储器”更新volatile变量,这意味着更新者线程的更改将在下次读取时被所有读取线程看到。 此外,更新是原子的(独立于变量类型)。

2)是的,如果您有多个编写器,像++这样的组合操作不是线程安全的。 对于单个写入线程,没有问题。 volatile关键字确保其他线程可以看到更新。)

3)只要你只分配和读取,volatile就足够了 - 但是如果你有多个写入器,你就不能确定哪个值是“最终”值,或者哪个值将由哪个线程读取。 即使是写线程本身也无法可靠地知道它们自己的值被设置。 (如果你只有boolean并且只能从true设置为false ,那么这里没有问题。)

如果您想要更多控制,请查看java.util.concurrent.atomic包中的类。

对于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之前设置bWorking设置为false。 但是,实际情况可能并非如此。 编译器可以重新排列语句,并首先设置bWorking,导致failure_message的线程不安全访问。 即使编译器没有,硬件也可能。 多核cpu有自己的缓存,因此事情并不那么简单。

如果它只是一个布尔值,它可能没问题。 如果不止于此,它可能会偶尔出现问题。 你写的代码有多重要,你能冒这个风险吗?

2)正确,++ / - ,+ =,其他运算符将采用多个cpu指令并且线程不安全。 根据您的平台和编译器,您可以编写非可移植代码来进行原子增量。

3)正确,这在一般情况下是不安全的。 当你有一个线程,写一个布尔值一次时,你可以发出吱吱声。 一旦引入多个写入,您最好有一些真正的线程同步。

关于cpu指令的注意事项

如果一个操作需要多个指令,那么你的线程可以在它们之间被抢占 - 并且操作将部分完成。 这对于线程安全来说显然是不好的,这就是为什么++,+ =等不是线程安全的原因之一。

但是,即使操作只需要一条指令,也不一定意味着它的线程安全。 使用多核和多CPU时,您必须担心更改的可见性 - 何时将CPU缓存刷新到主内存。

因此,虽然多个指令确实意味着不是线程安全的 ,但假设单个指令意味着线程安全是错误的

做锁定。 如果您正在编写多线程代码,则无论如何都需要锁定。 C#和Java使它变得相当简单。 C ++有点复杂,但你应该能够使用boost或自己创建RAII类。 鉴于你将要锁定所有地方,不要试图看看是否有一些地方你可以避免它。 所有这些都可以正常工作,直到你在一个关键的客户系统的星期二使用新的INtel微码在64路处理器上运行代码。 然后砰的一声。

人们认为锁很贵; 他们真的不是。 内核开发人员花了很多时间来优化它们,与一个磁盘读取相比,它们完全是微不足道的; 然而似乎没有人花费这么多精力来分析每一个最后的磁盘读取

添加关于性能调优邪恶的通常陈述,来自Knuth,Spolsky ......等的明智的说法,

使用1字节的bool,您可以在不使用锁定的情况下逃脱,但由于您无法保证处理器的内部结构,因此它仍然是个坏主意。 当然,除了1字节之外的任何东西,例如整数,你都不能。 一个处理器可以更新它,而另一个处理器在另一个线程上读取它,你可能会得到不一致的结果。 在C#中,我将围绕访问(读取或写入)bWorking使用lock {}语句。 如果它更复杂,例如IO访问大内存缓冲区,我会使用ReaderWriterLock或它的一些变体。 在C ++中,volatile不会有多大帮助,因为这只会阻止某些类型的优化,例如寄存器变量,这会在多线程中引起问题。 您仍然需要使用锁定构造。

总而言之,我绝不会在多线程程序中读取和写入任何内容而不以某种方式锁定它。

  1. 在任何合理的现存系统中更新bool将是原子的。 但是,一旦你的作者写完,就不知道你的读者读了多久,特别是考虑到多个核心,缓存,调度程序奇怪等等。

  2. 增量和减量(++, - )和复合赋值(+ =,* =)的部分问题在于它们具有误导性。 它们意味着某些事情正在以原子方式发生,实际上正在几次运作中发生。 但即使是简单的分配也可能是不安全的,你已经放弃了布尔变量的纯度。 保证像x=foo这样简单的写入是原子的,取决于平台的细节。

  3. 我假设线程安全,你的意思是无论作者做什么,读者总会看到一致的对象。 在您的示例中,这将始终是这种情况,因为布尔值只能计算两个值,两个值都有效,并且值只从true转换为false。 在更复杂的情况下,线程安全将变得更加困难。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM