欢迎光临
我们一直在努力

Win32临界区

线程安全问题

  • 变量存储特点
  • 局部变量:存在线程自己的栈空间,每个线程独立副本,多线程互不干扰,天然线程安全
  • 全局变量 / 堆变量:存全局地址空间,所有线程共享同一份数据,会产生线程安全问题
    ✅产生线程安全问题两个必要条件,必须同时满足:
  • 多个线程访问同一个共享资源
  • 访问中包含写操作(修改)
    只有读、没有写,不会出现线程安全问题
  • 售票案例(竞态条件)
  • DWORD g_dwTickets = 10;

    //多个线程同时执行这段代码
    while(g_dwTickets>0)
    {
    printf("还有:%d张票\\n",g_dwTickets);
    g_dwTickets;
    printf("卖出一张,还有:%d张\\n",g_dwTickets);
    }

    故障现象:重复卖票、票数变成负数
    根本原因:检查和修改不是原子操作 线程可以在任意 CPU 指令间隙发生线程切换
    示例流程:

  • 线程 A 判断 g_dwTickets>0,此时票数 = 1;时间片到,线程切换
  • 线程 B 抢到 CPU,完整执行:判断、自减,票数变为 0
  • 切回线程 A,A 继续执行g_dwTickets–,票数直接变成 ‑1
    竞态条件:程序结果依赖线程调度切换时机,结果不可预测
  • 临界资源、临界区

  • 临界资源:同一时刻只允许一个线程访问的共享资源,全局变量、共享内存都属于临界资源
  • 临界区:操作临界资源的那一段代码
    目标:保证同一时间,最多只有一个线程执行临界区代码
    自己手写 if 判断令牌不行:判断 + 赋值两条 C 语句,会被线程切换打断,依旧有竞态
    Windows 同步 API 底层依靠 CPU 硬件原子指令保证操作不可打断
  • CRITICAL_SECTION 临界区对象(用户态锁)

    API 函数:
    /

    /初始化临界区对象
    void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

    //进入临界区:获取锁(拿令牌),拿不到就阻塞等待
    void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

    //离开临界区:释放锁(归还令牌)
    void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

    使用完整步骤

  • 定义全局 / 堆上 CRITICAL_SECTION cs;
    ❗不能是局部栈变量,多个线程要访问同一个锁对象
  • 主线程调用 InitializeCriticalSection(&cs); 初始化
  • 所有读写共享变量的代码(包括 if/while 判断)全部包裹在 Enter ~ Leave 之间
  • 操作完成调用LeaveCriticalSection(&cs);释放锁
    错误写法:
  • //错误!判断在临界区外面
    while(g_dwTickets>0)
    {
    EnterCriticalSection(&cs);
    g_dwTickets;
    LeaveCriticalSection(&cs);
    }

    while 判断读取共享变量没有上锁,切换依然会出现竞态
    正确写法:

    while(1)
    {
    EnterCriticalSection(&cs);

    if(g_dwTickets > 0)
    {
    g_dwTickets;
    printf("卖出一张,剩余:%d\\n",g_dwTickets);
    LeaveCriticalSection(&cs);
    }
    else
    {
    LeaveCriticalSection(&cs);
    break;
    }
    }

    临界区使用权衡

  • 锁范围太小:部分共享变量操作没上锁,线程安全失效
  • 锁范围太大:临界区内代码执行久,其他线程大量等待,并发性能下降
  • CRITICAL_SECTION 关键特性

  • 用户态同步对象,不是内核对象
    不能用CloseHandle;不需要句柄;销毁使用DeleteCriticalSection()
  • 同一个线程可以多次 EnterCriticalSection(递归锁),内部维护引用计数;Enter 几次,就必须 Leave 几次
  • 只用于同一个进程内部多线程同步,不能跨进程使用
  • 获取不到锁的线程会阻塞
  • 总结

  • 局部变量在栈,线程私有,安全;全局 / 堆共享,多线程写会出问题
  • 线程安全两个条件:多线程访问同一资源 + 存在写操作。只读不会出问题
  • 竞态条件:检查和修改不是原子,线程在中间切换,逻辑错乱
  • CRITICAL_SECTION临界区:用户态锁,仅本进程内使用,不能跨进程
  • 所有对共享变量的判断、读写全部要包裹在 Enter‑Leave 中间,不能把判断放锁外面
  • Enter 几次,就要 Leave 几次;不能遗漏 Leave,否则其他线程永久卡死等待
  • #include <windows.h>
    #include <stdio.h>
    CRITICAL_SECTION g_cs;
    DWORD g_ticket = 10;

    DWORD WINAPI SellTicket(LPVOID p)
    {
    while(TRUE)
    {
    EnterCriticalSection(&g_cs);
    if(g_ticket > 0)
    {
    g_ticket;
    printf("线程%X卖出一张,剩余=%d\\n",GetCurrentThreadId(),g_ticket);
    LeaveCriticalSection(&g_cs);
    }
    else
    {
    LeaveCriticalSection(&g_cs);
    break;
    }
    }
    return 0;
    }
    int main()
    {
    InitializeCriticalSection(&g_cs);
    HANDLE h1 = CreateThread(NULL,0,SellTicket,NULL,0,NULL);
    HANDLE h2 = CreateThread(NULL,0,SellTicket,NULL,0,NULL);

    WaitForSingleObject(h1,INFINITE);
    WaitForSingleObject(h2,INFINITE);

    CloseHandle(h1);
    CloseHandle(h2);
    DeleteCriticalSection(&g_cs);
    return 0;
    }

    赞(0)
    未经允许不得转载:171主机测评 » Win32临界区
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址