PoEdu培训 Windows班 第二十八课 Windows 线程(九) 线程的原子操作和旋转锁
文章类别: 培训笔记 0 评论

PoEdu培训 Windows班 第二十八课 Windows 线程(九) 线程的原子操作和旋转锁

文章类别: 培训笔记 0 评论

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的结果, 如图
Alt 结果
这是为什么呢?

我们从反汇编的角度看一看
下面是反汇编代码

     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使用率过高

未完待续...

如有错误,请提出指正!谢谢.

回复