Windows编程之线程同步
阅读原文时间:2023年07月10日阅读:1

本笔记整理自:《Windows核心编程(第五版)》

目录

什么是线程同步

  • 多个线程是并行运行的,而在对堆区的变量是公有变量,任何线程都可以对他们进行访问和修改。这就会引发x访问冲突的问题。当多个线程同时修改一个变量时,极有可能会产生逻辑上的错误,甚至程序崩溃。

用户方式中的线程同步

InterlockedIncrement(LONG volatile *Addend)                                             // *Addend++;
InterlockedDecrement(LONG volatile *Addend)                                             // *Addend--;
InterlockedExchangeAdd(LONG volatile *Addend,LONG Value)                                // *Addend+=Value;
InterlockedExchangeSubtract(LONG volatile *Addend,LONG Value)                           // *Addend-=Value;
InterlockedExchange(LONG volatile *Target,LONG Value)                                   // *Target=Value;
TInterlockedExchangePointer(PVOID volatile *Target,PVOID Value)                         // *Target=&Value;
InterlockedCompareExchange(LONG volatile *Target,LONG Exchange,Long Compared)           // if(*Target==Compared) *pDest=Exchange;
InterlockedCompareExchangePointer(PVOID volatile *Target,PVOID Exchange,PVOID Compared) // if(*pDest==pCompare)   pDest=&value;
  • 是一种实现原子操作的较为简单的方式

  • 相关用法

    //Samples:
    CRITICAL_SECTION g_cs;
    InitializeCriticalSection(&g_cs);
    void thread_enter_function()
    {
    EnterCriticalSection(&g_cs);

    //访问线程共享的变量
    //在此范围内,涉及到的数据只允许一个线程使用
    //To-DO:...
    
    LeaveCriticalSection(&g_cs);

    }

  • 相关函数

    //初始化
    VOID InitializeCriticalSection(PCRITICAL_SECTION* pcs);

    //删除变量。当不需要这一结构体时,就可以调用此方法删除此变量
    VOID DeleteCriticalSection(PCRITICAL_SECTION* pcs);

    // 是否允许访问,可以用此函数代替EnterCriticalSection
    // 每一个返回true的TryEnterCriticalSection的调用必须搭配一个LeaveCriticalSection
    // 非挂起式关键段访问
    // 若有其他线程访问此关键段,则返回FALSE。可以访问则放回TRUE
    BOOL TryEnterCriticalSection(PCRITICAL_SECTION pcs);

    //进入关键段(当有其他在访问时会挂起)
    VOID EnterCriticalSection(PCRITICAL_SECTION pcs);

    //离开关键段
    VOID LeaveCriticalSection(PCRITICAL_SECTION pcs);

    //设置挂起前试图访问锁的次数
    //也就是说:若当前有其他地方正在访问关键段时,此处持续访问的次数,若超过这一次数,此处将会挂起。
    SetCriticalSectionSpinCount(PCRITICAL_SECTION pcs,DWORD dwSpinCount);

    //设置挂起前试图访问锁的次数并初始化变量
    InitializeCriticalSectionAndSpinCount(PCRITICAL_SECTION pcs,DWORD dwSpinCount);

内核对象的同步方式

  • 内核对象的同步是用什么来实现原子访问的呢?关键函数就是等待函数。

    /*

    • @params:
    • hObject:要等待的内核对象
    • dwMilliseconds:线程最多愿意花多长的时间来等待对象被触发。可以设为INFINITE来表示无限长的时间
      *
    • @return:
    • 指定了当前的状态
    • WAIT_OBJECT_0 : 表示等待的对象被触发
    • WAIT_TIMEOUT : 表示等待对象超过了dwMilliseconds
    • WAIT_FAILED : 给WaitForSingleObject传入了无效参数
      */
      DWORD WaitForSingleObject(HANDLE hObject,DWORD dwMilliseconds);

    /*

    • 和WaitForSingleObject类似,区别在于此函数可以同时检查多个内核对象的触发情况
      *
    • @params:
    • dwCount : 希望函数检查内核对象的数量,范围是 [1 , MAXIMUM_WAIT_OBJECTS]
    • phObjects : 内核对象数组
    • bWaitAll : 是否等待所有内核对象触发才取消堵塞(为false时只要有一个触发就取消堵塞)
    • dwMilliseconds:线程最多愿意花多长的时间来等待对象被触发。可以设为INFINITE来表示无限长的时间
      *
    • @return
    • 和WaitForSingleObject的区别在于:
    • WAIT_OBJECT_0 : 表示等待的对象被触发1个
    • WAIT_OBJECT_1 : 表示等待的对象被触发2个
    • bWaitAll设为false,正常情况下返回[ 1 , WAIT_OBJECT_0+(dwCount-1) ]
      *
    • PS:如果bWaitAll设为true,则返回WAIT_OBJECT_0
      / DWORD WaitForMultipleObjects(DWORD dwCount,CONST HANDLE phObjects,BOOL bWaitAll,DWORD dwMilliseconds);

基于此等待函数,下面将介绍四种内核对象。

  • 事件内核是最基本的对象。它主要管理:

    • 一个使用计数
    • 是否是人工重置事件的布尔值
    • 是否是触发状态的布尔值
  • 人工重置事件VS自动重置事件

    • 人工重置事件:得到通知时,等待时间的所有线程均变为可调度线程。
    • 自动重置事件:得到通知时,等待该事件的线程只有一个变为可调度线程。系统会自动将事件对象状态设置为未激活状态
  • 相关函数

    /*

    • @params
    • psa:安全性结构体
    • bManualReset : 创建的是一个手动事件(TRUE)还是自动重置事件(FALSE)
    • bInitialState : 初始的状态是触发(TRUE)还是未触发(FALSE)
    • pszName:事件名称(唯一标识字符串)
      */
      HANDLE CreateEvent(PSECURITY_ATTRIBUTES psa,BOOL bManualReset,BOOL bInitialState,PCTSTR pszName);

    /*

    • 打开已存在的事件。(供其他线程访问此事件)
    • @params
    • dwDesireAccess:(int)指定对事件对象的请求访问权限,如果安全描述符指定的对象不允许要求通过对调用该函数的过程,函数将返回失败
    • hInheriy:是否继承
    • pszName:事件名称
      */
      HANDLE OpenEvent(DWORD dwDesireAccess,BOOL hInheriy,PCTSTR pszName);

    BOOL SetEvent(HANDLE hEvent); //把事件设置为触发状态
    BOOL ResetEvent(HANDLE hEvent); //把事件设置为未触发状态
    BOOL PulseEvent(HANDLE hEvent); //触发一次或设置为未触发,相当于激发一次。(不常用)

在某个事件或按规定的间隔事件发出自己的信号通知的内核对象。

  • 相关函数

    /*

    • @params:
    • bManualReset:是手动重置计时器还是自动重置计时器
      */
      HANDLE CreateWaitableTimer(PSECURITY_ATTRIBUTES psa,BOOL bManualReset,PCTSTR pszName)

    //打开已存在的计时器内核对象
    HANDLE OpenWaitableTimer(PSECURITY_ATTRIBUTES psa,BOOL bManualReset,PCTSTR pszName)

    /*

    • @params
    • hTimer : 想要触发的计时器
    • pDueTime : 第一次触发的事件的时间
    • lPeriod :在第一次触发之后,计时器应该以怎样的频度触发。(单位为毫秒)
    • pfnCompletionRoutine : 可设为NULL
    • pvArgToCompletionRoutine : 可设为NULL
    • bResume : 可设为false
      */
      BOOL SetWaitableTimer(
      HANDLE hTimer,
      const LARGE_INTEGER *pDueTime;
      LONG lPeriod,
      PTIMERAPCROUTINE pfnCompletionRoutine,
      PVOID pvArgToCompletionRoutine,
      BOOL bResume
      );

    //取消计时器
    BOOL CancelWaitableTimer(HANDLE hTimer);

  • 用来对资源进行计数。它的规则如下:

    • 如果当前资源计数>0,那么信号量处于触发状态。
    • 如果当前资源计数=0,那么信号量处于未触发状态
    • 在线程中调用一个等待函数,如果>0,则计数器-1,线程取消阻塞。
  • 相关函数

    HANDLE CreateSemaphore(PSECURITY_ATTRIBUTES psa,LONG lInitialCount,LONG lMaximumCount,PCTSTR pszName);

    HANDLE OpenSemaphore(DWORD dwDesiredAccess,BOOL bInheritHandle,PCTSTR pszName);

    /*

    • @params:
    • hSemaphore : 信号量内核对象句柄
    • lReleaseCount : 增加的计数值
    • plPreviousCount : 增加前的计数值
      */
      BOOL ReleaseSemaphore(HANDLE hSemaphore,LONG lReleaseCount,PLONG plPreviousCount)
  • 用来确保一个线程独占对一个资源的访问。

  • 包含一个实用技术、线程ID以及一个递归计数

  • 互斥量规则如下

    • 如果线程ID为0(无效线程ID),那么该无耻两不为任何线程所占有,它处于触发状态
    • 如果线程ID非0,那么有一个线程已经占用了该互斥量,它处于未触发状态。
    • 操作系统对互斥量进行了特殊处理
  • 等待函数不在返回WAIT_OBJECT0,而是返回特殊的值WAIT_ABANDONED

  • 互斥量和关键段的比较

特征

互斥量

关键段

性能

是否能跨进程使用

声明

HANDLE hmtx;

CRITICAL_SECTION cs;

初始化方式

hmtx=CreateMutex(NULL,FALSE,NULL);

InitializeCriticalSection(&cs);

清理

CloseHandle(hmtx);

DeleteCriticalSection(&cs);

无限等待

WaitForSingleObject (hmtx, INFINITE);

EnterCriticalSection(&cs);

0等待

WaitForSingleObject (hmtx, 0);

TryEnterCriticalSection(&cs);

任意长时间的等待

WaitForSingleObject (hmtx, timeLength)

不支持

释放

ReleaseMutex(hmtx);

LeaveCriticalSection(&cs);

是否能同时等待其他内核对象

是(WaitForMultipleObjects或其他)

  • 相关函数

    /*

    • @params
    • bInitialOwner : 控制互斥量的初始化状态,
    • FALSE :无占用
    • TRUE:占用的线程为当前线程
      */
      HANDLE CreateMutex(
      PSECURITY_ATTRIBUTES psa;
      BOOL bInitialOwner,
      PCTSTR pszName
      );

    HANDLE OpenMutex(
    DWORD dwDesiredAccess,
    BOOL bInitialOwner,
    PCTSTR pszName
    );

    BOOL ReleaseMutex(HANDLE hMutex);