第9章 用内核对象进行线程同步(2)_可等待计时器(WaitableTimer)

9.4 可等待的计时器内核对象——某个指定的时间或每隔一段时间触发一次

(1)创建可等待计时器:CreateWaitableTimer(使用时应把常量_WIN32_WINNT定义为0x0400

参数

描述

psa

安全属性(如使用计数、句柄继承等)

bManualReset

手动重置计时器还是自动重置计时器。

①当手动计时器被触发所有正在等待计时器的线程都变可为可调度。

②当自动计时器被触发时只有一个正在等待计数器的线程变为可调度

pszName

对象的名字

(2)也可以打开一个己经存在的可等待计时器:OpenWaitableTimer

(3)设置可等待计时器状态:SetWaitableTimer

参数

描述

HANDLE hTimer

要想触发的计时器

LARGE_INTEGER*  pDueTime

计时器第1次被触发的时间(应该为世界协调时UTC

说明:pDueTime为正数时是个绝对时间为负数时,表示一个相对时间,表示要在相对于调用该函数以后多少个(100ns)毫秒应第1次触发计时器。如5秒后,则应为

-5*10 000 000

LONG lPeriod

第一次触发后,每隔多少时触发一次(单位是微秒)。

如果希望计时器只触发一次,之后不再触后,该参数为0.

PTIMERAPCROUTINE pfnCR

要加入APC队列的回调函数

PVOID pvArgToCR

传给回调函数的额外参数

BOOL bResume

如果为TRUE,而且系统支持电源管理,那么在计时器触发的时候,系统会退出省电模式。如设为TRUE,但系统不支持省电模式,GetLastError就会返回ERROR_NOT_SUPPORTED 适用平台。一般设为FALSE

(4)取消计时器:CancelWaitableTimer,调用后计时器永远不会触发。

(5)计数器的用法

第9章 用内核对象进行线程同步(2)_可等待计时器(WaitableTimer)

  ①利用CreateWaitableTimer创建计时器对象

  ②调用SetWaitableTimer来指定计时器首次触发及以后触发的时间间隔等。

  ③调用等待函数将调用线程挂起,直到计时器对象被触发。

  ④最后使用CloseHandle关闭计时器对象。

【SetWaitableTimer伪代码】——设置计时器在2015年8月18日14:00触发,以后每隔6小时触发一次

HANDLE hTimer;
SYSTEMTIME st = { 0 };
FILETIME ftLocal, ftUTC;
LARGE_INTEGER liUTC;

//创建一个自动的计时器对象
hTimer = CreateWaitableTimer(NULL, FALSE, NULL);

//首先,设置时间为2015年8月18日,14:00(本地时间)
st.wYear = 2015;
st.wMonth = 8;
st.wDay = 18;
st.wHour = 14;  //PM格式的
st.wMinute = 0;
st.wSecond = 0;
st.wMilliseconds = 0;

SystemTimeToFileTime(&st, &ftLocal);

//将本地时间转为UTC时间
LocalFileTimeToFileTime(&ftLocal, &ftUTC);

//将FILETIME转为LARGE_INTEGER(因为对齐方式不同)
//FILETIME结构的地址必须是4的整数倍(32位边界),
//而LARG_INTEGER结构的地址必须是8的整数倍(64位边界)
liUTC.LowPart = ftUTC.dwLowDateTime;
liUTC.HighPart = ftUTC.dwHighDateTime;

//设置计时器
//SetWaitableTimer传入的时间始终是UTC时间(这个时间必须是64位边界)
//在liUTC后,每隔6个小时触发一次
SetWaitableTimer(hTimer, &liUTC, 6 * 60 * 60 * 1000, NULL, NULL, FALSE);

9.4.1 线程APC队列及线程的可警告状态

(1)线程可警告状态

  ①通过一些方法让线程假“暂停”(注意此时线程仍被分配CPU时间片),转而去执行线程APC队列中的函数。线程的这种假“暂停”的状态,被称为“可警告”状态(或“可提醒”状态)。让线程进入可警告状态的方法可通过如调用SleepEx、Wait*Ex函数族。

  ②在线程进入可警告状态前,系统需要保存为线程函数分配的调用栈及各寄存器状态,然后再转向执行APC队列中的函数。

  ③此时对于线程函数来说确实是被暂停执行,进入“等待+可警告”状态只有当APC队列中的所有函数都被执行完毕后线程函数才会被唤醒来继续执行(实际上线程并未真正睡眠,只是被中断去执行APC函数,完毕又回来到该线程函数中来)

(2)线程APC队列(异步过程调用)

  ①线程APC队列其实就是为线程在线程函数之外,再安排一组函数去执行,本质上是利用线程实质是函数调用器的性质。

  ②默认情况下,创建线程时不会创建这个队列,但当调用了QueueUserAPC函数或其他可向APC队列添加实体的函数后,才会创建APC这个队列。

  ③可以用Wait函数族或SleepEx函数并传入bAlterable为TRUE,让线程进入一种假“暂停”的状态。这时系统调度器会转向调用线程APC队列中的函数

  ④需要注意的是有些函数也会使线程进入“等待状态”(没CPU时间片),但不是可警告状态。这时,线程不会转向执行APC队列。一般这些函数中没有bAlterable这个参数)

  ⑤不要在APC函数中调用让线程进入Alterable状态的API,这会引起递归,而导致线程栈溢出。

【ThreadAlterable程序】线程可警告示例程序

第9章 用内核对象进行线程同步(2)_可等待计时器(WaitableTimer)

#include <windows.h>
#include <tchar.h>

VOID CALLBACK APCFunc(ULONG_PTR dwParam)
{
    int i = dwParam;
    _tprintf(_T("%d APC Function Run!n"),i);
    Sleep(20);
}

int _tmain()
{
    //为主线程添加APC函数
    for (int i = 0; i < 100;i++){
        QueueUserAPC(APCFunc, GetCurrentThread(), i);
    }

    //SleepEx会让主线程进入一种假“暂停”状态。实际上,系统调度器只是暂停主线程函数本身,转而去执行线程中APC队列中的函数而己。
    //但主线程实际上没有暂停,只是时间片给了,APC队列中的函数,而不是线程函数而己。这种状态叫“等待+可警告状态”。当该句注释后,
    //因线程没有进入可警告状态,所以APC队列中的函数并不会被执行。
    SleepEx(INFINITE, TRUE);
    _tsystem(_T("PAUSE"));
    return 0;
}

【ThreadAPC程序】演示线程APC队列的执行

第9章 用内核对象进行线程同步(2)_可等待计时器(WaitableTimer)

#include <windows.h>
#include <tchar.h>
#include <locale.h>

DWORD WINAPI ThreadProc(PVOID pvParam)
{
    HANDLE  hEvent = (HANDLE)pvParam;

    _tprintf(_T("线程函数正在等待中...n"));
    Sleep(100);
    //使该线程假挂起(仍然分配CPU时间片),只是这时
    //不并执行该线程函数(给人造成线程暂停的假象),实际上这时
    //该线程仍在执行,只是转向去执行队列APC中的回调函数罢了。
    //该函数会在hEvent被触发,或APC队列执行完毕后返回
    DWORD dw = WaitForSingleObjectEx(hEvent, INFINITE, TRUE); //可警告状态
    switch (dw)
    {
    case  WAIT_OBJECT_0:
        _tprintf(_T("线程函数:事件触发!"));
        break;

    case  WAIT_IO_COMPLETION:
        //如果线程至少处理了APC队列中的一项
        _tprintf(_T("线程函数:APC队列中APC函数执行完,等待函数返回WAIT_IO_COMPLETIONn"));
        break;
    case WAIT_FAILED:
        _tprintf(_T("调用等待函数失败:%un"),GetLastError());
        break;
    }

    _tprintf(_T("线程函数运行结束!"));
    return 0;
}

VOID CALLBACK APCFunc(ULONG_PTR dwParam)
{
    int i = dwParam;
    _tprintf(_T("%d APC Func Run!n"), i);
    Sleep(20); //该线程真正被挂起(不再分配CPU时间片)
}

int _tmain()
{
    _tsetlocale(LC_ALL, _T("chs"));

    //创建自动、未触发的事件对象(即每次只唤醒一个线程)
    HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);

    //创建线程
    HANDLE hThread = CreateThread(NULL, 0, ThreadProc, hEvent, 0, NULL);

    Sleep(1000); //主线程休眠,以便子线程进入可警告状态

    //在子线程中加入APC队列函数
    for (int i = 0; i < 20; i++){
        QueueUserAPC(APCFunc, hThread, i);
    }

    //等待子线程结束
    WaitForSingleObject(hThread, INFINITE);

    CloseHandle(hThread);
    CloseHandle(hEvent);
    _tsystem(_T("PAUSE"));
    return 0;
}

9.4.2 利用可等待计时器将APC函数添加到APC队列中

(1)计时器时间一到,会触发计时器对象。但也可以在时间到时把一个APC添加到队列中。

(2)计时器APC回调函数的原型

   VOID APIENTRY TimerAPCRoutine(PVOID pvArgToCompletionRoutine,DWORD dwTimerLowValue,DWORD dwTimerHighValue);

  ①函数名可随意,这里取名为TimerAPCRoutine。后面两个参数系统会传入计时器被触发时的时间UTC时间)。

  ②可调用SleepEx、WaitForSingleMultipleObjectEx、SignalObjectAndWait、MsgWaitForMultipleObjectEx等函数让线程进入可警告状态。当线程进入可警告状态时,会转去执行APC队列中的函数。换句话说就是,只有当线程被设为可警告状态,加入到APC队列中的函数才是可被回调的。

  ③当计时器被触发并且线程处于可警告状态时,系统会让线程调用回调函数。

(3)注意事项

  ①当计时器被触发时,会向APC队列添加一个回调函数(如MyAPC),并转向去执行该函数。但由于APC队列的特点,在该函数执行完后,系统会再去检查队列中剩下的其它APC函数。只有当队列中其他函数都执行完毕,这个MyAPC函数才会返回。因此,必须确保TimerAPCRoutine函数在计时器再次被触发之前结束,否则函数加入队列的速度会超过处理它们的速度。

  ②当线程调用Wait*或SleepEx函数而进入等待状态时,这些等待函数会在下列几种情况中返回:A、等待函数所等待的对象被触发(返回值为WAIT_OBJECT_x之类);B、APC队列中所有函数执行完毕(返回值为WAIT_IO_COMPLETION);C、等待超时(返回值为WAIT_TIMEOUT);D、在调用MsgWaitForMutipleObjectsEx时,一个消息进入到线程的消息队列时。因此,下列的调用是错误的:

HANDLE hTimer = CreateWaitableTimer(NULL,FALSE,NULL);

SetWaitableTimer(hTimer,…,TimerAPCRoutine,…);

//当计时器触发时,内核对象hTimer被设为有信号,Wait*函数会返回,线程被“唤醒”,使得线程退出可警告状态,因此,APC函数无法被调用!
WaitForSingleObjectEx(hTimer,INFINITE,TRUE); //不能在等待计时器的同时,再以可警告来等待。

9.4.3 计时器的剩余问题

   ①用户计时器是通用SetTimer来设置的,一般会发送WM_TIMER给调用SetTimer的线程(对于回调计时器来说)或者窗口过程(对基于窗口的计时器来说),因此只有一个线程可以收到通知。而“手动重置”的等待计时器可以让多个线程同时收到通知。

  ②等待计时器可以让线程到了规定的时间就收到通知。而用户计时器,发送的WM_TIMER消息属于最低优先级的消息,当线程队列中没有其他消息的时候才会检索该消息,因此可能会有一点延迟,甚至有的消息会被丢弃。

  ③WM_TIMER消息的定时精度比较低,没有等待计时器那么高。

  ④用户计时器需要使用大量的用户界面设施,从而消耗更多的资源。等待计时器是内核对象,不仅可以在多线程间共享,而且可以具备安全性。

 【Timer(NonAPC)程序】可等待计时器(非APC)

第9章 用内核对象进行线程同步(2)_可等待计时器(WaitableTimer)

#include <windows.h>
#include <tchar.h>
#include <locale.h>
#include <time.h>

int  CreateTimer1();
int  CreateTimer2();

int _tmain()
{
    _tsetlocale(LC_ALL, _T("chs"));

    _tprintf(_T("创建绝对时间的计时器对象n"));
    CreateTimer1();

    _tprintf(_T("n"));

    _tprintf(_T("创建相对时间的计时器对象n"));
    CreateTimer2();

    _tsystem(_T("PAUSE"));
    return 0;
}

//创建绝对时间的计时器
int CreateTimer1()
{
    //声明局部变量
    HANDLE hTimer;
    SYSTEMTIME st = { 0 };
    FILETIME ftLocal, ftUTC;
    LARGE_INTEGER liUTC;

    //创建计时器对象
    hTimer = CreateWaitableTimer(NULL, FALSE, NULL);
    if (!hTimer)
        return -1;

    _tprintf(_T("第一次触发时间为2015-8-19 08:30:00,并每隔3秒报时一次(循环3次)n"));

    //设置开始时间
    st.wYear = 2015;
    st.wMonth = 8;
    st.wDay = 5;
    st.wHour = 8;
    st.wMinute = 30;
    st.wSecond = 0;
    st.wMilliseconds = 0;

    //系统时间转换文件时间
    SystemTimeToFileTime(&st, &ftLocal);

    //本地时间转换为UTC时间
    LocalFileTimeToFileTime(&ftLocal, &ftUTC);

    //转换FILETIME为LLARGE_INTEGER,为了变量对齐边界
    liUTC.LowPart = ftUTC.dwLowDateTime;
    liUTC.HighPart = ftUTC.dwHighDateTime;

    //设置计时器
    if (!SetWaitableTimer(hTimer,&liUTC,3*1000,NULL,NULL,FALSE)){
        CloseHandle(hTimer);
        return -1;
    }

    //等待计时器触发
    for (int i = 0; i < 3;i++){
        if (WaitForSingleObject(hTimer,INFINITE)!=WAIT_OBJECT_0){
            _tprintf(_T("计时器出错了n"));
            CancelWaitableTimer(hTimer);
            CloseHandle(hTimer);        
        }
        else
        {
            GetLocalTime(&st);
            //3秒钏到达,获取系统时间
            _tprintf(_T("3秒到了,第%d次触发。系统时间为:%d-%d-%d %d:%d:%dn"), 
                     i + 1, st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
        }
    }
    CancelWaitableTimer(hTimer);
    CloseHandle(hTimer);
    return 0;
}

//创建相对时间的计时器
int CreateTimer2()
{
    //声明局部变量
    HANDLE hTimer;
    LARGE_INTEGER liDueTime;
    SYSTEMTIME st = { 0 };

    //设置相对时间为5秒
    liDueTime.QuadPart = -5*10000000;//单位(100ns),设置相对时间时,必须为负数

    //创建计时器对象
    hTimer = CreateWaitableTimer(NULL, TRUE, NULL);
    if (!hTimer)
        return -1;

    GetLocalTime(&st);
    _tprintf(_T("创建5秒计时器,现在系统时间为:%d-%d-%d %d:%d:%dn"),
             st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);

    //设置计时器
    //调用SetWaitableTimer的时候,给第2个参数传递一个相对的时间在值,这个参数必须是
    //负数,表示自调用函数后若干秒后,计时器触发。
    if (!SetWaitableTimer(hTimer, &liDueTime, 0, NULL, NULL, FALSE)){
        CloseHandle(hTimer);
        return -1;
    }

    //等待计时器触发
    if (WaitForSingleObject(hTimer, INFINITE) != WAIT_OBJECT_0){
        _tprintf(_T("计时器出错了n"));
        CancelWaitableTimer(hTimer);
        CloseHandle(hTimer);
    } else
    {
        GetLocalTime(&st);
        //3秒钏到达,获取系统时间
        _tprintf(_T("5秒到了,系统时间为:%d-%d-%d %d:%d:%dn"),
                st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
    }
    CancelWaitableTimer(hTimer);
    CloseHandle(hTimer);
    return 0;
}

 【Timer(APC)程序】可等待计时器(APC)

第9章 用内核对象进行线程同步(2)_可等待计时器(WaitableTimer)

#include <windows.h>
#include <tchar.h>
#include <locale.h>
#include <time.h>

VOID CALLBACK TimerAPCProc(LPVOID lpArgToCompletionRoutine, 
            DWORD dwTimerLowValue,DWORD dwTimerHighValue);

int _tmain()
{
    _tsetlocale(LC_ALL, _T("chs"));

    HANDLE hTimer;
    SYSTEMTIME st;

    //创建一个可等待的计时器对象(手动或自动均可)
    hTimer = CreateWaitableTimer(NULL, TRUE, NULL);
    LARGE_INTEGER liDueTime;
    liDueTime.QuadPart = -5 * 10000000;

    //获得现在系统时间并输出
    GetLocalTime(&st);
    _tprintf(_T("创建5秒计时器,现在系统时间为:%d-%d-%d %d:%d:%dn"),
             st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);

    //设置计时器5秒后触发
    SetWaitableTimer(hTimer, &liDueTime, 5*1000, TimerAPCProc, NULL, TRUE);

    ////线程进入假“暂停”的可警告状态
    //SleepEx(INFINITE, TRUE); //只等待一次触发,该函数就会返回

    //等待计时器触发
    for (int i = 0; i < 3; i++){
        if (WaitForSingleObject(hTimer, INFINITE) != WAIT_OBJECT_0){
            _tprintf(_T("计时器出错了n"));
            CancelWaitableTimer(hTimer);
            CloseHandle(hTimer);
        } else
        {
            GetLocalTime(&st);
            //3秒钏到达,获取系统时间
            _tprintf(_T("5秒到了,第%d次触发。系统时间为:%d-%d-%d %d:%d:%dn"),
                     i + 1, st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
        }
    }

    //关闭可等待计时器对象
    CancelWaitableTimer(hTimer);
    CloseHandle(hTimer);

    _tsystem(_T("PAUSE"));
    return 0;
}

//APC回调函数
VOID CALLBACK TimerAPCProc(LPVOID lpArgToCompletionRoutine,
                           DWORD dwTimerLowValue, DWORD dwTimerHighValue)
{
    FILETIME ftUTC,ftLocal;
    SYSTEMTIME st;

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

    FileTimeToLocalFileTime(&ftUTC, &ftLocal);
    FileTimeToSystemTime(&ftLocal, &st);

    _tprintf(_T("5秒到了,系统时间为:%d-%d-%d %d:%d:%dn"),
             st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
}

 

原文链接: https://www.cnblogs.com/5iedu/p/4739798.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    第9章 用内核对象进行线程同步(2)_可等待计时器(WaitableTimer)

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/394659

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月3日 下午3:52
下一篇 2023年4月3日 下午3:52

相关推荐