第8章 用户模式下的线程同步(2)_临界区(CRITICAL_SECTION)

8.4 关键段(临界区)——内部也是使用Interlocked函数来实现的!

8.4.1 关键段的细节

(1)CRITICAL_SECTION的使用方法

第8章 用户模式下的线程同步(2)_临界区(CRITICAL_SECTION)

  ①CRITICAL_SECTION cs;            //声明为全局变量(也可是成员变量,甚至局部变量)

  ②InitializeCriticalSection(&cs);     //初始化临界区,注意cs是临界区对象,不能被移动和复制

  ③EnterCriticalSection(&cs);        //进入或等待临界区(任何要访问共享资源的代码都须包含应在

                                                    //Enter和Leave之间,如果忘了哪怕一个地方,都可能破坏资源)

  ④LeaveCriticalSection(&cs);       //离开临界区

  ⑤DeleteCriticalSection(&cs);      //不再需要临界区对象。

(2)CRITICAL_SECTION数据结构

第8章 用户模式下的线程同步(2)_临界区(CRITICAL_SECTION)

  ①LockCount字段:最重要的字段,初始化为-1。该字段在XP和Vista以后版本含义有所不同,在Vista以后版本中。

    A、最低位——0表示临界区被锁,1表示没被锁。-->0x1 & LockCount

    B、第2位(低位数起):1表示没有线程被唤醒。0表示一个线程被唤醒 -->(0x2 & lockCount)>>1;

      C、其余各位表示等待锁的线程数量(-1-lockCount)>>2

  ②RecursionCount:表示拥有者线程己经获得该临界区的次数,初始值为0。当拥有者线程每调用EnterCriticalSection时会递增1。也就是说只有拥有者调用EnterCriticalSection时RecursionCount才递增。但为了防止拥有者线程一直霸占临界区,系统允许其他线程调用LeaveCriticalSection使该值递减。但不管是拥有者线程还是其他线程,调用Leave的总次数应该等于Enter的次数,才能被解开锁,当RecursionCount<=0时,锁就解开,OwningThread被设为0。

  ③OwningThread:此字段表示当前占用该临界区的线程ID

  ④LockSemaphore:此字段命名不恰当,它实际上是一个事件对象,用于通知操作系统该临界区已被释放,等待该临界区的线程之一现在可以获得该临界区并继续执行。因为系统是在临界区阻止另一个线程时才自动分配事件句柄,所以如果在不再需要临界区时要将其删除,以防止LockSemaphore 字段可能会导致内存泄漏。

(3)EnterCriticalSection函数的执行过程

  ①如果没有线程正在访问资源,那么EnterCritical会更新临界区成员变量,以表示调用线程己经获得临界区锁,并立即返回,如此调用线程继续执行。

  ②当调用线程获得临界区锁后,如果此时调用线程再次Enter,则会更新RecursionCount,以表示调用线程被获准访问的次数。并立即返回。

  ③当调用线程获得临界区锁后,如果此时是其他线程要进入临界区,则EnterCriticalSection会使用一个事件内核对象(lockSemaphore)将这个线程切换到等待状态。(注意,这些等待的线程会事件内核对象记录下来,以表示正在等待该内核对象的都有哪些线程,当然线程本身也会记录,他在等哪些内核对象)(注意:临界区本身不是内核对象!

(4)LeaveCriticalSection函数:会更新CRITICAL_SECTION的成员变量,如果此时仍有线程处于等待状态,那么该函数会将其中之一的等待线程换回可调度状态

(5)TryEnterCriticalSection函数

  ①该函数不会让调用线程进入等待状态,它通过返回值来表示是否获准访问资源。TRUE表示获准,FALSE表示其他线程正在使用资源,申请被拒绝。

  ②如果返回TRUE时,说明该调用线程己经正在访问资源,CRITICAL_SECTION 成员变量被更新过。所以每个返回TRUE的TryEnterCriticalSection都须调用LeaveCriticalSection

【CriticalsectionInfo程序】

第8章 用户模式下的线程同步(2)_临界区(CRITICAL_SECTION)

#include <windows.h>
#include <malloc.h>
#include <tchar.h>
#include <locale.h>

#define THREADNUM   3

CRITICAL_SECTION g_cs;
HANDLE* hThread = NULL;
/*
临界区中LockCount和RecursionCount字段的含义
1、XP和Windows2000下
(1)LockCount:
    ①初始为-1,每调用EnterCriticalSection时LockCount加1,调用LeaveCriticalSection减1
    ②如LockCount = 5 表示某一线程正在使用临界区,此外还有5个线程正在等待锁
(2)RecursionCount:调用线程多次调用EnterCriticalSection的次数
(3)EntryCount:除了调用线程以外的其他线程调用EnterCriticalSection的次数。
(4)当第1次调用EnterCriticalSection后,LockCount、RecursionCount、EntryCount、ContentionCount各加1,
     OwningThread设为调用线程的ID。
    A、当拥有者再次调用EnterCriticalSection:LockCount++,Recursion++、EntryCount不变
    B、当其他线程调用EnterCriticalSection:LockCount++、EntryCount++、Recursion不变
    C、当拥有者调用LeaveCriticalSection:LockCount减1(到-1)、Recursion减到0,OwningThread设为0.
    D、其他线程调用LeaveCriticalSection,与拥有者调用LeaveCriticalSection变化一样。
2、Windows2003sp1及以后
(1)LockCount:
    A、最低位——0表示临界区被锁,1表示没被锁。-->0x1 & LockCount
    B、第2位(低位数起):1表示没有线程被唤醒。0表示一个线程被唤醒 -->(0x2 & lockCount)>>1;
    C、其余各位表示等待锁的线程数量(-1-lockCount)>>2
*/
//////////////////////////////////////////////////////////////////////////
void ShowCriticalSectionInfo(PCRITICAL_SECTION pcs)
{
    _tprintf(_T("Thread[%d],CriticalSection Information:n"),GetCurrentThreadId());
    _tprintf(_T("---------------------------n"));
    _tprintf(_T("LockCount:%d(临界区被锁:%s;是否有线程唤醒:%s;等待锁的线程数量:%d)n"),
                          pcs->LockCount,
                          (0x1 & pcs->LockCount) ? _T("") : _T(""), 
                          ((0x2 & pcs->LockCount)) >> 1 ? _T("") : _T(""),
                          ((-1 - pcs->LockCount) >> 2));

    _tprintf(_T("RecursionCount:%dn"), pcs->RecursionCount);
    _tprintf(_T("OwningThread:%dn"), pcs->OwningThread);
    _tprintf(_T("LockSemephore:0x%08Xn"), pcs->LockSemaphore);
    //_tprintf(_T("SpinCount:0x%08Xn"), pcs->SpinCount);
}

//////////////////////////////////////////////////////////////////////////
//线程函数1
DWORD WINAPI ThreadProc1(PVOID pParam)
{
    //1、演示First进程两次进入临界区。
    _tprintf(_T("第1个子线程[%d]两次进入临界区n"),GetCurrentThreadId());
    EnterCriticalSection(&g_cs);
    EnterCriticalSection(&g_cs);
    ShowCriticalSectionInfo(&g_cs);

    return 0;
}

//线程函数2
DWORD WINAPI ThreadProc2(PVOID pParam)
{
    int nIndex = (int)pParam;
    //第nIndex个子程线进入临界区,并拿到锁
    _tprintf(_T("n第%d个子线程[%d]尝试进入临界区n"), nIndex,GetCurrentThreadId());
    EnterCriticalSection(&g_cs);
    ShowCriticalSectionInfo(&g_cs);

    _tprintf(_T("n第%d个子线程己经进入临界区......n"), nIndex, GetCurrentThreadId());

    _tprintf(_T("n第%d个子线程线程[%d]离开临界区n"), nIndex, GetCurrentThreadId());
    LeaveCriticalSection(&g_cs);
    ShowCriticalSectionInfo(&g_cs);
    return 0;
}

int _tmain()
{
    _tsetlocale(LC_ALL, _T("chs"));

    InitializeCriticalSection(&g_cs);//初始化临界区时的状态:
    
    hThread = (HANDLE*)malloc(sizeof(HANDLE)*THREADNUM);
    hThread[0] = CreateThread(NULL, 0, ThreadProc1, NULL, CREATE_SUSPENDED, NULL);
    hThread[1] = CreateThread(NULL, 0, ThreadProc2, (LPVOID)2, CREATE_SUSPENDED, NULL);
    hThread[2] = CreateThread(NULL, 0, ThreadProc2, (LPVOID)3, CREATE_SUSPENDED, NULL);
    ResumeThread(hThread[0]);
    WaitForSingleObject(hThread[0], INFINITE);
    _tsystem(_T("PAUSE"));

    //演示在子线程拥有临界区,但在其他线程(主线程)释放(因子线程两次Enter,主线程要两次Leave)
    _tprintf(_T("n主线程[%d]解开临界区锁n"), GetCurrentThreadId());
    LeaveCriticalSection(&g_cs);
    LeaveCriticalSection(&g_cs);
    ShowCriticalSectionInfo(&g_cs);
    _tsystem(_T("PAUSE"));

    //第2个子线程启动,进入尝试进入临界区(应该可行,因为锁被主线程释放)
    ResumeThread(hThread[1]);
    WaitForSingleObject(hThread[1], INFINITE);
    _tsystem(_T("PAUSE"));

    //主线程锁一下临界区,并启动第3个线程
    _tprintf(_T("n主线程[%d]锁定临界区n"), GetCurrentThreadId());
    EnterCriticalSection(&g_cs);
    ResumeThread(hThread[2]); //此时第3个线程进入等待状态
    Sleep(1000);
    ShowCriticalSectionInfo(&g_cs);

    //主线程解开临界区锁,并恢复第3个线程
    _tprintf(_T("n主线程[%d]解开临界区锁n"), GetCurrentThreadId());
    LeaveCriticalSection(&g_cs);
    Sleep(1000);

    _tsystem(_T("PAUSE"));

    WaitForMultipleObjects(THREADNUM, hThread, TRUE, INFINITE);
    for (int i = 0; i < THREADNUM;i++){
        CloseHandle(hThread[i]);
    }
    free(hThread);
    DeleteCriticalSection(&g_cs);

    return 0;
}

 

8.4.2 关键段和旋转锁

(1)当一个线程试图进入关键段,但这个关键段正被另一个线程占用时,函数会立即把调用线程切换到等待状态。这意味着线程必须从用户模式切换到内核模式,CPU开销比较大。

(2)但往往当前占用资源的线程可能很快就结束对资源的访问,事实上,在需要等待的线程完成切换到内核模式之前,占用资源的线程可以己经释放了资源,这无法浪费大量CPU时间。

(3)为了提高关键段的性能,可加入合并旋转锁到关键段中。当调用EnterCriticalSection时,先尝试旋转方式的访问资源。只有尝试一个后仍失败。才切换到内核模式并进入等待状态。

(4)要使用具有旋转方式的关键段,必须调用以下函数来初始化关键段 InitializeCriticalSectionAndSpinCount(pcs,dwSpinCount);其中 dwSpinCount为旋转次数(如4000)。如果在单CPU机器上,系统会忽图dwSpinCount参数。

(5)改变关键段的旋转次数SetCriticalSectionSpinCount(pcs,dwSpinCount);

8.4.3 关键段和错误处理

(1)InitializeCriticalSection返回值为VOID,这是Microsoft设计时考虑不周。实际上该函数调用仍可能失败,如给关键分配内存时,当失败时将抛出STATUS_NO_MEMORY异常。

(2)InitializeCriticalSectionAndSpinCount失败时将返回FALSE

(3)关键段内部使用的事件内核对象只有在两个(或多个)线程在同一时刻争夺同一关键段时才会创建它。这样做是为了节省系统资源。只有在DeleteCriticalSection后,该内核对象才会被释放(因此,用完关键段后,不要忘了调用DeleteCriticalSection函数)

(4)在EnterCriticalSection函数中,仍有发生潜在的异常,如创建事件内核对象时,可能会抛出EXCEPTION_INVALID_HANDLE异常。要解决这个问题有两种方法,其一是用结构化异常处理来捕获错误。还有一种是选择InitializeCriticalSectionAndSpinCount来创建关键段,传将dwSpinCount最高位设为1,即告诉系统初始化关键段时就创建一个相关联的事件内核对象,如果无法创建该函数返回FALSE。

(5)注意死锁

  用临界区资源使多线程同步时候要特别注意线程死锁问题,假设程序有两临界资源(g_csA、g_csB)与两个子线程(子线程 A、子线程 B),子线程执行体流程如下图(图1)表示,当子线程 A 先获得临界资源 g_csA 后由于子线程 A 的时间片用完了,所以跳到子线程 B 进行执行,这时 B 将获得临界资源 g_csB,然后由于 A 获得临界资源 g_csA,所以 B 只好等待直至子线程B时间片用完,然后跳到子线程 A 继续执行,但是这时的临界资源 g_csB 已经被子线程 B占有,所以子线程 A 有进行等待直至时间片用完。于是子线程A与子线程B就进入了死锁现象流程如下图所示(图2)。

第8章 用户模式下的线程同步(2)_临界区(CRITICAL_SECTION)

 【CriticalSection程序】——模拟多线程对同一内存进行的各种操作(分配、释放、读、写)

第8章 用户模式下的线程同步(2)_临界区(CRITICAL_SECTION)

#include <windows.h>
#include <tchar.h>
#include <locale.h>
#include <time.h>

//////////////////////////////////////////////////////////////////////////
CRITICAL_SECTION  g_cs = { 0 };
DWORD*  g_pdwAnyData = NULL;
//模拟对这个全局的数据指针进行分配、写入、释放等操作
void AllocData();
void WriteData();
void ReadData();
void FreeData();

//////////////////////////////////////////////////////////////////////////
DWORD WINAPI ThreadProc(PVOID pvParam);
#define THREADCOUNT  20
#define THREADACTCNT  20   //每个线程执行10次动作

int _tmain()
{
    _tsetlocale(LC_ALL, _T("chs"));

    srand((unsigned int)time(NULL));

    //初始化旋转式的临界区(旋转次数1024次,同时临界区内部创建事件内核对象)
    if (!InitializeCriticalSectionAndSpinCount(&g_cs, 0x80000400))
        return 0;

    HANDLE aThread[THREADCOUNT];
    DWORD  dwThreadID = 0;
    SYSTEM_INFO si = { 0 };
    GetSystemInfo(&si);

    for (int i = 0; i < THREADCOUNT;i++){
        aThread[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, 
                                  NULL,CREATE_SUSPENDED,&dwThreadID);

        //设置线程的亲缘性
        SetThreadAffinityMask(aThread[i], 1 << (i % si.dwNumberOfProcessors));
        ResumeThread(aThread[i]);
    }

    WaitForMultipleObjects(THREADCOUNT, aThread, TRUE, INFINITE);

    for (int i = 0; i < THREADCOUNT;i++){
        CloseHandle(aThread[i]);
    }

    DeleteCriticalSection(&g_cs);  //不要忘了删除临界区对象
    if (NULL != g_pdwAnyData){
        free(g_pdwAnyData);
        g_pdwAnyData = NULL;
    }

    return 0;
}

//线程函数
DWORD WINAPI ThreadProc(PVOID pvParam)
{
    //为了增加冲突的可能性,让每个线程执行时,
    //会随机分配20项任务(如分配内存、读取、释放等)
    int iAct = rand() % 4;
    int iActCnt = THREADACTCNT;

    while (iActCnt--){
        switch (iAct)
        {
        case 0:AllocData();break;
        case 1:FreeData(); break;
        case 2:WriteData(); break;
        case 3:ReadData(); break;
        }

        iAct = rand() % 4;
    }
    return  0;
}

void AllocData()
{
    EnterCriticalSection(&g_cs);
    __try{
        _tprintf(_T("线程[ID:0x%X] Alloc Datant"), GetCurrentThreadId());
        if (NULL == g_pdwAnyData){
            g_pdwAnyData = (DWORD*)malloc(sizeof(DWORD));
            _tprintf(_T("g_pdwAnyData为NULL,现己分配内存(Address:0x%08X)n"), 
                         g_pdwAnyData);
        } else{
            _tprintf(_T("g_pdwAnyData不为NULL,无法分配内存(Address:0x%08X)n"),
                     g_pdwAnyData);
        }
    }
    __finally{
        LeaveCriticalSection(&g_cs);
    }
}

void WriteData()
{
    EnterCriticalSection(&g_cs);
    __try{
        _tprintf(_T("线程[ID:0x%X] Write Datant"), GetCurrentThreadId());
        if (NULL != g_pdwAnyData){
            *g_pdwAnyData = rand();
            _tprintf(_T("g_pdwAnyData不为空,写入数据(%u)n"),
                     *g_pdwAnyData);
        } else{
            _tprintf(_T("g_pdwAnyData为NULL,不能写入数据!n"));
        }
    }
    __finally{
        LeaveCriticalSection(&g_cs);
    }
}

void ReadData()
{
    EnterCriticalSection(&g_cs);
    __try{
        _tprintf(_T("线程[ID:0x%X] Read Datant"), GetCurrentThreadId());
        if (NULL != g_pdwAnyData){
            _tprintf(_T("g_pdwAnyData不为空,读取数据(%u)n"),
                     *g_pdwAnyData);
        } else{
            _tprintf(_T("g_pdwAnyData为NULL,不能读取数据!n"));
        }
    }
    __finally{
        LeaveCriticalSection(&g_cs);
    }
}

void FreeData()
{
    EnterCriticalSection(&g_cs);
    __try{
        _tprintf(_T("线程[ID:0x%X] Free Datant"), GetCurrentThreadId());
        if (NULL != g_pdwAnyData){
            _tprintf(_T("g_pdwAnyData不为NULL,释放掉!n"));
            free(g_pdwAnyData);
            g_pdwAnyData = NULL;
        } else{
            _tprintf(_T("g_pdwAnyData为NULL,不能释放!n"));
        }
    }
    __finally{
        LeaveCriticalSection(&g_cs);
    }
}

 

原文链接: https://www.cnblogs.com/5iedu/p/4727152.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    第8章 用户模式下的线程同步(2)_临界区(CRITICAL_SECTION)

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/394671

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月3日 下午3:52
下一篇 2023年4月3日 下午3:52

相关推荐