Windows 线程(九) 线程的原子操作和旋转锁
原子操作
首先, 我们先来看如下的例子
#include <Windows.h>
#include <process.h>
#include <tchar.h>
#include <iostream>
INT g_nNum = 0;
UINT WINAPI ThreadRun1(LPVOID lParam)
{
g_nNum += 1;
return 0;
}
UINT WINAPI ThreadRun2(LPVOID lParam)
{
g_nNum += 1;
return 0;
}
int main()
{
_tsetlocale(LC_ALL, TEXT(""));
HANDLE hThreads[2];
hThreads[0] = (HANDLE)_beginthreadex(NULL, 0, ThreadRun1, NULL, 0, NULL);
hThreads[1] = (HANDLE)_beginthreadex(NULL, 0, ThreadRun2, NULL, 0, NULL);
WaitForMultipleObjects(sizeof(hThreads) / sizeof(HANDLE), hThreads, TRUE, INFINITE);
_tprintf(TEXT("g_nNum:[%d]\n"), g_nNum);
for (size_t i = 0; i < sizeof(hThreads) / sizeof(HANDLE); ++i)
{
CloseHandle(hThreads[i]);
}
return 0;
}按道理来讲, 我们的g_nNum绝对等于2, 这个小学数学没毛病!
然而我们多次运行上边的代码, 就会发现, 我们的世界观崩塌啦! 竟然有等于1的结果, 如图 
这是为什么呢?
我们从反汇编的角度看一看
下面是反汇编代码
1: #include <Windows.h>
2: #include <process.h>
3: #include <tchar.h>
4: #include <iostream>
5:
6: INT g_nNum = 0;
7:
8: UINT WINAPI ThreadRun1(LPVOID lParam)
9: {
00A51770 55 push ebp
00A51771 8B EC mov ebp,esp
00A51773 81 EC C0 00 00 00 sub esp,0C0h
00A51779 53 push ebx
00A5177A 56 push esi
00A5177B 57 push edi
00A5177C 8D BD 40 FF FF FF lea edi,[ebp-0C0h]
00A51782 B9 30 00 00 00 mov ecx,30h
00A51787 B8 CC CC CC CC mov eax,0CCCCCCCCh
00A5178C F3 AB rep stos dword ptr es:[edi]
10: g_nNum += 1;
00A5178E A1 38 A1 A5 00 mov eax,dword ptr [g_nNum (0A5A138h)]
00A51793 83 C0 01 add eax,1
00A51796 A3 38 A1 A5 00 mov dword ptr [g_nNum (0A5A138h)],eax
11: return 0;
00A5179B 33 C0 xor eax,eax
12: }
00A5179D 5F pop edi
00A5179E 5E pop esi
00A5179F 5B pop ebx
00A517A0 8B E5 mov esp,ebp
00A517A2 5D pop ebp
00A517A3 C2 04 00 ret 4
13:
14: UINT WINAPI ThreadRun2(LPVOID lParam)
15: {
00A517C0 55 push ebp
00A517C1 8B EC mov ebp,esp
00A517C3 81 EC C0 00 00 00 sub esp,0C0h
00A517C9 53 push ebx
00A517CA 56 push esi
00A517CB 57 push edi
00A517CC 8D BD 40 FF FF FF lea edi,[ebp-0C0h]
00A517D2 B9 30 00 00 00 mov ecx,30h
00A517D7 B8 CC CC CC CC mov eax,0CCCCCCCCh
00A517DC F3 AB rep stos dword ptr es:[edi]
16: g_nNum += 1;
00A517DE A1 38 A1 A5 00 mov eax,dword ptr [g_nNum (0A5A138h)]
00A517E3 83 C0 01 add eax,1
00A517E6 A3 38 A1 A5 00 mov dword ptr [g_nNum (0A5A138h)],eax
17: return 0;
00A517EB 33 C0 xor eax,eax
18: }我们可以看到, g_nNum += 1 的反汇编指令就是如下代码:
mov eax,dword ptr [g_nNum (0A5A138h)]
add eax,1
mov dword ptr [g_nNum (0A5A138h)],eax 那么, 我们要知道, CPU的最小执行单元就是一句汇编指令
而不是 g_nNum += 1 这一句代码
所以, 我们的CPU可能会出现如下执行过程:
; 线程ThreadRun1 执行..
mov eax,dword ptr [g_nNum (0A5A138h)]
; 线程ThreadRun1时间片用完, 保存CONTEXT
; 此时, EAX == 0
; 线程切换, ThreadRun2 执行
; 加载 ThreadRun2 的 CONTEXT
mov eax,dword ptr [g_nNum (0A5A138h)]
; 此时, EAX == 0
add eax,1
; 此时, EAX == 1
mov dword ptr [g_nNum (0A5A138h)],eax
; 此时, g_nNum == 1
; 线程ThreadRun2时间片用完, 保存CONTEXT
; 线程切换, ThreadRun1 执行
; 加载 ThreadRun1 的CONTEXT
; 此时, EAX == 0
add eax,1
; 此时, EAX == 1
mov dword ptr [g_nNum (0A5A138h)],eax
; 此时, g_nNum == 1经过如上的步骤, g_nNum成功的错误了...
那么我们如何进行避免呢?
我们可以使用原子操作的方式
原子操作/线程锁
什么叫做原子操作?
原子操作就是同一资源在同一时间只有一个线程能够访问
它是系统级的操作
它是在硬件中进行的限制操作
Windows中提供了一系列的原子操作API
比如InterlockedExchange和一系列InterlockedExchange开头的函数
在WindowsAPI中, 有一个API是 InterlockedExchangeAdd
这个函数会帮我们进行锁的操作
我们将代码改造如下:
#include <Windows.h>
#include <process.h>
#include <tchar.h>
#include <iostream>
INT g_nNum = 0;
UINT WINAPI ThreadRun1(LPVOID lParam)
{
//g_nNum += 1;
InterlockedExchangeAdd((LONG*)&g_nNum, 1);
return 0;
}
UINT WINAPI ThreadRun2(LPVOID lParam)
{
//g_nNum += 1;
InterlockedExchangeAdd((LONG*)&g_nNum, 1);
return 0;
}
int main()
{
_tsetlocale(LC_ALL, TEXT(""));
HANDLE hThreads[2];
hThreads[0] = (HANDLE)_beginthreadex(NULL, 0, ThreadRun1, NULL, 0, NULL);
hThreads[1] = (HANDLE)_beginthreadex(NULL, 0, ThreadRun2, NULL, 0, NULL);
WaitForMultipleObjects(sizeof(hThreads) / sizeof(HANDLE), hThreads, TRUE, INFINITE);
_tprintf(TEXT("g_nNum:[%d]\n"), g_nNum);
for (size_t i = 0; i < sizeof(hThreads) / sizeof(HANDLE); ++i)
{
CloseHandle(hThreads[i]);
}
return 0;
}旋转锁
我们可以通过InterlockedExchangeAPI实现一个旋转锁
实现旋转锁的一种方式:
#include <Windows.h>
#include <process.h>
#include <tchar.h>
#include <iostream>
BOOL g_bIsUse = FALSE;
UINT WINAPI ThreadRun1(LPVOID lParam)
{
// 上锁
while (InterlockedExchange((LONG*)&g_bIsUse, TRUE) == TRUE)
Sleep(0);
// 上锁完成, 进行操作
// 这里的代码是绝对线程安全的
// 解锁
InterlockedExchange((LONG*)&g_bIsUse, FALSE);
return 0;
}
UINT WINAPI ThreadRun2(LPVOID lParam)
{
// 上锁
while (InterlockedExchange((LONG*)&g_bIsUse, TRUE) == TRUE)
Sleep(0);
// 上锁完成, 进行操作
// 这里的代码是绝对线程安全的
// 解锁
InterlockedExchange((LONG*)&g_bIsUse, FALSE);
return 0;
}
int main()
{
_tsetlocale(LC_ALL, TEXT(""));
HANDLE hThreads[2];
hThreads[0] = (HANDLE)_beginthreadex(NULL, 0, ThreadRun1, NULL, 0, NULL);
hThreads[1] = (HANDLE)_beginthreadex(NULL, 0, ThreadRun2, NULL, 0, NULL);
WaitForMultipleObjects(sizeof(hThreads) / sizeof(HANDLE), hThreads, TRUE, INFINITE);
for (size_t i = 0; i < sizeof(hThreads) / sizeof(HANDLE); ++i)
{
CloseHandle(hThreads[i]);
}
return 0;
}旋转锁的缺点
使用旋转锁, 会使饥饿线程得不到调度
- 线程饥饿是
线程优先级造成的 - 可能会使
饥饿线程一直饥饿下去
使用旋转锁, 也会导致CPU使用率过高
未完待续...
如有错误,请提出指正!谢谢.
本文由 花心胡萝卜 创作,采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: 2017-06-24 at 06:17 am