C/C++全局变量多线程并发访问临界锁(Critical Section Lock)

!本文可能 超过1年没有更新,今后内容也许不会被维护或者支持,部分内容可能具有时效性,涉及技术细节或者软件使用方面,本人不保证相应的兼容和可操作性。

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

若无特别说明,本网站文章均为原创,原则上这些文章不允许转载,但是如果阁下是出于研究学习目的可以转载到阁下的个人博客或者主页,转载遵循创作共同性“署名-非商业性使用-相同方式共享”原则,请转载时注明作者出处谢绝商业性、非署名、采集站、垃圾站或者纯粹为了流量的转载。谢谢合作!
请稍后...

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*