PoEdu培训 Windows班 第二十九课 Windows 线程(十) volatile关键字, 线程参数和线程锁
文章类别: 培训笔记 0 评论

PoEdu培训 Windows班 第二十九课 Windows 线程(十) volatile关键字, 线程参数和线程锁

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

Windows 线程(十) volatile关键字, 线程参数和线程锁

volatile

我们都知道, 编译器在编译的时候, 会进行代码的优化
当我们在Release版本下的时候, 优化的更为厉害
像如下代码:

BOOL bFlag = FALSE;

if (!bFlag)
{
    bFlag = TRUE;
    printf(".....\n");
    bFlag = FALSE;
}

在Release版本下, 编译器极有可能就优化到只剩下 printf 这一句代码
为了防止这种优化的发生, 我们需要在变量前面加上volatile关键字
修改如下:

volatile BOOL bFlag = FALSE;

if (!bFlag)
{
    bFlag = TRUE;
    printf(".....\n");
    bFlag = FALSE;
}

它的作用就是告诉编译器, 不要对我的变量进行优化
当我使用这个变量的值的时候, 都要去内存中取值

问题线程分析

我们有如下程序, 代码:

#include <Windows.h>
#include <tchar.h>
#include <process.h>

INT g_nNum = 0;
CONST INT LOOPCOUNT = 1000;
CONST INT THREADCOUNT = 1000;

UINT WINAPI ThreadFun(LPVOID lParam)
{
    INT nThreadNo = (INT)lParam;
    g_nNum = 0;
    for (INT i = 0; i < LOOPCOUNT; ++i)
    {
        g_nNum += i;
    }
    _tprintf(TEXT("Thread [%4d] : g_nNUm = [%d]\n"), nThreadNo, g_nNum);
    return 0;
}

INT main()
{
    HANDLE hThreads[THREADCOUNT] = {0};
    for (INT i = 0; i < THREADCOUNT; ++i)
    {
        hThreads[i] = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, (LPVOID)i, 0, NULL);
    }

    WaitForMultipleObjects(THREADCOUNT, hThreads, TRUE, INFINITE);
    for (INT i = 0; i < THREADCOUNT; ++i)
    {
        CloseHandle(hThreads[i]);
    }
    return 0;
}

线程函数的lParam

线程参数lParam是一个LPVOID类型, 也就是一个void*
那么我们在例子中进行传递的时候, 是直接将一个int强制转换成 void* 后进行传递
那么按照我们的想法, void* 是一个指针, 那么我们修改代码如下:

// 只写改动的地方
UINT WINAPI ThreadFun(LPVOID lParam)
{
    INT* nThreadNo = (INT*)lParam;
    // ...
    _tprintf(TEXT("Thread [%4d] : g_nNUm = [%d]\n"), *nThreadNo, g_nNum);
    return 0;
}

INT main()
{
    HANDLE hThreads[THREADCOUNT] = {0};
    for (INT i = 0; i < THREADCOUNT; ++i)
    {
        hThreads[i] = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, &i, 0, NULL);
    }
    // ...
}

那么经过修改后, 我们执行一下, 会发现我们的nThread混乱了...
Alt 结果1
这是为什么呢?
那么, 我们主线程在进行for循环的时候, 变量i的值是一直在变的
那么我们线程的lParam是指向主线程i变量的地址
那么我们去取i的值的时候, 因为i是变化的, 所以我们的nThread混乱了...
那么我们在进行修改:

// 只写改动的地方
UINT WINAPI ThreadFun(LPVOID lParam)
{
    INT nThreadNo = *((INT*)lParam);
    // ...
    _tprintf(TEXT("Thread [%4d] : g_nNUm = [%d]\n"), *nThreadNo, g_nNum);
    return 0;
}

经过这样修改后, 结果还是混乱的
Alt 结果2
因为我们的线程不是顺序执行的, 是经过CPU调度的
而线程的执行顺序是不固定的
所以这样修改也是错误的

那么, 我们其实需要的, 就是一个值, 主线程中i的值
那么我们直接将这个值传递过去就可以了
void* 在这里就是指的任意类型的变量, 而不是指针
线程的执行顺序是没有顺序的, 我们只能人工干预它, 来达到顺序执行的目的

例子的执行结果:
Alt 结果3

原子操作和旋转锁

那么, 在解决了nThreadNo的问题之后, 我们在来看 g_nNum 的值
明显的, g_nNum 的值不能保证每次都是正确的
首先, 我们要避免编译器的优化, 给 g_nNum 加上 volatile 关键字
其次, 我们上节课学过的, g_nNum不是线程安全的, 需要进行原子操作
那么, 我们经过修改的代码如下:

#include <Windows.h>
#include <tchar.h>
#include <process.h>

volatile INT g_nNum = 0;
CONST INT LOOPCOUNT = 1000;
CONST INT THREADCOUNT = 1000;

UINT WINAPI ThreadFun(LPVOID lParam)
{
    INT nThreadNo = (INT)lParam;
    g_nNum = 0;
    for (INT i = 0; i < LOOPCOUNT; ++i)
    {
        // g_nNum += i;
        InterlockedExchangeAdd((LONG*)&g_nNum, i);
    }
    _tprintf(TEXT("Thread [%4d] : g_nNUm = [%d]\n"), nThreadNo, g_nNum);
    return 0;
}
// ...

通过运行我们发现, g_nNum出错的几率更加高了...
Alt 结果4
这是为什么呢?
我们还是从反汇编的角度看一看
Alt 结果5
我们可以看到, 一条新的指令 lock xadd
这条指令是原子操作的加
但是, 它仅仅只保证了在加的时候是原子操作, 而不是整个for循环过程都是原子操作

所以我们在进行改写代码:

#include <Windows.h>
#include <tchar.h>
#include <process.h>

volatile INT g_nNum = 0;
volatile BOOL g_bIsUse = FALSE;

CONST INT LOOPCOUNT = 1000;
CONST INT THREADCOUNT = 1000;

UINT WINAPI ThreadFun(LPVOID lParam)
{
    INT nThreadNo = (INT)lParam;
    // 等待到上锁成功
    while (InterlockedExchange((LONG*)&g_bIsUse, TRUE) == TRUE)
        Sleep(0);

    g_nNum = 0;
    for (INT i = 0; i < LOOPCOUNT; ++i)
    {
        g_nNum += i;
        //InterlockedExchangeAdd((LONG*)&g_nNum, i);
    }
    _tprintf(TEXT("Thread [%4d] : g_nNUm = [%d]\n"), nThreadNo, g_nNum);
    InterlockedExchange((LONG*)&g_bIsUse, FALSE);
    return 0;
}

INT main()
{
    HANDLE hThreads[THREADCOUNT] = { 0 };
    for (INT i = 0; i < THREADCOUNT; ++i)
    {
        hThreads[i] = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, (LPVOID)i, 0, NULL);
    }

    WaitForMultipleObjects(THREADCOUNT, hThreads, TRUE, INFINITE);
    for (INT i = 0; i < THREADCOUNT; ++i)
    {
        CloseHandle(hThreads[i]);
    }
    return 0;
}

经过如上修改, 我们的代码就运行正确了

InterlockedExchange

这个函数需要注意的是, 它的返回值是变量修改之前的值
所以在写旋转锁的时候, 要注意函数的返回值

多线程调试技巧

比如说, 我们旋转锁代码写错了
但是我们并没有意识到我们写错了
那么要确保我们加锁成功, 我们可以采用printf的方式
在我们的加锁的代码段的开始写上一句printf
查看输出结果就能很清晰明白的看清楚我们的锁是否正确了

未完待续...

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

回复