提醒:本页面将不再更新、维护或者支持,文章、评论所叙述内容存在时效性,涉及技术细节或者软件使用方面不保证能够完全有效可操作,请谨慎参考!

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