Delphi简单类型是否线程安全?

13

我声明了两个全局变量:

var
  gIsRunning: Boolean = False;
  gLogCounter: Integer = 0;

这些变量只在主线程中编写,而在其他线程中读取。在这种情况下,这些变量是线程安全的吗?


6
在目前的形式下,这个问题实际上是无法回答的。要回答这个问题,您需要明确指定您所说的“线程安全”的确切含义。有些答案认为您的意思是,“我的变量是否会受到破坏?”,还有一个答案涉及如何使用锁同时写入两个变量。这些解释都可能是正确的,但我们无法确定,因为您没有提供足够的信息。 - David Heffernan
4个回答

49
您可能在谈论原子变量。整数和布尔变量是原子的。布尔值(字节)始终是原子的,整数(32位)是原子的,因为编译器正确地对齐它们。
原子性意味着任何读取或写入操作都是作为一个整体执行的。如果线程A执行原子写入,线程B在同一时间执行相同数据的原子读取,则由线程B读取的数据始终是一致的-不可能有些位由线程A的先前写入操作获取,而另一些位由当前写入操作获取。
但原子性并不意味着线程安全-您可以轻松编写使用原子变量的不安全代码。变量本身不能是线程安全的-只有作为整体的代码才能(或不能)是线程安全的。

6
我看过的关于这个问题的最佳总结评论! - Misha
1
是的。由于编译器或CPU的代码重新排序,您仍然可以使用原子变量编写线程不安全的代码。或者由于CPU或核之间的内存缓存。 - Lars Truijens
2
除了一点,我同意你的答案。原子性操作的属性,而不是变量数据类型的属性。例如,你可以说增量是原子性的,但你不能说整数是原子性的。你可以在这里查看:http://en.wikipedia.org/wiki/Atomicity_(programming) - Andrey

15
只要只有一个线程可以写入它们,那么它们就是线程安全的。线程安全的真正问题在于两个线程试图同时修改一个值,但在这里你不会遇到这种情况。
如果它们更大,例如记录或数组,那么你可能会遇到一个线程尝试写入一个值,完成一部分,然后被上下文切换,另一个线程读取部分(因此是损坏的)数据的问题。但对于单个布尔值(1字节)和整数值(4字节),编译器可以自动将它们对齐,以使CPU可以保证对它们的所有读写操作都是原子的,因此在这里这不是问题。

1
@Mason,好的,我同意你的答案,但是即使只是读取,从多个线程访问全局变量是否可以被认为是良好的设计实践呢? - RRUZ
1
@RRUZ:这要看是谁在考虑。我还没有见过任何“通用客观设计实践指南”,你见过吗?所以,我给出了一个技术性答案,因为那可以被客观地回答。 - Mason Wheeler
2
不,全局变量并不被认为是良好的设计实践。线程与此无关。 - Rob Kennedy
2
@Rob,我不是在谈论全局变量。我只是想指出,如果OP实现了一个基于全局变量和多个线程的系统(应用程序),而没有使用关键部分,并且有一天另一个程序员或他自己在线程中编写了一个全局变量,那么该应用程序将变得不稳定。 - RRUZ
2
@mghie,我指的是Mason在他的回答中提到的上下文切换。你不需要上下文切换就会遇到撕裂问题。你只需要两个线程共享内存总线即可。 - David Heffernan
显示剩余5条评论

13
简单类型只要可以在内存中单次读取(或单次写入),就是“线程安全”的。我不确定这是由CPU内存总线宽度还是它们的“整数”大小(32位和64位CPU)定义的。也许其他人可以澄清这一点。
我知道现在读取大小至少为32位。(回到英特尔286时代,一次只有8位)。
但是,有一件事情需要知道。即使它可以一次读取32位,它也不能从任意地址开始读取。它需要是32位(或4字节)的倍数。因此,如果一个整数没有对齐到32位,则可能需要在2个后续读取中读取它。值得庆幸的是,编译器会自动将几乎所有字段对齐到32位(甚至64位)。
但有一种例外情况,紧密记录(packed records)从不对其,因此,即使在这样的记录中,整数也不会是线程安全的。
由于它们的大小,int64也不是线程安全的。大多数浮点类型也是如此。(除了Single类型)
现在,有了这些认识,有些情况下你实际上可以从多个线程写入全局变量,并且仍然是“线程安全”的。
例如:
var
  LastGoodValueTested : Integer

procedure TestValue(aiValue : Integer);
begin
  if ValueGood(aiValue) then
    LastGoodValue := aiValue
end;

在这里,你可以从多个线程中调用TestValue例程,并且不会破坏LastGoodValueTested变量。然而,可能出现写入变量的值不是最后一个值的情况,这取决于ValueGood(aiValue)和赋值之间是否发生了线程上下文切换。因此,根据需要,这可能或可能不可接受。

现在,

var     
  gLogCounter: Integer = 0;

procedure Log(S : string);
begin
  gLogCounter := gLogCounter + 1;
end;

在这里,您实际上可以使计数器变得不正确,因为它不是一元操作。您首先读取该变量。 然后将其加1。 然后您将其保存回去。在线程执行这些操作的中间可能会发生上下文切换。因此,这种情况需要同步。

在这种情况下,可以重写为

procedure Log(S : string);
begin
  InterlockedIncrement(gLogCounter);
end;

我认为这比使用临界区稍微快一些,但我不确定。


到目前为止,这是最好的解释。直接分配给一个字节/整数是线程安全的,但进行任何算术和/或逻辑操作都需要同步。 - Lieven Keersmaekers
不,@Lieven,即使进行算术运算也没问题,只要只有一个线程执行它。如果所有其他线程都是读取器,则没有什么可担心的。读取器线程将读取变量的先前值或新值。没有读取某些“中间”值的机会。 - Rob Kennedy
@Rob,我是指当涉及到多个线程时,就像 @Ken 给出的示例一样。 最好明确说明这一点(正如您现在的评论所做的那样:) - Lieven Keersmaekers

8
它们不是线程安全的,您必须使用临界区,例如使用InitializeCriticalSectionEnterCriticalSectionLeaveCriticalSection函数来访问这些变量。请注意保留HTML标记。
//declaration of your global variables 
var 
   MyCriticalSection: TRTLCriticalSection;
   gIsRunning: Boolean;
   gLogCounter: Integer;

//before the threads starts
InitializeCriticalSection(MyCriticalSection); 

//Now in your thread 
  EnterCriticalSection(MyCriticalSection); 
//Here you can make changes to your variables. 
  gIsRunning:=True;
  inc(gLogCounter); 
//End of protected block
LeaveCriticalSection(MyCriticalSection); 

1
哪些方面缺乏线程安全性?其中有没有可能有这些变量会收到无效值?读者是否会看到不正确的值? - Rob Kennedy
@Rob 我的答案在你的另一条评论中。 - RRUZ
2
@Rob 如果没有锁定,可能出现问题的是读写顺序。 - David Heffernan
不完全是这样,@David。假设在另一个线程写入变量之前,某个线程读取该变量很重要。如果没有关键部分,任何一个线程都可能赢得比赛。有了关键部分,比赛就不再是谁先访问变量的问题了。相反,它是关于谁先进入关键部分的问题。关键部分并不能解决这个问题。我认为问题并没有询问关于多个变量的值需要相互一致的事情。 - Rob Kennedy
3
在我看来,这个问题没有提供足够的细节以便回答。你假设了问题是什么,但事实上OP并没有提出一个明确的问题。OP没有说明如何访问这些变量。RRUZ提出了一种假设,而其他人,包括你自己,则提出了不同的假设。所有答案都做出了合理的假设,但在软件开发中,假设是毫无意义的。 - David Heffernan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接