C/C++全局变量多线程并发访问临界锁(Critical Section Lock)
提醒:本页面将不再更新、维护或者支持,文章、评论所叙述内容存在时效性,涉及技术细节或者软件使用方面不保证能够完全有效可操作,请谨慎参考!
因为之前一直习惯于C语言编程,对C++的一些使用方式没有在意过,在阅读一些代码后渐渐的发现了C++的一些便捷性,这种便捷性在一定程度上降低了我们的编码量,对于全局变量多线程并发访问一般可以通过临界区(Critical Section)实现,比如在C语言中形如以下代码:
#include <windows.h>
#define THREAD_NUM 5
CRITICAL_SECTION g_cs;
int g_nResource;
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
EnterCriticalSection(&g_cs);
printf("Thread id %d Resource Count %d\n",
GetCurrentThreadId(), g_nResource);
g_nResource++;
LeaveCriticalSection(&g_cs);
return 0;
}
int main(void)
{
HANDLE hThreads[THREAD_NUM] = {0};
InitializeCriticalSection(&g_cs);
for (i = 0; i < THREAD_NUM; i++)
hThreads[i] = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
WaitForMultipleObjects(THREAD_NUM, hThreads, TRUE, INFINITE);
DeleteCriticalSection(&g_cs);
return 0;
}
这段代码是在C语言下一种常见的编码习惯,但是多处调用
EnterCriticalSection
或者
LeaveCriticalSection
也会让人烦不胜烦,又比如形如以下的代码也是经常会出现的错误:
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
EnterCriticalSection(&g_cs);
// 检查传入的lpParam是否为NULL
if (lpParam == NULL)
return ERROR_INVALID_PARAMETER; // 这里错误,没有释放临界区就直接返回
printf("Thread id %d Resource Count %d\n",
GetCurrentThreadId(), g_nResource);
g_nResource++;
LeaveCriticalSection(&g_cs);
return 0;
}
这一类错误很容易在构建复杂应用时发生,而且调试锁死的线程也相对比较困难,当然对于C语言来说除了在返回(return)前记得释放锁外还有一种比较优雅的办法就是善用goto,比如以下:
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
EnterCriticalSection(&g_cs);
// 检查传入的lpParam是否为NULL
if (lpParam == NULL)
goto CleanUp;
printf("Thread id %d Resource Count %d\n",
GetCurrentThreadId(), g_nResource);
g_nResource++;
CleanUp;
LeaveCriticalSection(&g_cs);
return 0;
}
在函数体最后释放资源前加上一个标签(label),然后判断失败则跳转到这个label,这样可以避免之前忘记释放锁的错误。
好了言归正传,今天介绍一种利用C++对象构造和析构机制的全局锁,首先我们建立一个全局锁的类。
class CGlobalLock // tag: glock
{
public:
CGlobalLock()
{
EnterCriticalSection(&s_cs);
}
~CGlobalLock()
{
LeaveCriticalSection(&s_cs);
}
static void Init()
{
InitializeCriticalSection(&s_cs);
}
static void Deinit()
{
DeleteCriticalSection(&s_cs);
}
private:
static CRITICAL_SECTION s_cs;
};
怎么样,这段代码是不是很简单,接下来我们结合上面的例子来讲一讲具体的用法:
#include <windows.h>
#include "globallock.h" // 包含class CGlobalLock
#define THREAD_NUM 5
int g_nResource;
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
CGlobalLock glock;
printf("Thread id %d Resource Count %d\n",
GetCurrentThreadId(), g_nResource);
g_nResource++;
return 0;
}
int main(void)
{
HANDLE hThreads[THREAD_NUM] = {0};
CGlobalLock::Init();
for (i = 0; i < THREAD_NUM; i++)
hThreads[i] = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
WaitForMultipleObjects(THREAD_NUM, hThreads, TRUE, INFINITE);
CGlobalLock::Deinit();
return 0;
}
代码就是这样,可能Init和Deinit还好理解,而线程体
ThreadProc
单单初始化一个名称为glock的
CGlobalLock
的对象而不做其他事情就可以实现加锁和解锁?
是的,这里利用了构造函数、析构函数和变量作用域的概念,首先建立glock时必定会调用构造函数,而构造函数则使用
EnterCriticalSection
进入临界区,从而实现加锁,等到线程体执行完毕时必然会使glock超出变量作用域,这样C++需要对glock进行销毁,就必然触发析构函数,而析构函数使用
LeaveCriticalSection
释放了锁,也就是说这里glock的作用域是
ThreadProc
的函数体,只要执行流程超出
ThreadProc
函数体则必然调用析构函数释放锁,所以定义这个对象即是加锁,而解锁则交给作用域无需我们操心了。这样我们就可以放心大胆的if return了。
有点类似于try finally,无论try里发生了什么finally里面的代码无论如何都是会被执行的。
注:对于单纯的整形自增可以直接使用原子操作
InterLockedIncrement
,从而避免全局变量锁,文中代码仅仅示意对于全局变量访问的全局锁作用。