最原始的文章地址已找不到了
所谓原子操作,就是"不可中断的一个或一系列操作" , 在确认一个操作是原子的情况下,多线程环境里面,我们可以避免仅仅为保护这个操作在外围加上性能昂贵的锁,甚至借助于原子操作,我们可以实现互斥锁。
很多操作系统都为int类型提供了+-赋值的原子操作版本,比如 NT 提供了 InterlockedExchange 等API, Linux/UNIX也提供了atomic_set 等函数。
前两天有同学问我:在x86上,g_count++ (int类型) 是否是一个原子操作? 我的回答是"不是的, 多个CPU的机器(SMP)上面这就不是原子操作"。
今天想起,在单CPU上这个是否是原子操作呢,但是这个和编译器有关,编译器可能有两种编译方式:
A. 多条指令版本 , 这就不是原子的
MOV 寄存器 , g_count
ADD 寄存器, 1
MOV g_count , 寄存器
B. 单指令版本, 这在单CPU的x86上就是原子的
INC g_count
只能写程序验证了, 让5个线程每个对 g_count++ 一亿次,假如是原子操作的话,结果应该是5亿:
其实还需要对 g_count 进行volatile声明,防止编译器对这里不适当的优化,为了看看编译器对volatile的处理,我另外做了个volatile版本作为比较。
#include <windows.h>
#include <stdio.h>
int g_count = 0;
DWORD WINAPI ThreadFunc( LPVOID lpParam )
{
int i;
printf( "Thread %d start/n", (DWORD*)lpParam );
for (i=0; i <100000000 ; i++)
g_count++;
printf( "Thread %d quit/n", (DWORD*)lpParam );
return 0;
}
#define THREAD_NUM 5
VOID main( VOID )
{
DWORD dwThreadId;
HANDLE hThread;
int i;
for (i=0;i<THREAD_NUM;i++)
{
hThread = CreateThread(
NULL, // default security attributes
0, // use default stack size
ThreadFunc, // thread function
(LPVOID)i, // argument to thread function
0, // use default creation flags
&dwThreadId); // returns the thread identifier
// Check the return value for success.
if (hThread == NULL)
{
printf( "CreateThread failed./n" );
}
}
printf("Press any key after all thread exit.../n");
getchar();
printf("g_count %d/n", g_count);
if (g_count!=THREAD_NUM*100000000)
{
printf("ERROR! g_count %d!=%d/n", g_count, THREAD_NUM*100000000);
}
getchar();
//一个随手的程序,就不close handle了
}
volatile的本意是易变的, 它限制编译器的优化,因为CPU对寄存器处理比内存快很多,我想这个程序的没有加上volatile的版本优化以后应该是这样:
MOV 寄存器, g_count
for循环一亿次, 执行 INC 寄存器
MOV g_count, 寄存器
这样,最后g_count的值应该是1亿,2亿,3亿,4亿,5亿的整数,1亿出现的可能性较高。
而加上volatile以后,或者是没有代码优化的版本,都是老老实实对内存加上一亿次,假如不是原子操作的话,最后结果就会比五亿小。
用的是Vc6的cl编译器,我预期的结果是这样的:
++是原子操作 | 没有代码优化 |
代码优化(cl -O2编译) |
没有 volatile |
g_count == 五亿 |
g_count的值应该是1亿,2亿,3亿,4亿,5亿的整数 |
volatile |
g_count == 五亿 |
g_count == 五亿 |
++ 不是原子操作 | 没有代码优化 | 代码优化(cl -O2编译) |
没有 volatile |
g_count < 五亿 |
g_count的值应该是1亿,2亿,3亿,4亿,5亿的整数,1亿出现的可能性较高 |
volatile |
同上 |
g_count < 五亿 |
但是最后的结果却让我大跌了一下眼镜:
VC6实验的结果 | 没有代码优化 | 代码优化 |
没有 volatile |
g_count 一般为五亿, 偶尔< 五亿(疑惑中...) |
都是五亿(疑惑中...) |
volatile |
同上(疑惑中...) |
g_count = < 五亿(这个可以解释) |
这个结果太让人疑惑了,没办法,只能看asm代码了, 首先看看为什么volatile的版本为什么和预期不符合吧:
for (i=0; i <100000000 ; i++)
初始化i=0;
mov DWORD PTR _i$[ebp], 0
jmp SHORT $L52751
$L52752: i++
mov ecx, DWORD PTR _i$[ebp]
add ecx, 1
mov DWORD PTR _i$[ebp], ecx
$L52751: 判断 i <100000000
cmp DWORD PTR _i$[ebp], 100000000 ; 05f5e100H
jge SHORT $L52753
g_count++;
//这里发现编译使用的是多个指令,也就是说g_count++不是原子的
mov edx, DWORD PTR _g_count
add edx, 1
mov DWORD PTR _g_count, edx
jmp SHORT $L52752
//初始化 i = 100000000, 这个循环变量被直接放到了寄存器里面
mov eax, 100000000 ; 05f5e100H
$L52793:
//g_count++;这里发现编译使用的是多个指令,也就是说g_count++不是原子的
mov ecx, DWORD PTR _g_count
inc ecx
mov DWORD PTR _g_count, ecx
//下面又是循环体的asm代码
dec eax // i--
jne SHORT $L52793 // if (i>0) 则继续循环
终于发现了问题所在了, 优化以后,循环从i++变成了i--, 就是如下的形式:
for (i=100000000; i >0 ; i--)
g_count++;
因为将一个数字和0比较和将其与其他数字比较更加有效率优势,而且这里i在循环体里面并不使用,所以VC编译器将其变换成上面的形式,可以大大节省循环运行的时钟周期。
这样,未优化的版本有很大的机会出现 g_count == 五亿 就有了解释,是因为:
- CPU对于纯粹的整数运算是很快的,一亿次循环里面,可能只有一两次的线程上下文切换
- 没有优化的版本循环体比++操作本身更加耗时,这样切换操作很可能出现在 for 循环中, 而不是 g_count++的三条指令之间
这里也证明了VC6编译器对于 ++ 的运行代码是是非原子的,查了一下资料 这3条指令在pentium以后的CPU比一条inc更快
发现汇编代码的循环体完全没有了:
mov eax, DWORD PTR _g_count
push esi
add eax, 100000000 ; 05f5e100H
表示成C的代码大概就是这样: g_count+=100000000; 编译器还是很聪明,发现这个循环其实使用前面的语句也可以达到目的,干脆把循环拿掉了,这样因为线程执行时间很短,往往一个线程都执行完了其他线程还没有被调度,所以结果都是5亿了。
附带以下总结:
1. 不要小看编译器的聪明程度,上面的那些优化,我在gcc上也作了验证,我们不要太在意i++/++i之类的优化,要相信编译器能做好它
2. ++的操作在单CPU的x86上也不是原子性的,所以优化多线程性能的兄弟不要在这里搞过火,老实用InterlockedIncrement 吧
3. x86上,不管是否SMP, 对于int(要求地址4 bytes对齐)的读取和赋值还是原子的,不过这个就和这个试验无关了(RISC的机器就不要这样做了,大家还是加锁吧)
相关推荐
探究volatile和原子操作的关系 在多线程编程中,volatile和原子操作都是非常重要的概念。volatile关键字用于告诉编译器不要对变量进行优化,而原子操作则是指不可中断的操作。在本文中,我们将探究volatile和原子...
然而,`volatile`并不保证操作的原子性,所以它不能单独解决并发问题。 接着,`nonatomic`和`atomic`与Objective-C的属性有关,它们定义了属性赋值和取值操作的线程安全性。默认情况下,Objective-C的属性是`atomic...
4. **线程安全**:了解原子操作、volatile关键字和线程局部存储(TLS)在多线程编程中的应用。 5. **操作系统调度**:了解调度策略,如FCFS(先来先服务)、SJF(短作业优先)、优先级调度等。 在准备面试时,阅读...
Volatile是C/C++编程语言中的一个关键字,它与const一起被称为"cv特性",用于指示变量的值可能被系统或其他线程/进程改变,从而强制编译器每次从内存中读取该变量的最新值。在多线程或嵌入式编程中,volatile的作用...
volatile变量在编程语言中,尤其是Java和C/C++中,是一种特殊的标识符,用于指示编译器该变量的值可能在编译器不知情的情况下发生变化。这个关键字的主要作用是告诉编译器不要对这个变量进行优化,每次使用时都需要...
`volatile`关键字是C++和Java等编程语言中用于处理多线程环境或者与硬件交互时的一个关键特性。它主要用于修饰变量,表明该变量的值可能会在编译器不知情的情况下发生变化,例如由其他线程修改、外部硬件事件影响...
1. **不保证原子性**:`volatile` 并不能保证对变量的读写操作是原子性的,因此在需要保证原子性的场景下还需要结合其他机制(如锁)来实现。 2. **性能开销**:由于 `volatile` 需要禁用编译器优化并确保内存一致性...
在C++编程语言中,`volatile`和`mutable`是两个非常重要的关键字,它们分别用于处理多线程环境中的变量同步和常量类成员的特殊修改。下面将详细阐述这两个关键字的用途、注意事项以及使用场景。 `volatile`关键字:...
`std::atomic`是C++11为支持原子操作而引入的标准库部分,与之前的`volatile`关键字相比,在多线程环境下提供了更强有力的支持。书中对比了两者的特点,并指导读者如何根据实际需求选择合适的技术方案。 ##### 6. ...
在编程领域,volatile关键字在C/C++中扮演着重要的角色,尤其是在多线程、嵌入式系统以及驱动程序开发中。然而,由于其复杂的行为和编译器优化的影响,不当使用volatile可能导致难以预料的后果。本文将深入探讨...
在C++中,虽然没有直接的对应概念,但`volatile`在一定程度上确保了对变量的写操作之后,其他线程的读操作能观察到这个写操作的结果。然而,`volatile`并不能保证原子性,所以对于多线程环境下需要同步的复杂操作,...
在C++编程中,`volatile`关键字是一个非常重要的概念,它与`const`相反,用于指示编译器对待特定变量的特殊处理。`volatile`的关键作用在于提示...理解和正确使用`volatile`是编写高效、可靠的C++程序的重要组成部分。
4. **原子操作**:利用C++11及以后版本的`std::atomic`来确保指针的赋值和读取是原子的,可以防止指令重排序。 5. **即时初始化**:在获取锁后立即检查和初始化对象,这样可以确保对象构造完毕后再释放锁。 **最佳...
文章目录atomic构造赋值访问特化操作atomic_flag构造操作内存序 原子对象可以保证:从不同的线程访问其包含的数据不会造成数据竞争。此外,它还能够同步不同线程对内存的访问。 atomic 构造 default (1) atomic()...
- **C++记忆体模型**:此模型定义了多个线程如何安全地共享和访问数据,包括原子操作、内存排序以及数据竞争的处理。 - **C++对象模型**:详细解释了C++中的对象是如何在内存中表示和管理的,涉及布局、类型和大小的...
std::atomic 提供了一种原子操作机制,能够确保多线程下的数据一致性。volatile 则是一种编译器指令,用于告知编译器不要对变量进行优化。 并发 API 并发 API 是 C++11 中引入的一种多线程编程机制,能够简化多...
- `volatile` 不会提供原子操作的保障,也就是说它不能阻止多线程环境下的竞态条件。 - 单纯使用 `volatile` 关键字并不能解决所有并发问题,通常需要与其他同步机制(如互斥锁)结合使用。 - 使用 `volatile` ...