Windows核心编程笔记(九)使用内核对象进行线程同步

上一章作者介绍了用户模式下的线程同步,虽然它们有很好的性能,但是他们功能有限并不是能够胜任实际工作中出现的复杂情况,以及特殊的要求,于是Windwos提供了一些用于线程同步的内核对象,它们根据不同的应用场景而设计,满足不同的环境下的线程同步的要求,因为是基于内核对象来完成同步,因此线程同步时可以跨进程的。

这些内核对象包括,事件,可等待计时器,信号量,互斥量。


在具体介绍每个线程同步内核对象之前,需要了解两个基础的用于等待内核对象的函数WaitForSingleObjectEx 和  WaitForMultipleObjects.


DWORD WINAPI WaitForSingleObject(
  __in  HANDLE hHandle,  //内核对象句柄
  __in  DWORD dwMilliseconds  //等待的时间、单位:毫秒  如果指定(INFINITE)表示无信号一直等待。
);

返回值  

WAIT_ABANDONED   (只在对象为互斥量类型时用到, 表示上一个线程因为错误,线程异常终止,内核对象才变为了有信号的)

WAIT_OBJECT_0  (内核对象有信号)
 
WAIT_TIMEOUT    (等待时间到了)
 
WAIT_FAILED  (参数错误,函数调用失败)
 

DWORD WINAPI WaitForMultipleObjects(
  __in  DWORD nCount,  (多少个要等待的对象)
  __in  const HANDLE *lpHandles,  内核对象句柄数组
  __in  BOOL bWaitAll,  (是否等待所有对象有信号才返回)
  __in  DWORD dwMilliseconds  (等待时间)
);

该API函数等待一个或者一组线程同步内核对象,由 bWaitAll指定等待方式, bWaitAll == 1在所有对象处于有信号状态返回, bWaitAll=0 只有其中一个处于有信号状态就返回


返回值




WAIT_OBJECT_0 to (WAIT_OBJECT_0 + nCount– 1)      (WAIT_OBJECT_0+N)表示第N个对象处于有信号状态在bWaitAll为1时,如果bWaitAll为0    WAIT_OBJECT_0表示所有对象处于有信号状态,成功返回。
 
WAIT_ABANDONED_0 to (WAIT_ABANDONED_0 + nCount– 1)  (如果bWaitAll为1,WAIT_ABANDONED_0表示句柄数组中至少有一个是互斥量对象的句柄,如果bWaitAll为0,那么WAIT_ABANDONED_0+N表示句柄数组中第N个是一个互斥量句柄,上一个线程没有释如果放互斥量的情况下异常终止

 
WAIT_TIMEOUT   (同WaitForSingleObject)
 
WAIT_FAILED    (WaitForSingleObject)



(一)事件内核对象

事件内核对象在上一章的用户模式的同步对象 CriticalSection 中已经提到,CriticalSection只是用了它的自动重置模式,自动重置模式在WaitForSingleObject获取到有信号后自动将对象设置为无信号状态并返回。

临界区对象在EnterCriticalSection中调用执WaitForSingleObjec行等待,在LeaveCriticalSection中调用SetEvent将事件对象重置为有信号状态。


此外事件内核对象可以使用手动重置模式,在手动模式下,WaitForSingleObject并改变其状态,需要手动调用SetEvent将对象置为有信号状态,或者调用ResetEvent将对象置为无信号状态,在将对象置为有信号状态时,所有使用WaitXX执行等待的线程都可以返回WAIT_OBJECT_0 得到调度。



API:

HANDLE WINAPI CreateEvent(
  __in_opt  LPSECURITY_ATTRIBUTES lpEventAttributes,
  __in      BOOL bManualReset, //TRUE自动模式 FALSE手动模式
  __in      BOOL bInitialState,//初始值是否是有信号的,TRUE (是),FALSE(不是)
  __in_opt  LPCTSTR lpName
);

BOOL WINAPI SetEvent(
  __in  HANDLE hEvent
);


BOOL WINAPI ResetEvent(
  __in  HANDLE hEvent
);


适应:在自动模式下与 临界区对象  互斥量 SRWLock的互斥模式  功能是一样的,都保证对共享资源的访问在同一时间只运行一个线程独占。

区别  :临界区对象在使用内核机制前有个一个旋转锁等待循环,如果在确定对共享资源独占期间占用很少的CPU周期,应该使用临界区对象。

              如果区分写共享资源和读共享资源的线程,即存在生产和消费两种类型的线程,使用SRWLock

如果要进行进程间的同步,或者在独占共享资源之后的操作可能占用很多的CPU周期,如等待用户输入并把用户的输入写入共享资源,应该使用事件或者互斥量,这两个内核同步对象,使线程在无信号状态不参与调度,不占用CPU,而不是SRWLock那样循环的使用原子访问和NOP


                 互斥量和事件对象的区别很小,互斥量允许一个在异常终止后系统自己是否它对对象的占用,从而避免死锁。事件对象则多了一个手动模式,允许多个线程同时得到执行



(二)可等待的计时器

可等待计时器,类似于SetTimer,它设置一个定时器,并可以设置周期循环触发的时间,在时间到期时所有等待线程可以得到执行(手动模式),或者一个等待线程被唤醒(自动模式)。同时它还可以设置一个APC回调函数,在时间到期时将ACP挂入 SetWaitableTimer 主调线程的APC队列中。



API:

HANDLE WINAPI CreateWaitableTimer(
  __in_opt  LPSECURITY_ATTRIBUTES lpTimerAttributes,
  __in      BOOL bManualReset, //自动还是手动模式
  __in_opt  LPCTSTR lpTimerName   //内核对象名字
);


BOOL WINAPI SetWaitableTimer(
  __in      HANDLE hTimer,
  __in      const LARGE_INTEGER *pDueTime,   //触发的时间,如果为负数表示相对时间,单位纳秒
  __in      LONG lPeriod,                    //触发后多久循环触发一次一次
  __in_opt  PTIMERAPCROUTINE pfnCompletionRoutine,  //APC回调函数地址
  __in_opt  LPVOID lpArgToCompletionRoutine,        //回调函数参数
  __in      BOOL fResume                        //是否唤醒睡眠中的系统,利用电源管理
);

取消对象的活动状态,不再触发。

BOOL WINAPI CancelWaitableTimer(
  __in  HANDLE hTimer
);


测试代码 使用自动重置模式,10秒后触发,之后每秒触发一次

#include <iostream>
#include <stdio.h>
#include <windows.h>
#include <thread>
#include <winbase.h>



//using namespace std;
using std::cout;
using std::endl;


HANDLE hTimer = NULL;
volatile LONG g_IsPassed = 0;

void ThreadForTimer(int pParameter)
{
    //int ID  = *static_cast<int*>(pParameter);
    int ID = pParameter;
    InterlockedAdd(&g_IsPassed,1);
    while(1)
    {
        cout.flush();
        if(WaitForSingleObject(hTimer,INFINITE) == WAIT_OBJECT_0)
        {
            cout<<"Thread "<<ID<<" has obtained the single Time:"<<GetTickCount()<<endl;
        }
        else
        {
            cout<<"Thread exception occurred"<<endl;
        }
    }


}

int main(int argc, char *argv[])
{
    LARGE_INTEGER liDueTime;

    liDueTime.QuadPart = -100000000L;

    // Create an unnamed waitable timer.
    hTimer = CreateWaitableTimer(NULL, FALSE, NULL);
    if (NULL == hTimer)
    {
        printf("CreateWaitableTimer failed (%d)\n", GetLastError());
        return 1;
    }

    printf("Waiting for 10 seconds   Time:%d \n ",GetTickCount());


    if (!SetWaitableTimer(hTimer, &liDueTime, 1000, NULL, NULL, 0))
    {
        printf("SetWaitableTimer failed (%d)\n", GetLastError());
        return 2;
    }

    for(int i = 0; i != 5; ++i)
    {
        new std::thread(ThreadForTimer,i);
        while(1)
        {
            if(InterlockedCompareExchange(&g_IsPassed,0,1) == 1)  break;
            YieldProcessor();
        }
    }
    SleepEx(INFINITE,TRUE);

    return 0;


}

运行结果





  hTimer = CreateWaitableTimer(NULL, FALSE, NULL);   第二个参数改为TRUE   手动模式后




每个线程一直都处于可调度状态



可以等待计数器内核对象的适用情况非常的明确,当需要一个定时器在某和时间点唤醒等待线程,或者按一定时间循环唤醒时。



(三)信号量内核对象

信号量内核对象用来对资源进行计数,在需要对资源使用做限制的时候信号量非常有用,在内部它存放了一个当前可用资源数的变量,和一个最大资源数的变量,每个等待线程使用等待函数时,'如果可以资源数大于0,将其值减一并返回,如果可以资源数已经为0则等待函数进入睡眠,可以通过ReleaseSemaphore增加可以用资源数量,对计数访问修改都是使用的原子操作,保证了计数在多线程下不被破坏。


API:


HANDLE WINAPI CreateSemaphore(
  __in_opt  LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
  __in      LONG lInitialCount,  可用数量 
  __in      LONG lMaximumCount,  最大数量
  __in_opt  LPCTSTR lpName
);



BOOL WINAPI ReleaseSemaphore(
  __in       HANDLE hSemaphore,
  __in       LONG lReleaseCount,  增加多少
  __out_opt  LPLONG lpPreviousCount
);


测试代码:


#include <iostream>
#include <stdio.h>
#include <windows.h>
#include <thread>
#include <winbase.h>
#include <string>
#include <random>
#include <vector>


//using namespace std;
using std::cout;
using std::endl;

HANDLE g_hSemaphore;
HANDLE g_hEvent;
std::vector<int> vtrBuffer;

void ThreadForSemaphoreInc()
{
    std::random_device rd;
    while(1)
    {
        if(WaitForSingleObject(g_hEvent,INFINITE) == WAIT_OBJECT_0)
        {
            for(int i = 0; i !=10; ++i)
            {
               vtrBuffer.push_back(rd()%100);
            }
            ReleaseSemaphore(g_hSemaphore,10,NULL);
            SetEvent(g_hEvent);

        }
        cout<<"The buffer has been full"<<endl<<endl;
        Sleep(5*1000);

    }

}
void ThreadForSemaphoreDec()
{
    HANDLE handles[2] = {g_hEvent,g_hSemaphore};
    while(1)
    {
        if(WaitForMultipleObjects(2,handles,TRUE,INFINITE) == WAIT_OBJECT_0)
        {
            cout<<"Decreasing the buffer,the popuped value is "<<vtrBuffer.back()<<endl;
            vtrBuffer.pop_back();
            SetEvent(g_hEvent);
        }

    }
}

int main(int argc, char *argv[])
{
    g_hEvent = CreateEvent(NULL,FALSE,TRUE,NULL);
    if(!g_hEvent) cout<<"CreateEvent failed and the error code is "<<GetLastError()<<endl;

    g_hSemaphore = CreateSemaphore(NULL,0,10,NULL);
    if(!g_hEvent) cout<<"CreateSemaphore failed and the error code is "<<GetLastError()<<endl;

    new std::thread(ThreadForSemaphoreInc);

    new std::thread(ThreadForSemaphoreDec);
    Sleep(1000*1000);


    return 0;
}

运行结果





(四)互斥量对象


互斥量内核对象用来确保一个线程独占对一个资源的访问。互斥量对象包含一个使用计数,线程ID,和一个递归计数。互斥量与关键段的行为完全相同,但互斥量是内核对象,而关键段是用户模式下的同步对象。


互斥量规则:


(1)如果线程ID为0(无效线程ID),那么该互斥量不为任何线程占用,它处于触发在状态


(2)如果线程ID为非零值,那么有一个线程已经占用了该互斥量,它处于未触发在状态


(3)与其他内核对象不同,操作系统对互斥量进行了特殊处理,允许它们违反一些常规的规则。


 为了获得对被保护资源的访问权,线程要调用等待函数并传入互斥量句柄。在内部,等待函数会检查线程ID是否为0,如果为0,等待线程将互斥量对象线程ID设为当前线程

ID,递归计数为1。否则,主调线程将会被挂起。当其他线程完成对保护资源的互斥访问,释放对互斥量的占有时,互斥量的线程ID被设为0,原来被挂起的线程变为可调度状态,并将互斥量对象对象ID设为此线程ID,递归计数为1。
 
前面一直提到递归计数,却没有解释它的意思。当线程试图等待一个未触发的互斥量对象,此时通常处于等待状态。但是系统会检查想要获得互斥量的线程的线程ID与互斥量对象内部记录的线程ID是否相同。如果相同,那么系统会让线程保持可调度状态,即使该互斥量尚未触发。每次线程等待成功一个互斥量,互斥对象的递归计数就会被设为1。因此,使递归对象大于1 的唯一途径是让线程多次等待同一个互斥量。
当目前占有互斥量的线程不再需要访问互斥资源时,它必须调用ReleaseMutex来释放互斥量。



API:


HANDLE WINAPI CreateMutex(
  __in_opt  LPSECURITY_ATTRIBUTES lpMutexAttributes,
  __in      BOOL bInitialOwner,
  __in_opt  LPCTSTR lpName
);


BOOL WINAPI ReleaseMutex(
  __in  HANDLE hMutex
);


(五)各种同步对象的概括









每种同步内核对象都有个OpenXX版本的函数,用来打开已经创建过的对象,如OpenMutex,OpenEvent,


每种创建同步内核对象的函数都有一个对应的后缀为Ex,的版本  如CreateSemaphoreEx,CreateEventEx,  带后缀的版本多了一个__in      DWORD dwDesiredAccess参数,用来设置创建的对象可以用什么访问权限来使用。

每种同步内核对象都是可以具名的,所有的内核对象名字同在一个命名空间内,如果要创建的对象名字已存在,创建函数失败,GetLastError 返回ERROR_ALREADY_EXISTS


每种同步内核对象在不使用的时候都要调用CloseHandle关闭,不然会造成内核对象泄露。


每种同步内核对象都可以拥有不同进程间的线程同步,可以使用DuplicationHandle,句柄继承,打开同名对象的方式,让两个进程中各自的线程引用同一个内核对象来完成同步工作。




(六)其他等待函数

DWORD WaitForInputIdle(
  HANDLE hProcess,       // handle to process
  DWORD dwMilliseconds   // time-out interval
);
用于等待一个进程,第一次创建窗口。



DWORD WINAPI MsgWaitForMultipleObjects(
  __in  DWORD nCount,
  __in  const HANDLE *pHandles,
  __in  BOOL bWaitAll,
  __in  DWORD dwMilliseconds,
  __in  DWORD dwWakeMask
);

线程在等待内核对象变为有信号的同时,如果有窗口消息到来,线程也可以变为可调度状态。



BOOL WaitForDebugEvent(
  LPDEBUG_EVENT lpDebugEvent,  // debug event information
  DWORD dwMilliseconds         // time-out value
);

被调试进程有调试事件时触发,否则一直等待



DWORD WINAPI SignalObjectAndWait(
  __in  HANDLE hObjectToSignal,
  __in  HANDLE hObjectToWaitOn,
  __in  DWORD dwMilliseconds,
  __in  BOOL bAlertable
);


将一个对象置为有信号的同时,等待另一个对象,原子操作在内部。




  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值