Windows中的原子性,波动性和线程安全性

时间:2015-01-24 07:37:18

标签: windows multithreading thread-safety atomicity volatility

我对原子性的理解是,它用来确保一个值可以整体读取/写入而不是部分读取/写入。例如,64位值实际上是两个32位DWORD(假设此处为x86),在线程之间共享时必须是原子的,以便同时读/写两个DWORD。这样一个线程就无法读取未更新的半变量。你如何保证原子性?

此外,我的理解是,波动性根本不能保证线程安全。这是真的吗?

我已经看到它暗示许多只是原子/易失性的地方是线程安全的。我不知道那是怎么回事。我还需要一个内存屏障,以确保在实际可以保证在另一个线程中读/写之前,读/写任何原子或其他值?

因此,例如,让我说我创建一个线程暂停,做一些计算将一些值更改为线程可用的结构然后恢复,例如:

HANDLE hThread = CreateThread(NULL, 0, thread_entry, (void *)&data, CREATE_SUSPENDED, NULL);
data->val64 = SomeCalculation();
ResumeThread(hThread);

我认为这取决于ResumeThread中的任何内存障碍?我应该为val64进行互锁交换吗?如果线程正在运行,那会怎么改变呢?

我确定我在这里问了很多,但基本上我想弄清楚的是我在标题中提出的问题:对Windows中的原子性,波动性和线程安全性的一个很好的解释。感谢

2 个答案:

答案 0 :(得分:6)

  

它用于确保整个

中的值被读/写

这只是原子性的一小部分。其核心意味着“不间断”,即处理器上的指令,其副作用不能与另一指令交错。通过设计,当内存更新可以通过单个内存总线周期执行时,内存更新是原子的。这要求内存位置的地址对齐,以便单个周期可以更新它。未对齐的访问需要额外的工作,部分字节由一个周期写入,另一个周期。现在它不再是不可中断的了。

获得对齐更新非常简单,它是编译器提供的保证。或者,更广泛地说,由编译器实现的内存模型。它只选择对齐的内存地址,有时故意留下几个字节的未使用间隙以使下一个变量对齐。对变量的更新大于处理器的本机字大小永远不会是原子的。

但更重要的是使线程工作所需的处理器指令类型。每个处理器都实现了CAS instruction,compare-and-swap的变体。它是实现同步所需的核心原子指令。更高级别的同步原语,如监视器(也称为条件变量),互斥体,信号,关键部分和信号量都建立在该核心指令之上。

这是最小的,处理器通常提供额外的操作来使简单的操作成为原子。就像递增一个变量一样,它的核心是一个可中断的操作,因为它需要一个读 - 修改 - 写操作。需要它是原子的是很常见的,大多数C ++程序都依赖于它来实现引用计数。

  

波动率并不能保证线程安全

没有。这是一个属性,可以追溯到更简单的时间,当机器只有一个处理器核心时。它只影响代码生成,特别是代码优化器尝试消除内存访问并在处理器寄存器中使用值的副本的方式。对代码执行速度产生很大的巨大差异,从寄存器读取值比从内存中读取值快3倍。

应用 volatile 可确保代码优化器不会认为寄存器中的值是准确的,并强制它再次读取内存。它真正唯一关心的是那些本身不稳定的存储器值,这些器件通过存储器映射的I / O暴露它们的寄存器。它已被大量滥用,因为该核心意义是试图将语义放在具有弱内存模型的处理器之上,Itanium是最令人震惊的例子。您今天使用 volatile 获得的内容在很大程度上取决于您使用的特定编译器和运行时。永远不要将它用于线程安全,而是始终使用同步原语。

  

简单地说是原子/易失性是线程安全的

如果这是真的,编程会简单得多。原子操作只涵盖非常简单的操作,真正的程序通常需要保持整个对象的线程安全。使其所有成员以原子方式更新,并且永远不会公开部分更新的对象的视图。像迭代列表这样简单的东西是一个核心示例,当您查看其元素时,您不能让另一个线程修改列表。那时你需要达到更高级别的同步原语,这种原语可以阻止代码直到可以安全地继续进行。

真正的程序经常受到此同步需求的影响,并且表现出Amdahls' law行为。换句话说,添加额外的线程实际上并不会使程序更快。有时实际上让它变慢。无论谁找到一个更好的鼠标陷阱,保证诺贝尔,我们还在等待。

答案 1 :(得分:2)

一般来说,C和C ++对于如何阅读或写一个“易变”的内容并不提供任何保证。对象在多线程程序中表现。 ('新的' C ++ 11可能会做,因为它现在包含线程作为标准的一部分,但是传统的线程不是标准C或C ++的一部分。)使用volatile并对原子性和缓存做出假设 - 代码中的兼容性意味着可移植性是一个问题。关于特定编译器和平台是否会处理对“易失性”的访问,这是一个废话。以线程安全的方式对象。

一般规则是:' volatile'不足以确保线程安全访问。您应该使用一些平台提供的机制(通常是一些函数或同步对象)来安全地访问线程共享值。

现在,特别是在Windows上,特别是在VC ++ 2005+编译器上,特别是在x86和x64系统上,访问原始对象(如int)可以在以下情况下成为线程安全的:

  1. 在64位和32位Windows上,对象必须是32位类型,并且必须是32位对齐。
  2. 在64位Windows上,该对象也可以是64位类型,并且必须是64位对齐的。
  3. 必须声明为volatile。
  4. 如果这些是真的,那么对对象的访问将是易失性的,原子的并且被确保高速缓存一致性的指令所包围。必须满足大小和对齐条件,以便编译器生成在访问对象时执行原子操作的代码。声明对象volatile可确保编译器不会进行与缓存可能已读入寄存器的先前值相关的代码优化,并确保生成的代码在访问时包含适当的内存屏障指令。

    即便如此,您可能仍然可以更好地使用Interlocked *函数来访问小东西,并使标准同步对象(如Mutexes或CriticalSections)陷入更大的对象和数据结构。理想情况下,获取库并使用已包含适当锁的数据结构。让你的图书馆和操作系统尽可能地努力工作!

    在您的示例中,我希望您确实需要使用线程安全访问来更新val64,无论线程是否已启动。

    如果线程已经在运行,那么你肯定需要某种线程安全的写入val64,使用InterchangeExchange64或类似的,或者通过获取和释放某种将执行适当的内存屏障指令的同步对象。同样,线程也需要使用线程安全访问器来读取它。

    如果线程还没有恢复,那么它就不那么清楚了。 ResumeThread可能使用或充当同步函数并执行内存屏障操作,但文档没有指定它,因此最好假设它没有。

    参考文献:

    关于32位和64位对齐类型的原子性...... https://msdn.microsoft.com/en-us/library/windows/desktop/ms684122%28v=vs.85%29.aspx

    On' volatile'包括记忆围栏... https://msdn.microsoft.com/en-us/library/windows/desktop/ms686355%28v=vs.85%29.aspx