因为之前一直习惯于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
,从而避免全局变量锁,文中代码仅仅示意对于全局变量访问的全局锁作用。