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

loo*_*oop 6 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);
Run Code Online (Sandbox Code Playgroud)

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

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

Han*_*ant 6

它用于确保整个读/写值

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

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

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

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

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

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

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

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

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

真正的程序经常受到这种同步需求的影响,并表现出Amdahls的法律行为.换句话说,添加额外的线程实际上并不会使程序更快.有时实际上让它变慢.无论是谁找到了更好的鼠标陷阱都是诺贝尔,我们还在等待.