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混乱了... 
这是为什么呢?
那么, 我们主线程在进行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;
}经过这样修改后, 结果还是混乱的 
因为我们的线程不是顺序执行的, 是经过CPU调度的
而线程的执行顺序是不固定的
所以这样修改也是错误的
那么, 我们其实需要的, 就是一个值, 主线程中i的值
那么我们直接将这个值传递过去就可以了
void* 在这里就是指的任意类型的变量, 而不是指针
线程的执行顺序是没有顺序的, 我们只能人工干预它, 来达到顺序执行的目的
例子的执行结果: 
原子操作和旋转锁
那么, 在解决了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出错的几率更加高了... 
这是为什么呢?
我们还是从反汇编的角度看一看 
我们可以看到, 一条新的指令 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
查看输出结果就能很清晰明白的看清楚我们的锁是否正确了
未完待续...
如有错误,请提出指正!谢谢.
本文由 花心胡萝卜 创作,采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: 2017-06-24 at 06:45 am