zoukankan      html  css  js  c++  java
  • 第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)计数器的用法

      ①利用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程序】线程可警告示例程序

    #include <windows.h>
    #include <tchar.h>
    
    VOID CALLBACK APCFunc(ULONG_PTR dwParam)
    {
        int i = dwParam;
        _tprintf(_T("%d APC Function Run!
    "),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队列的执行

    #include <windows.h>
    #include <tchar.h>
    #include <locale.h>
    
    DWORD WINAPI ThreadProc(PVOID pvParam)
    {
        HANDLE  hEvent = (HANDLE)pvParam;
    
        _tprintf(_T("线程函数正在等待中...
    "));
        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_COMPLETION
    "));
            break;
        case WAIT_FAILED:
            _tprintf(_T("调用等待函数失败:%u
    "),GetLastError());
            break;
        }
    
        _tprintf(_T("线程函数运行结束!"));
        return 0;
    }
    
    VOID CALLBACK APCFunc(ULONG_PTR dwParam)
    {
        int i = dwParam;
        _tprintf(_T("%d APC Func Run!
    "), 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)

    #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("创建绝对时间的计时器对象
    "));
        CreateTimer1();
    
        _tprintf(_T("
    "));
    
        _tprintf(_T("创建相对时间的计时器对象
    "));
        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次)
    "));
    
        //设置开始时间
        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("计时器出错了
    "));
                CancelWaitableTimer(hTimer);
                CloseHandle(hTimer);        
            }
            else
            {
                GetLocalTime(&st);
                //3秒钏到达,获取系统时间
                _tprintf(_T("3秒到了,第%d次触发。系统时间为:%d-%d-%d %d:%d:%d
    "), 
                         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:%d
    "),
                 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("计时器出错了
    "));
            CancelWaitableTimer(hTimer);
            CloseHandle(hTimer);
        } else
        {
            GetLocalTime(&st);
            //3秒钏到达,获取系统时间
            _tprintf(_T("5秒到了,系统时间为:%d-%d-%d %d:%d:%d
    "),
                    st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
        }
        CancelWaitableTimer(hTimer);
        CloseHandle(hTimer);
        return 0;
    }

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

    #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:%d
    "),
                 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("计时器出错了
    "));
                CancelWaitableTimer(hTimer);
                CloseHandle(hTimer);
            } else
            {
                GetLocalTime(&st);
                //3秒钏到达,获取系统时间
                _tprintf(_T("5秒到了,第%d次触发。系统时间为:%d-%d-%d %d:%d:%d
    "),
                         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:%d
    "),
                 st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
    }
  • 相关阅读:
    sqlserver日期推算
    bcp sqlserver 导入 导出 数据
    sqlserver 2008 序列号
    mysql 学习总结
    使用Eclipse对JUnit测试函数进行Debug时断点无效问题
    ORACLE死锁故障排查的一般性手法的备忘录
    BigDecimal进行Format时产生的[java.lang.IllegalArgumentException: Digits < 0]异常
    Java中keySet()返回值的排序问题
    严蔚敏数据结构视频教程下载
    C、C++经典书籍
  • 原文地址:https://www.cnblogs.com/5iedu/p/4739798.html
Copyright © 2011-2022 走看看