关键段 死锁 读写锁 事件 互斥量 信号量

本文介绍了关键段的概念及其与读写锁的区别,强调了线程间的互斥与同步问题。关键段需配对使用Enter和Leave,否则可能导致死锁。读写锁允许多个读线程并发访问,而写锁则确保同一时间只有一个线程持有。事件作为内核对象,可用于跨线程/进程同步。互斥量与关键段类似,但可跨进程使用,遗弃状态下系统会自动调整。信号量作为计数器,用于控制线程数量,避免死锁。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

关键段结构

typedef struct _RTL_CRITICAL_SECTION {

    PRTL_CRITICAL_SECTION_DEBUGDebugInfo;

    LONG LockCount;

    LONG RecursionCount;

    HANDLE OwningThread; //  这个最关键,理解了这个变量就理解了关键段

    HANDLE LockSemaphore;

    DWORD SpinCount;

} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;

 

关键段中有个线程所有权的概念,

简单来说就是调用 EnterCriticalSection 的线程, 自身必须调用 LeaveCriticalSection 才会释放. Enter 与Leave 要配对使用.

如若在别的线程调用 LeaveCriticalSection 是无效的; 

同时需要注意 EnterCriticalSection 可以在同一线程中调用 RecursionCount 次 , 那么 LeaveCriticalSection 也需要调用相同次数.

否则死锁;

注意:

           关键段与读写锁 都是用于线程间的互斥, 但无法用于线程同步 , 线程同步需要用到内核对象,比如event对象.

           比如关键段只能在同一线程中使用Enter , Leave. 而无法在一个线程中使用Enter , 另一个使用Leave;

 

 

代码说明:

int g_count = 0;

//2个关键段
CRITICAL_SECTION cs_lock_param,cs_lock_thread;

unsigned int WINAPI t1( void* lpParameter)
{
    // 应在同一线程中调用 Enter , Leave
    EnterCriticalSection(&cs_lock_param); 
    int param  = *(int*)lpParameter;
    LeaveCriticalSection(&cs_lock_param);

    EnterCriticalSection(&cs_lock_thread);
//    EnterCriticalSection(&cs_lock_thread); 此处取消注释将死锁.Enter , Leave 注意配对使用
    g_count++;
    cout <<"tid:" << GetCurrentThreadId() << "," <<g_count  << "," << param<< endl;
    LeaveCriticalSection(&cs_lock_thread);
    return 0;
}

int main()
{
     /*
         初始化关键段, 也可以使用InitializeCriticalSection
         InitializeCriticalSectionAndSpinCount 在多核处理器上或许速度更快些,使用旋转锁
         先循环尝试进入,如果超时, 则切换成内核等待状态
     */
    InitializeCriticalSectionAndSpinCount(&cs_lock_param,4000); // 4000 是循环次数.
    InitializeCriticalSectionAndSpinCount(&cs_lock_thread,4000);


    HANDLE handles[5];
    for(int i = 0; i < 5; ++i) {
        /*
            注意不能在这里EnterCriticalSection;
            即在另一个线程中 Leave 是无效的
        */
        //EnterCriticalSection(&cs_lock_param); 
        handles[i] = (HANDLE)_beginthreadex(NULL,0,t1,&i,0,NULL);
    }

    WaitForMultipleObjects(5,handles,TRUE,INFINITE);

    DeleteCriticalSection(&cs_lock_param);
    DeleteCriticalSection(&cs_lock_thread);
    for(int i = 0; i < 5 ; ++i)
        CloseHandle(handles[i]);
return 0;
}

 

 

 

读写锁:

读锁: 多个线程可同时访问.

          AcquireSRWLockShared 可同时进入.

          对应的读线程很适合用于读取共享资源

写锁:与关键段类似,如果愿意, 你也可以把关键段替换成 写锁 ,

           AcquireSRWLockExclusive, 同一时间只有一个线程可以获取此锁

 

用写锁模拟关键段:

unsigned int WINAPI write_thread( void* lpParameter)
{
    //获取写锁
    AcquireSRWLockExclusive(&srwlock);
    g_count++;
    cout << "tid:" << GetCurrentThreadId() << " writing" << endl;
    Sleep(3000);
    cout << "tid:" << GetCurrentThreadId() << " writing end , gcount:" << g_count   << endl;
    cout << flush;

    //释放
    ReleaseSRWLockExclusive(&srwlock); //如果注释则将死锁
    return 0;
}

int main()
{
    //初始化
    InitializeSRWLock(&srwlock);

    HANDLE handles[5];
    for(int i = 0; i < 5 ; ++i)
        handles[i] = (HANDLE)_beginthreadex(0,0,write_thread,0,0,0);
    WaitForMultipleObjects(5,handles,TRUE,-1);

    for(int i = 0; i < 5;++i)
        CloseHandle(handles[i]);
return 0;
}

 

读写锁:

volatile int g_count = 0; 

SRWLOCK   srwlock;

//读线程
unsigned int WINAPI read_thread( void* lpParameter)
{
    AcquireSRWLockShared(&srwlock);
    cout << "tid:" << GetCurrentThreadId() << "  reading , gcount:" << g_count << endl;
    Sleep(1000);
    ReleaseSRWLockShared(&srwlock);
    return 0;
}

//写线程
unsigned int WINAPI write_thread( void* lpParameter)
{
    AcquireSRWLockExclusive(&srwlock);
    g_count++;
    cout << "tid:" << GetCurrentThreadId() << " writing" << endl;
    Sleep(500);
    cout << "tid:" << GetCurrentThreadId() << " writing end , gcount:" << g_count   << endl;
    cout << flush;
    ReleaseSRWLockExclusive(&srwlock);
    return 0;
}

int main()
{
    //初始化
    InitializeSRWLock(&srwlock);
    //5个写线程, 10个读
    HANDLE handles_w[5] , handles_r[10];

    //3个读锁
    for(int i = 0; i < 3; ++i)
        handles_r[i] = (HANDLE)_beginthreadex(0,0,read_thread,0,0,0);
    //5个写锁
    for(int i = 0; i < 5 ; ++i)
        handles_w[i] = (HANDLE)_beginthreadex(0,0,write_thread,0,0,0);
    //7个读锁
    for(int i = 3; i < 10; ++i)
        handles_r[i] = (HANDLE)_beginthreadex(0,0,read_thread,0,0,0);


    WaitForMultipleObjects(5,handles_w,TRUE,-1);
    WaitForMultipleObjects(10,handles_r,TRUE,-1);

    for(int i = 0; i < 10;++i)
    {
        if(i<5)
            CloseHandle(handles_w[i]);
        CloseHandle(handles_r[i]);
    }


return 0;
}

 

事件:

是一个内核对象, 意味着可以跨线程/进程 调用;

最简单的一个例子:

    /*
        第一和第四个参数不说明了;
        先看第三个参数, 如果把其设置成TRUE .
        相当于 调用了SetEvent , 即触发状态.
        此时 WaitForSingleObject 将直接返回
    */

    HANDLE e = CreateEvent(0,FALSE,TRUE,0);
    WaitForSingleObject(e,-1);
    cout << "end" << endl;

 

 

第一个例: 手动档Event . 简介  

从下面的代码中可以知, 不像关键段必须在同一个线程中使用

HANDLE ge;

//3个线程

unsigned int WINAPI event_thread1( void* lpParameter)
{
    WaitForSingleObject(ge,-1);
    cout << "event_thread1" << endl;
//    SetEvent(ge); 如果CreateEvent 第2个参数是FALSE;
    return 0;
}
unsigned int WINAPI event_thread2( void* lpParameter)
{
    WaitForSingleObject(ge,-1);
    cout << "event_thread2" << endl;
//    SetEvent(ge); 如果CreateEvent 第2个参数是FALSE;
    return 0;
}
unsigned int WINAPI event_thread3( void* lpParameter)
{
    WaitForSingleObject(ge,-1);
    cout << "event_thread3" << endl;
//    SetEvent(ge); 如果CreateEvent 第2个参数是FALSE;
    return 0;
}

int main()
{
    /*
        CreateEvent(NULL,TRUE,FALSE,NULL);
        重点说第二个参数, 其他参数参msdn;

        如果是 TRUE 意味着手动档, 即如果想重新使用 Event对象需要调用 ResetEvent;
        在3线程中 WaitForSingleObject 之后都是触发状态,所以将一起执行.
        就像跑100米, 裁判打枪(SetEvent) , 3个线程一起跑;
       
        
        如果是FALSE,WaitForSingleObject 之后将自动调用ResetEvent
        所以, 只有其中一个线程将运行, 此时 Event是非触发状态. 如果需要继续
        其他线程则需要重新调用SetEvent .
    */
    ge = CreateEvent(NULL,TRUE,FALSE,NULL);
    HANDLE handles[3];
    handles[0] = (HANDLE)_beginthreadex(0,0,event_thread1,0,0,0);
    handles[1] = (HANDLE)_beginthreadex(0,0,event_thread2,0,0,0);
    handles[2] = (HANDLE)_beginthreadex(0,0,event_thread3,0,0,0);

    //触发对象. 意思是启动吧,我的哥;
    SetEvent(ge);

    WaitForMultipleObjects(3,handles,TRUE,-1);
return 0;
}

 

 

第二个例子:自动档 . 此例子解决同步问题

 

HANDLE hEvent;
unsigned int __stdcall fun_event(void* p)
{
    int n = *(int*)p;
    printf("in thread :%d\n",n);

    SetEvent(hEvent);
    return 0;
}

int main()
{
    /*
        由于第二个参数是自动重置, 意味WaitForSingleObject返回后由
        系统调用ResetEvent;

        如果第二个参数是TRUE , 以下的代码将有问题.
        问题在于 WaitForSingleObject 只将等待一次,
        即第一个线程调用SetEvent 后, WaitForSingleObject将不会Reset.
        此时Event对象, 一直处于触发状态.
        所以除了第一个线程创建之后将等待, 其他线程将直接创建,WaitForSingleObject 将直接返回;
        如果想让代码有效则需要在 WaitForSingleObject 之后 ResetEvent
     */

    hEvent = CreateEvent(NULL,FALSE,FALSE, NULL);
    HANDLE handles[5];

    for(int i = 0; i < 5; ++i)
    {
        printf("in main :%d\n",i);
        handles[i] = (HANDLE)_beginthreadex(0,0,fun_event,&i,0,0);
        WaitForSingleObject(hEvent,-1);
//        ResetEvent(hEvent);  如果 CreateEvent(NULL,TRUE,FALSE, NULL); 则需要自己Reset
    }

    WaitForMultipleObjects(5,handles,TRUE,-1);
    CloseHandle(hEvent);
    return 0;
}

 

补充一个 : PulseEvent ,例子不写了, 相当于先Set , 后Reset. 一直没在实际工程中用过;

 

互斥量 与关键段类似 , 一个内核状态,一个用户状态.  都有线程所有权. 

关键段使用  EnterCriticalSection  来解锁.

互斥量 使用  WaitForSingleObject  来解锁.   一旦等待成功则 互斥量 内部将记录此线程id.

                       WaitForSingleObject   之后 线程成为了此 mutex 的拥有者 即独占了资源的访问权

                       , 在使用完后需要ReleaseMutex;  

                      如果忘记了ReleaseMutex 对于互斥量来说 是一个遗弃状态.

 

对于关键段 来说 如果忘记了 Leave 则直接死锁. 

而互斥量 是一个内核对象 , 即可以跨进程/线程使用 . 一旦忘记ReleaseMutex ,系统将调整这种状态, 即修改mutex

内部的变量,全部清除 . 好让其他 WaitForSingleObject   的进程/线程 继续;

例子:

HANDLE mutex;
unsigned int WINAPI mutex_thread( void* lpParameter)
{
    DWORD r = WaitForSingleObject(mutex,-1);  //线程争取中... 哪个被系统分配到了就进去
    cout << "ret:" << r <<endl;
    Sleep(1000);
    ReleaseMutex(mutex);
    return 0;
}

int main()
{
    mutex = CreateMutex(NULL,FALSE,NULL);  //注意第2个参数 , 如果是TRUE , 则需在主线程中先释放
    HANDLE arr[3];
    for(int i = 0; i < 3; ++i){
        arr[i] = (HANDLE)_beginthreadex(0,0,mutex_thread,0,0,0);
    }

    //ReleaseMutex(mutex); 如果CreateMutex(NULL,TRUE,NULL)
    WaitForMultipleObjects(3,arr,TRUE,-1);
    
return 0;
}

 

遗弃状态 : 意思是当需要 ReleaseMutex 的线程,因为某些原因崩溃了 . WaitForSingleObject 会返回 WAIT_ABANDONED

对上面的例子进行修改. 成为遗弃状态的mutex;

 

HANDLE mutex;
unsigned int WINAPI mutex_thread( void* lpParameter)
{
    DWORD r = WaitForSingleObject(mutex,-1);
    cout << "ret:" << r <<endl;    //看返回值,第2,3个线程都是128
    Sleep(1000);

    /*

       除了第一个抢占到的线程返回了 0 . 其他都是128;
       
       原因是第一个抢占的线程并没有  ReleaseMutex .
       
       此时系统将清除 mutex 内部的状态 . 让后续的线程继续抢占.
    

    
    */


//    ReleaseMutex(mutex);  注释掉这一行
    return 0;
}




//main中无变化. 
int main()
{
    mutex = CreateMutex(NULL,FALSE,NULL); 
    HANDLE arr[3];
    for(int i = 0; i < 3; ++i){
        arr[i] = (HANDLE)_beginthreadex(0,0,mutex_thread,0,0,0);
    }

    WaitForMultipleObjects(3,arr,TRUE,-1);
    
return 0;
}

 

 

 

信号量:

可以把Semaphore看成一个计数器 , 增加1 , 则有一个Wait 的线程可以启动;

 

常常用于线程的控制,  比如 最多同时启动3个线程,  当然也可以模拟事件对象 类似的同步功能

CreateSemaphore 第2个参数是当前用数, 第3个参数是最多数量

如果第2个是  0 , 第3个是 3 . 此时如果A线程正在 WaitForSingleObject, 将等待. 直到B线程

ReleaseSemaphore(hSem,1,NULL); 则给信号量增加1个可用数, 那么A线程将可调度了;

 

死锁的例子:

由于起始可用数为 0 , 同时没有一个线程ReleaseSemaphore ,将产生死锁. 注意

Semaphore 没有遗弃的问题

HANDLE sem;
unsigned int WINAPI sem_thread( void* lpParameter)
{
    WaitForSingleObject(sem,-1); //等待信号量.
    cout << GetCurrentThreadId() << endl;
    return 0;
}

int main()
{
    sem = CreateSemaphore(NULL,0,3,NULL); //起始可以用数: 0 , 最大可用数 3.
    HANDLE arr[3];
    for(int i = 0; i < 3; ++i){
        arr[i] = (HANDLE)_beginthreadex(0,0,sem_thread,0,0,0);
    }

    WaitForMultipleObjects(3,arr,TRUE,-1);

return 0;
}

 

修改上面的代码:

HANDLE sem;
unsigned int WINAPI sem_thread( void* lpParameter)
{
    WaitForSingleObject(sem,-1); //等待信号量.
    cout << GetCurrentThreadId() << endl;

    ReleaseSemaphore(sem,1,NULL);   //让信号量加1.
    return 0;
}

int main()
{
    sem = CreateSemaphore(NULL,0,3,NULL); //起始可以用数: 0 , 最大可用数 3.
    HANDLE arr[3];
    for(int i = 0; i < 3; ++i){
        arr[i] = (HANDLE)_beginthreadex(0,0,sem_thread,0,0,0);
    }

    //过了3秒 , 增加1个可用数;
    Sleep(3000);
    //调用后 , 将有1个线程抢占到;
    ReleaseSemaphore(sem,1,NULL);


    WaitForMultipleObjects(3,arr,TRUE,-1);

return 0;
}

 

Sem模拟事件:

HANDLE sem;
unsigned int WINAPI sem_thread( void* lpParameter)
{
    long count = 0;
    BOOL ret = ReleaseSemaphore(sem,1,&count); //相当于SetEvent
    cout << "ret :" << ret << ", precount:" << count << endl;
    return 0;
}
int main(int argc, char *argv[])
{

    //模拟事件.
    sem = CreateSemaphore(NULL,0,1,NULL); //起始可以用数: 0 , 最大可用数 1;
    HANDLE arr[3];
    for(int i = 0; i < 3; ++i){
        arr[i] = (HANDLE)_beginthreadex(0,0,sem_thread,0,0,0);
        //此时将等待, 直到ReleaseSemaphore , 与事件类似;
        WaitForSingleObject(sem,-1);
    }
    WaitForMultipleObjects(3,arr,TRUE,-1);

    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值