2011-03-30 103 views
10

我声明了两个全局变量:Delphi简单类型线程安全吗?

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

这些变量是只写在主线程和其他线程读取。在这种情况下,这些变量是否线程安全?

+5

这个问题实际上不可能以目前的形式回答。要回答它,你需要明确地指出你[线程安全的意思](http://blogs.msdn.com/b/ericlippert/archive/2009/10/19/what-is-this-thing-you-call -thread-safe.aspx)。你有一些答案认为你的意思是“我的变量是否遭受撕裂?”。你有一个答案,处理如何通过使用锁来自动写入两个变量。这两种解释都可能是正确的。但是我们无法分辨,因为您没有足够的信息。 – 2011-03-30 08:02:40

回答

46

您可能在说关于原子变量。整数和布尔变量是原子的。布尔(字节)总是原子的,整数(32位)是原子的,因为编译器正确地对齐它们。

原子性意味着任何读取或写入操作都是作为一个整体来执行的。如果线程A同时执行原子写入和线程B原子读取相同数据,则线程B读取的数据总是一致的 - 线程B读取的某些比特不可能从当前写入操作获得,并且前面写的一些位(线程A)

但是,原子性并不意味着线程安全 - 您可以使用原子变量轻松编写不安全的代码。变量本身不能是线程安全的 - 只有整个代码可以是(或不是)线程安全的。

+6

我在这个问题上看到的最佳汇总评论! – Misha 2011-03-30 03:55:43

+0

+1“只有代码作为一个整体可以(或不)线程安全” – 2011-03-30 06:23:06

+1

是的。由于代码由编译器或CPU重新排序,您仍然可以使用原子变量编写线程不安全的代码。或者由于CPU或内核之间的内存缓存。 – 2011-03-30 11:08:59

8

不,他们不是线程安全,则必须使用例如临界区访问这些变量,使用InitializeCriticalSectionEnterCriticalSectionLeaveCriticalSection功能

//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

以什么方式缺乏线程安全性?有没有机会获得这些变量之一的无效值?读者会看到一个不正确的值吗? – 2011-03-30 02:24:09

+0

@Rob我的回答是在你的另一个评论。 – RRUZ 2011-03-30 02:29:18

+2

@Rob如果不锁定,读取和写入的顺序可能是错误的。 – 2011-03-30 07:59:19

13

只要有只有一个线程可以写给他们,然后是的,他们是线程安全的。线程安全的真正问题是两个线程试图同时修改一个值,并且在这里你不会有这个问题。

如果它们较大,比如记录或数组,那么您可能会遇到一个线程试图写入值,中途转入,然后获取上下文切换以及另一个线程读取部分(因此损坏)数据的问题。但对于单个布尔值(1字节)和整数(4字节)值,编译器可以自动对齐它们,使得CPU可以保证所有对它们的读写都是原子的,所以这不是问题。

+0

@梅森,好吧,我同意你的答案,但即使只读了多线程访问全局变量也可以被认为是一个好的设计实践吗? – RRUZ 2011-03-30 02:13:43

+1

@RRUZ:取决于谁在考虑。我没有看到任何地方的“通用目标良好设计实践指南”,对吗?所以我给出了一个技术性的答案,因为这*可以被客观地回答。 – 2011-03-30 02:20:04

+2

没有@Rruz,全局变量不被认为是很好的设计实践。线程与此无关。 – 2011-03-30 02:20:34

11

简单类型是“线程安全的”,只要它们可以从内存中读取一次(或写入一次写入)即可。我不确定它是由CPU内存总线宽度还是它们的“整数”大小(32位与64位CPU)定义的。也许别人可以澄清那部分。

我知道阅读的大小nowaday是至少32位。 (回到英特尔286天,一次只有8位)。

虽然有一点需要了解。尽管它一次可以读取32位,但它不能在任何地址开始读取。它需要是32位(或4字节)的倍数。所以,即使一个整数可以在2个后续读取中读取,如果它没有对齐到32位。谢天谢地,编译器会自动将几乎所有的字段对齐到32位(甚至64位)。

虽然有一个例外,但是打包的记录永远不会对齐,因此即使是这样的记录中的整数也不会是线程安全的。

由于它们的大小,int64也不是线程安全的。同样可以讲述大多数浮动类型。 (除了我认为的单数)。

现在,考虑到所有这些情况,您可以从多个线程实际编写一个全局变量,但仍然是“线程安全的”。

例如,

var 
    LastGoodValueTested : Integer 

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

在这里,你可以调用多个线程的常规测试值,你就不会腐败的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; 

我认为这是比使用临界区稍快...但是,我不知道。

+0

+1迄今为止的最好解释。直接分配给一个字节/整数是线程安全的,执行*任何*算术和/或逻辑需要同步。 – 2011-03-30 06:19:33

+0

不,@Lieven,即使做算术也行,只要只有一个线程做它。如果所有其他线程都是读者,那么没有什么可担心的。读者线程将读取变量的前一个值或新值。没有机会阅读一些“中间值”。 – 2011-03-30 14:24:31

+0

@Rob,当涉及多个线程时,我暗示了@Ken给出的例子。最好是明确的*(正如你现在的评论所做的那样)* – 2011-03-30 14:36:00