线程同步——内核对象(互斥、事件、信号量、可等待计时器)

三、内核模式下的线程同步

  • Windows系统中有多种机制可用于线程同步,它们一般都被称之为内核对象(并非全部),一般我们常用的有以下几种:
    • 互斥对象(Mutex)
    • 事件对象(Event)
    • 信号量(Semaphore)
    • 可等待计时器(Waitable Timer)

0.等待函数

  1. WaitForSingleObject
    等待函数的作用是使一个线程进入到等待状态,直到指定的内核对象被触发为止,其函数原型如下所示:
DWORD WaitForSingleObject(
    _In_    HANDLE    hHandle,                //内核对象句柄
    _In_    DWORD    dwMiliseconds        //等待超时时间(微秒,INFI)
);

/******
***return
WAIT_OBJECT_0: 成功返回
WAIT_TIMEOUT:  超时返回
WAIT_FAILED:   传入的参数错误
*/

在创建线程时使用等待函数后,此函数会在等待超时或线程结束时返回,因此我们的主线程一次只能启动一个线程,从而避免上述例子中多线程访问同一个数据时所引发的问题.

  1. WaitForMultipleObjects

与WaitForSingleObject类似,唯一的不同之处在于它允许调用线程同时检查多个内核对象的触发状态

DWORD WaitForMultipleObjects(
    DWORD dwCount,              // 检查的内核对象的数量
    CONST HANDLE* phObjects,    // 内核对象句柄数组
    BOOL bWaitAll,              // 是否在所有内核对象触发之后返回
    DWORD dwMilliseconds)       // 等待时间
/***********
* return
WAIT_FAILED
WAIT_TIMEOUT
// 如果 bWaitAll 是 TRUE ,则返回 WAIT_OBJECT_0
// 如果 bWaitAll 是 FALSE, 则返回值是 WAIT_OBJECT_0和(WAIT_OBJECT_0 + dwCount - 1) 之间的任意一个值,内核对象数组的下标为:返回值 - WAIT_OBJECT_0
*/

对一些内核对象来说,成功地调用 WaitForSingleObject 与 WaitForMultipleObjects 事实上会改变对象的状态。如:自动重置事件内核对象,当时间对象被触发的时候,函数会检测到这一情况,这时它可以直接返回 WAIT_OBJECT_0 给调用线程。但是,就在函数返回之前,它会使事件变为非触发状态——这就是等待成功所引起的副作用。 WaitForMultipleObjects是以原子方式工作的,当函数检查内核对象的状态时,任何其他线程都不能在背后修改对象的状态。

1.事件对象

事件(Event) 是在线程同步中最常使用的一种同步对象,而且比其他对象要简单一些。像其他对象一样,事件包含了一个使用计数,一个是用来标识自动重置/手动重置的BOOL值,另一个是表示事件有没有触发的BOOL值

  • 事件对象有两种状态,他们分别是:
    • 手动状态:被触发后所有等待该事件的线程都将变为可调度状态,常用于控制具有较强自定义要求的多线程同步环境.
    • 自动状态:被触发后只有一个等待事件线程会变成可调度状态。
/*关键函数*/
HANDLE CreateEvent(
    LPSECURITY_ATTRIBUTES  lpEventAttributes,//属性
    BOOL  hManualReset,    //手工重置
    BOOL  bInitialState,      //初始状态
    LPCTSTR  lpName            //事件对象名称
);
//注:非手工状态下,调用SetEvent放行一个线程后,会自动再次设为无
//信号状态,直到再次调用SetEvent

/*设置标记为有信号状态(释放等待函数)*/
BOOL SetEvent(
    HANDLE hEvent        //事件对象句柄
);

/*重置标记为无信号状态(阻塞等待函数)*/
BOOL    WINAPI    ResetEvent(
    HANDLE  hEvent        //事件对象句柄
);

//打开内核对象
HANDLE OpenEvent(
    DWORD    dwDesiredAccess,//对事件对象的请求访问权限
    BOOL        bInheritHandle,//是否能继承
    LPCTSTR    lpName        //事件对象的名字
);

一个防多开的例子

if (!OpenEvent(EVENT_MODIFY_STATE, TRUE,L"Global\\Text"))
        CreateEvent(NULL, TRUE, TRUE, L"Global\\Text");
else
        return 0;

示例:

int     g_nNum = 0;
HANDLE  g_hEventA = nullptr;
HANDLE  g_hEventB = nullptr;
DWORD WINAPI ThreadProcA(LPVOID lpParam) {
    for (int i = 0; i < 5; i++){
        WaitForSingleObject(g_hEventA, INFINITE);
        ResetEvent(g_hEventB);
        printf("%d ", g_nNum++);
        SetEvent(g_hEventB);
    }
    return 0;
}
DWORD WINAPI ThreadProcB(LPVOID lpParam){
    for (int i = 0; i < 5; i++){
        WaitForSingleObject(g_hEventB, INFINITE);
        ResetEvent(g_hEventA);
        printf("%d ", g_nNum++);
        SetEvent(g_hEventA);
    }
    return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
    if (!(g_hEventA = CreateEvent(NULL, TRUE, TRUE, NULL))) return 0;
    if (!(g_hEventB = CreateEvent(NULL, TRUE, FALSE, NULL))) return 0;
    CreateThread(NULL, 0, ThreadProcA, NULL, 0, nullptr);
    CreateThread(NULL, 0, ThreadProcB, NULL, 0, nullptr);
    system("pause");
    return 0;
}

2. 可等待的计时器内核对象(Waitable Timer)

可等待的计时器(Waitable Timer): 会在某个指定的时间触发,或每隔一段时间触发一次。

创建可等待的计时器:

HANDLE CreateWaitableTimer(
    PSECURITY_ATTRIBUTES psa,
    BOOL bManualReset,
    PCTSTR pszName);

打开获取一个已经存在的可等待计时器的句柄:

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

在创建可等待的计时器的时候,计时器总是处于未触发状态。等我们想要触发计时器的时候必须调用 SetWaitableTimer 函数:

BOOL SetWaitableTimer(
    HANDLE hTimer,
    const LARGE_INTEGER *pDueTime,               // 表示计时器第一次触发的时间
    LONG lPeriod,                                // 在第一次触发之后,多长时间触发一次
    PTIMERAPCROUTINE pfnCompletionRoutine,       // 计时器函数
    PVOID pvArgToCompletionRoutine,              // 传入参数
    BOOL bResume);                               // 是否支持挂起与恢复

取消计时器:

BOOL CancelWaitableTimer(HANDLE hTimer);         //取消一个计时器
//第一次触发时间为200811日下午1:00,之后每隔6小时触发一次
HANDLE  hTimer;
SYSTEMTIME  st;
FILETIME    ftLocal, ftUTC;
LARGE_INTEGER   liUTC;
hTimer  = CreateWaitableTimer(NULL, FALSE, NULL);

st.wYear = 2008;
st.wMonth = 1;
st.wDayOfWeek = 0;   //忽略
st.wDay = 1;
st.wHour = 13;
st.wMinute = 0;
st.wSecond = 0;
st.wMilliseconds = 0;

SystemTimeToFileTime(&st, &ftLocal);

// 将本地时间转换为 UTC 时间
LocalFileTimeToFileTime(&ftLocal, &ftUTC);
// 将 FILETIME 转换为 LARGE_INTEGER , FILETIME 与 LARGE_INTEGER 二进制格式一致,但是前者是地址是32为边界,后者是64位边界
liUTC.LowPart = ftUTC.dwLowDateTime;
liUTC.HighPart = ftUTC.dwHighDateTime;

SetWaitableTimer(hTimer, &liUTC, 6*60*60*1000,
    NULL, NULL, FALSE);

// 触发一次就不再触发的计时器,即给 lPeriod参数传入0就可以了。
创建APC(asynchronous procedure call, 异步过程调用)计时器
// 异步过程调用原型
VOID APIENTRY TimerAPCRoutine(PVOID pvArgToCompletionRoutine, DWORD dwTimerLowValue, DWORD dwTimerHighValue)
{
    FILETIME ftUTC, ftLocal;
    SYSTEMTIME st;
    TCHAR szBuf[256];

    ftUTC.dwLowDateTime = dwTimerLowValue;
    ftUTC.dwHighDateTime = dwTimerHighValue;

    FileTimeToLocalFileTime(&ftUTC, &ftLocal);

    FileTimeToSystemTime(&ftLocal, &st);

    GetDateFormat(LOCALE_USER_DEFAULT, DATE_LONGDATE,
        &st, NULL, szBuf, _countof(szBuf));
    _tcscat_s(szBuf, _countof(szBuf), TEXT(" "));
    GetTimeFormat(LOCALE_USER_DEFAULT, 0,
        &st, NULL, _tcschr(szBuf, TEXT('\0')),
        (int)(_countof(szBuf) - _tcslen(szBuf)));

    //Show the time to the user
    MessageBox(NULL, szBuf, TEXT("Timer went off at..."), MB_OK);
}

void SomeFunc() {
    // Create a timer. (It doesn't matter whether it's manual-reset
    // or auto-reset.)
    HANDLE hTimer = CreateWaitableTimer(NULL, TRUE, NULL);

    LARGE_INTEGER li = {0};
    SetWaitableTimer(hTimer, &li, 5000, TimerAPCRoutine, NULL, FALSE);

    SleepEx(INFINITE, TRUE);

    CloseHandle(hTimeer);
}

SetWaitableTimer线程必须是由于调用 SleepEx、WaitForSingleObjectEx、WaitForMultipleObjectsEx、MsgWaitForMultipleObjectsEx 或 SignalObjectAndWait 而进入等待状态,APC异步调用才会被调用。

3.信号量

  • 信号量是一种用于管理多个线程的复杂同步问题的解决方案,它可以限制某一时间段最多有多少个线程可以同时处于运行状态
  • 信号量实现这个功能的原理是维护了一个计数器,计数器的值可以在0至用户指定的最大值之间,当一个线程完成了对信号量的等待后,计数器的值增加,当一个信号量被释放时,信号量的计数器减少。
  • 当计数器的值为0时,信号量处于无信号状态(阻塞),不为0时则信号量处于有信号状态(释放)
  • 如果我们将信号量的最大值设为1,那么它的作用与互斥对象将完全一致

信号量关键函数:

/*创建信号量*/
HANDLE  WINAPI  CreateSemaphore(
    _In_opt_    LPSECURITY_ATTRIBUTES  lpSemaphoreAttributes,//属性
    _In_           LONG                               lInitialCount,     //信号初始值
    _In_           LONG                               lMaximumCount,//信号最大值
    _In_opt_    LPCTSTR                           lpName    //信号量名称
  );

/*释放信号量*/
BOOL ReleaseSemaphore(
    HANDLE  hSemaphore,        //信号量句柄
    LONG      lReleaseCount,    //释放的信号量数量
    LPLONG  lpPreviousCount   //返回信号量上次值
);

/*打开信号量*/
HANDLE    WINAPI    OpenSemaphore(
    DWORD    dwDesiredAccess,    //对信号量的请求访问权限
    BOOL        bInheritHandle,    //是否允许子进程继承此句柄
    LPCTSTR    lpName            //信号量名称
);

示例

int    g_nNum    =    0;
HANDLE  g_hSemaphore    =    nullptr;
DWORD  WINAPI  ThreadProc(LPVOID  lpParam)
{
    for (int i = 0; i < 5; i++)
    {
        WaitForSingleObject(g_hSemaphore,  INFINITE);
        printf("%d", g_nNum++);
        ReleaseSemaphore(g_hSemaphore, 1, NULL);
    }
    return    0;
}
int    _tmain(int argc, _TCHAR*    argv[])
{
    if (!(g_hSemaphore=CreateSemaphore(NULL,0,1,NULL)) )
    {
        return    0;
    }
    CreateThread(NULL, 0, ThreadProc, NULL, 0, nullptr);
    CreateThread(NULL, 0, ThreadProc, NULL, 0, nullptr);

    system("pause");
    return 0;
}

4.互斥对象

互斥对象是一个非常简单的多线程同步内核对象,如果一个信号量未被线程所拥有(被等待函数获取),那么它是”有信号状态(非阻塞)”,只要它被线程获取,那么它就会变成”无信号状态(阻塞)”,需要注意的是,单一互斥对象只对同一线程有效,以下是互斥对象的一些常用API

//创建互斥对象
HANDLE  CreateMutex(
    LPSECURITY_ATTRIBUTES    lpMutexAttributes,    //属性
    BOOL    bInitialOwner,     //初始状态
    LPCTSTR lpName    //互斥对象名称
);
//释放互斥对象
BOOL ReleaseMutex(HANDLE hMutex);   //互斥对象句柄

//打开互斥对象
HANDLE OpenMutex(
    DWORD dwDesiredAccess,    //对互斥对象的请求访问权限
    BOOL bInheritHandle,//是否希望子进程能够继承句柄
    LPCTSTR lpName        //互斥对象名称
);

如果线程成功地等待了互斥量对象不止一次,那么线程必须调用 ReleaseMutex 相同的次数才能使对象递归计数变成0.当递归计数变成0的时候,函数还会将线程ID设为0,这样就触发了对象。

互斥对象—示例

int     g_nNum    =    0;
HANDLE    g_hMutex    =    nullptr;
DWORD    WINAPI    ThreadProc(LPVOID    lpParam)
{
    for (int i = 0; i < 5; i++)
    {
        WaitForSingleObject(g_hMutex,INFINITE);
        printf("%d",g_nNum++);
        ReleaseMutex(g_hMutex);
    }
    return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
    if (!(g_hMutex=CreateMutex(NULL, FALSE,NULL)))
        return    0;
    CreateThread(NULL, 0, ThreadProc, NULL, 0, nullptr);
    CreateThread(NULL, 0, ThreadProc,NULL, 0, nullptr);

    system("pause");
    return 0;
}

互斥与临界区的比较:

特征 互斥量 临界区
性能
是否能跨进程使用
声明 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, dwMilliseconds); 不支持
释放 ReleaseMutex(hmtx); LeaveCriticalSection(&cs);
是否能同时等待其他内核对象 WaitForMultipleObjects

总结

线程同步对象速查:

对象 何时处于未触发状态 何时处于触发状态 成功等待的副作用
进程 进程仍在运行 进程终止时(ExitProcess,TerminateProcess)
线程 线程仍在运行 线程终止时(ExitThread,TerminateThread)
作业 作业尚未超时的 作业超时时
文件 有待处理的IO请求时 IO请求完成时
控制台输入 没有输入时 有输入时
文件变更通知 文件没有变更时 文件系统检测到变更的时候 重置通知
自动重置事件 ResetEvent,PulseEvent或等待成功的时候 SetEvent/PulseEvent被调用的时候 重置事件
手动重置事件 ResetEvent,PulseEvent SetEvent/PulseEvent被调用的时候 没有
自动重置可等待计时器 CancelWaitableTimer或等待成功时 时间到的时候(SetWaitableTimer) 重置计时器
手动重置可等待计时器 CancelWaitableTimer 时间到的时候(SetWaitableTimer) 没有
信号量 等待成功的时候 计数大于0的时候(ReleaseSemaphore) 计数减1
互斥量 等待成功的时候 不为线程占用的时候(ReleaseMutex) 把所有权交给线程
临界区(用户模式) 等待成功的时候((Try)EnterCriticalSection) 不为线程占用的时候(LeaveCriticalSection) 把所有权交给线程
SRWLock(用户模式) 等待成功的时候(AcquireSRWLock(Exclusive)) 不为线程占用的时候(ReleaseSRWLock(Exclusive)) 把所有权交给线程
条件变量(用户模式) 等待成功的时候(SleepConditionVariable*) 被唤醒的时候(Wake(All)ConditionVariable) 没有

其他的线程同步函数

  1. 异步设备I/O
    异步设备IO(asynchronous device I/O)允许线程开始读取操作或写入操作,但不必等待读取操作或写入操作完成。设备对象是可同步的内核对象。
  2. WaitForInputIdle函数(将自己挂起)

    DWORD WaitForInputIdle(
    HANDLE hProcess,
    DWORD dwMilliseconds);
  3. MsgWaitForMultipleObjects(Ex)既可以等待内核触发状态,也可以处理消息
  4. WaitForDebugEvent 函数等待调试消息
  5. SignalObjectAndWait 函数 通过一个原子操作触发一个内核对象并等待另一个内核对象
    调用这个函数时,参数hObjectToSignal标识的必须是一个互斥量、信号量或事件。任何其他类型的对象将导致函数返回 WAIT_FAILED,这时调用GetLastError会返回ERROR_INVALID_HANDLE。该函数内部会检查对象的类型并分别执行与ReleaseMutex、ReleaseSemaphore、SetEvent 等价的操作。
  6. 使用等待链遍历API检测死锁
可能的锁 描述
临界区 Windows会记录哪个线程正在占用哪个临界区
互斥量 Windows会记录哪个线程正在占用哪个互斥量。即便已经被遗弃的互斥量
进程和线程 Windows会记录哪个线程正在等待进程终止或线程终止
SendMessage调用 知道哪个线程正在等待SendMessage调用返回
COM初始化和调用 Windows会记录对CoCreateInstance的调用以及对COM对象的方法的调用
高级本地过程调用(Advanced Local Procedure Call,ALPC) 本地过程调用

你可能感兴趣的:(windows核心编程,多线程)