线程安全通用规则

本文关键字:规则 安全 线程 | 更新日期: 2023-09-27 18:04:32

关于线程安全的几个问题,我认为我理解,但希望澄清,如果你能这么好。我使用的编程语言有c++、c#和Java。希望在描述特定的语言关键字/功能时记住这些。

1) 1个写入器,n个读取器的情况。在n个线程读取一个变量的情况下,比如在轮询循环中,一个写入器更新这个变量,需要显式锁定吗?

考虑:

// thread 1.
volatile bool bWorking = true;
void stopWork() { bWorking = false; }
// thread n
while (bWorking) {...}

在这里,仅仅有一个内存屏障并使用volatile来完成它就足够了吗?正如我所理解的,在我上面提到的语言中,对原语的简单读写不会交错,因此不需要显式锁,但是如果没有显式锁或易失性,则无法保证内存一致性。我的假设正确吗?

假设我上面的假设是正确的,那么它只对简单的读写是正确的。这是b工作= x…和x = b工作;只有安全的操作吗?IE复杂的赋值操作,如一元操作符(++,——)在这里是不安全的,+=,*=等也是如此… ?

3)我假设如果情况1是正确的,那么当只涉及赋值和读取时,将该语句扩展为对n个写入器和n个读取器也是安全的是不安全的?

线程安全通用规则

对于Java:

1) volatile变量在每次读写时从/更新到"主存",这意味着更新线程的更改将被所有读线程在下一次读时看到。而且,更新是原子的(与变量类型无关)。

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

3)只要你只赋值和读,volatile就足够了——但是如果你有多个写器,你不能确定哪个值是"最终"的,或者哪个值将被哪个线程读取。甚至写线程本身也不能可靠地知道它们自己的值被设置了。(如果你只有boolean,只从true设置到false,这里没有问题。)

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

执行锁定。如果您正在编写多线程代码,那么无论如何都需要锁。c#和Java让它变得相当简单。c++稍微复杂一些,但您应该能够使用boost或创建自己的RAII类。考虑到你将在所有的地方都被锁定,不要试图去看是否有几个地方你可以避免它。在三月的某个星期二,在一些关键任务客户系统上使用新的英特尔微码在64路处理器上运行代码之前,一切都将正常运行。然后爆炸。

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

添加关于性能调优弊端的常见陈述,来自Knuth, Spolsky ......的明智言论等,等,

For 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缓存刷新到主存。

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

使用一个1字节的bool,您可能可以不使用锁定,但由于您不能保证处理器的内部结构,这仍然是一个坏主意。当然,对于超过1字节的任何内容,例如整数,您都不能。一个处理器可能正在更新它,而另一个处理器正在另一个线程上读取它,您可能会得到不一致的结果。在c#中,我会在访问(读或写)bWorking时使用lock{}语句。如果是更复杂的事情,例如对大内存缓冲区的IO访问,我会使用ReaderWriterLock或它的一些变体。在c++中,volatile没有多大帮助,因为它只会阻止某些类型的优化,比如寄存器变量,而这些优化会在多线程中完全导致问题。您仍然需要使用锁结构。

所以总的来说,我不会在一个多线程程序中读写任何东西而不以某种方式锁定它。
  1. 在任何现有的系统上,更新bool都是原子性的。然而,一旦写入器完成写入,就不知道读取器还需要多长时间才能读取,尤其是考虑到多个核心、缓存、调度程序等问题时。

  2. 递增和递减(++,——)和复合赋值(+=,*=)的部分问题在于它们具有误导性。它们暗示着原子地发生的事情实际上是在几个操作中发生的。但是即使是简单的赋值也可能是不安全的,如果您偏离了布尔变量的纯粹性。确保像x=foo这样简单的写入是原子的取决于您的平台的细节。

  3. 我假设你说的线程安全是指无论编写程序做什么,读取程序总是看到一个一致的对象。在您的示例中,情况总是如此,因为布尔值只能计算为两个值,两个值都是有效的,并且值仅从true转换为false一次。在更复杂的情况下,线程安全将变得更加困难。