【问题标题】:Are Delphi simple types thread safe?Delphi简单类型线程安全吗?
【发布时间】:2011-03-30 01:51:27
【问题描述】:

我声明了两个全局变量:

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

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

【问题讨论】:

  • 这个问题实际上不可能以目前的形式回答。要回答这个问题,您需要准确指定 mean by threadsafe 的内容。您有一些答案认为您的意思是“我的变量是否曾遭受撕裂?”。您有一个答案,该答案涉及如何使用锁以原子方式写入这两个变量。这两种解释都可能是正确的。但我们无法确定,因为您没有指定足够的信息。

标签: multithreading delphi thread-safety


【解决方案1】:

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

原子性意味着任何读或写操作都是作为一个整体执行的。如果线程A同时执行原子写入和线程B原子读取相同的数据,线程B读取的数据始终是一致的——线程B读取的某些位不可能从当前的写入操作中获得,并且上一次写入的一些位(由线程 A)

但是原子性并不意味着线程安全——你可以很容易地用原子变量编写不安全的代码。变量本身不能是线程安全的——只有整个代码可以是(或不是)线程安全的。

【讨论】:

  • 我在这个问题上看到的最好的总结评论!
  • 是的。由于编译器或 CPU 对代码进行了重新排序,您仍然可以使用原子变量编写线程不安全的代码。或者由于 CPU 或核心之间的内存缓存。
  • 我同意你的回答,除了一件事。 原子性操作的属性,不是变量数据类型。例如,您可以说增量是原子的,但不能说整数是原子的。你可以在这里查看:en.wikipedia.org/wiki/Atomicity_(programming)
【解决方案2】:

只要只有一个线程可以写入它们,那么是的,它们是线程安全的。线程安全的真正问题是两个线程同时尝试修改一个值,而您不会在这里遇到这种情况。

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

【讨论】:

  • @Mason,好的,我同意你的回答,但是从多个线程访问全局变量即使只被读取也可以被认为是一种好的设计实践?
  • @RRUZ:取决于谁在考虑。我还没有在任何地方看到“良好设计实践的通用目标指南”,是吗?所以我给出了一个技术性的回答,因为这可以客观地回答。
  • 不@Rruz,全局变量不被认为是好的设计实践。线程与此无关。
  • @Rob,我不是在谈论全局变量。我只想指出,如果 OP 实现了一个基于全局变量和多个线程而不使用关键部分的系统(应用程序),并且有一天另一个程序员或他自己在线程内编写了一个全局变量,应用程序将变得不稳定。
  • @mghie 我指的是 Mason 在他的回答中提到的上下文切换。你不需要上下文切换来破坏撕裂。你只需要两个线程共享内存总线。
【解决方案3】:

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

我知道现在的读取大小至少是 32 位。 (在 Intel 286 年代,一次只有 8 位)。

不过,有 1 件事需要了解。尽管它一次可以读取 32 位,但它不能从任何地址开始读取。它需要是 32 位(或 4 个字节)的倍数。因此,如果整数未与 32 位对齐,则即使是整数也可以在 2 次后续读取中读取。值得庆幸的是,编译器会自动将几乎所有字段对齐到 32 位(甚至 64 位)。

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

由于它们的大小,int64 也不是线程安全的。大多数浮动类型也是如此。 (我相信除了单身)。

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

例如,

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;

我认为这比使用关键部分要快一些……但我不确定。

【讨论】:

  • +1 迄今为止最好的解释。直接分配给字节/整数是线程安全的,执行任何算术和/或逻辑需要同步。
  • 不,@Lieven,即使做算术也可以,只要只有一个线程做过。如果所有其他线程都是读者,那么没有什么可担心的。读取器线程将读取变量的先前值或新值。没有机会读取一些“中间”值。
  • @Rob,我在@Ken 给出的示例中暗示涉及多个线程时。最好明确(就像您现在的评论一样:)
【解决方案4】:

不,它们不是线程安全的,您必须使用例如临界区、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); 

【讨论】:

  • 在什么方面缺乏线程安全?这些变量中的一个是否有可能收到无效值?读者会看到错误的值吗?
  • @Rob 我的答案在您的另一条评论中。
  • @Rob 如果没有锁定,可能会出错的是读写顺序。
  • 不是真的,@David。假设某个线程在另一个线程写入变量之前读取变量很重要。如果没有关键部分,任何一个线程都可以赢得这场比赛。 有了临界区,竞争不再是谁先访问变量。相反,它是关于谁首先进入临界区。关键部分不能解决这个问题。我认为这个问题没有问任何关于需要相互一致的多个变量的值。
  • @Rob 这个问题没有指定足够的细节供任何人在我看来回答。您已经假设您认为问题是什么,但现实是 OP 并没有提出一个格式正确的问题。 OP 没有说明如何访问这些变量。 RRUZ 做了一个假设。其他人,包括你自己,做出了不同的假设。所有的答案都做出了合理的假设,但假设让你在软件开发中一事无成。
猜你喜欢
  • 2010-10-14
  • 2015-02-15
  • 1970-01-01
  • 2010-11-17
  • 1970-01-01
  • 2010-09-09
  • 1970-01-01
相关资源
最近更新 更多