zoukankan      html  css  js  c++  java
  • 第8章 用户模式下的线程同步(2)_临界区(CRITICAL_SECTION)

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

    8.4.1 关键段的细节

    (1)CRITICAL_SECTION的使用方法

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

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

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

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

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

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

    (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程序】

    #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:
    "),GetCurrentThreadId());
        _tprintf(_T("---------------------------
    "));
        _tprintf(_T("LockCount:%d(临界区被锁:%s;是否有线程唤醒:%s;等待锁的线程数量:%d)
    "),
                              pcs->LockCount,
                              (0x1 & pcs->LockCount) ? _T("") : _T(""), 
                              ((0x2 & pcs->LockCount)) >> 1 ? _T("") : _T(""),
                              ((-1 - pcs->LockCount) >> 2));
    
        _tprintf(_T("RecursionCount:%d
    "), pcs->RecursionCount);
        _tprintf(_T("OwningThread:%d
    "), pcs->OwningThread);
        _tprintf(_T("LockSemephore:0x%08X
    "), pcs->LockSemaphore);
        //_tprintf(_T("SpinCount:0x%08X
    "), pcs->SpinCount);
    }
    
    //////////////////////////////////////////////////////////////////////////
    //线程函数1
    DWORD WINAPI ThreadProc1(PVOID pParam)
    {
        //1、演示First进程两次进入临界区。
        _tprintf(_T("第1个子线程[%d]两次进入临界区
    "),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("
    第%d个子线程[%d]尝试进入临界区
    "), nIndex,GetCurrentThreadId());
        EnterCriticalSection(&g_cs);
        ShowCriticalSectionInfo(&g_cs);
    
        _tprintf(_T("
    第%d个子线程己经进入临界区......
    "), nIndex, GetCurrentThreadId());
    
        _tprintf(_T("
    第%d个子线程线程[%d]离开临界区
    "), 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("
    主线程[%d]解开临界区锁
    "), 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("
    主线程[%d]锁定临界区
    "), GetCurrentThreadId());
        EnterCriticalSection(&g_cs);
        ResumeThread(hThread[2]); //此时第3个线程进入等待状态
        Sleep(1000);
        ShowCriticalSectionInfo(&g_cs);
    
        //主线程解开临界区锁,并恢复第3个线程
        _tprintf(_T("
    主线程[%d]解开临界区锁
    "), 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)。

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

    #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 Data
    	"), GetCurrentThreadId());
            if (NULL == g_pdwAnyData){
                g_pdwAnyData = (DWORD*)malloc(sizeof(DWORD));
                _tprintf(_T("g_pdwAnyData为NULL,现己分配内存(Address:0x%08X)
    "), 
                             g_pdwAnyData);
            } else{
                _tprintf(_T("g_pdwAnyData不为NULL,无法分配内存(Address:0x%08X)
    "),
                         g_pdwAnyData);
            }
        }
        __finally{
            LeaveCriticalSection(&g_cs);
        }
    }
    
    void WriteData()
    {
        EnterCriticalSection(&g_cs);
        __try{
            _tprintf(_T("线程[ID:0x%X] Write Data
    	"), GetCurrentThreadId());
            if (NULL != g_pdwAnyData){
                *g_pdwAnyData = rand();
                _tprintf(_T("g_pdwAnyData不为空,写入数据(%u)
    "),
                         *g_pdwAnyData);
            } else{
                _tprintf(_T("g_pdwAnyData为NULL,不能写入数据!
    "));
            }
        }
        __finally{
            LeaveCriticalSection(&g_cs);
        }
    }
    
    void ReadData()
    {
        EnterCriticalSection(&g_cs);
        __try{
            _tprintf(_T("线程[ID:0x%X] Read Data
    	"), GetCurrentThreadId());
            if (NULL != g_pdwAnyData){
                _tprintf(_T("g_pdwAnyData不为空,读取数据(%u)
    "),
                         *g_pdwAnyData);
            } else{
                _tprintf(_T("g_pdwAnyData为NULL,不能读取数据!
    "));
            }
        }
        __finally{
            LeaveCriticalSection(&g_cs);
        }
    }
    
    void FreeData()
    {
        EnterCriticalSection(&g_cs);
        __try{
            _tprintf(_T("线程[ID:0x%X] Free Data
    	"), GetCurrentThreadId());
            if (NULL != g_pdwAnyData){
                _tprintf(_T("g_pdwAnyData不为NULL,释放掉!
    "));
                free(g_pdwAnyData);
                g_pdwAnyData = NULL;
            } else{
                _tprintf(_T("g_pdwAnyData为NULL,不能释放!
    "));
            }
        }
        __finally{
            LeaveCriticalSection(&g_cs);
        }
    }
  • 相关阅读:
    删DS.Store
    switch 多重选择
    PrintWrite写入文件
    读取文件
    notepad++如何把文件保存为java文件
    让notepad++成为轻量级JAVA的IDE
    Jenkins构建Python项目提示:'python' 不是内部或外部命令,也不是可运行的程序
    相关服务账号
    Jenkins安装与启动
    jmeter安装
  • 原文地址:https://www.cnblogs.com/5iedu/p/4727152.html
Copyright © 2011-2022 走看看