zoukankan      html  css  js  c++  java
  • 多线程编程(16) 多线程同步之 WaitableTimer (等待定时器对象)[续二]


    喝酒醉了一天, 重装系统一天, 两天没上博客了; 继续学习...

    想过没有? WaitableTimer 是在 "定时等待", 前面例子中的 WaitForSingleObject 等待函数 "也在等待", 这就 "双重等待" 了, 这不好, 太浪费资源.

    其实作为同步工具, 前面的几种方法(事件、信号、临界区)基本够用了; WaitableTimer 的作用并不是为了重复前面的功能, 它的主要功用类似 TTimer 类; 譬如每隔多长时间执行一段代码、或在指定的时间去执行一段代码.

    既然有了方便的 TTimer, 何必再使用 WaitableTimer 呢?
    因为 WaitableTimer 比 TTimer 精确的多, 它的间隔时间可以精确到毫秒、它的指定时间甚至是精确到 0.1 毫秒;
    而 TTimer 驱动的 WM_TIMER 消息, 是消息队列中优先级最低的, 也就是再同一时刻 WM_TIMER 消息总是被最后处理.
    还有重要的一点 WaitableTimer 可以跨线程、跨进程使用.

    继续探讨一个重要的点: 很多时候为了让线程不冲突, 线程也在等待, 既然有等待, 那 WaitableTimer 非常精确的定时又有什么价值呢? 对这个问题的思考, 可以让我们很好地理解 APC 函数.

    SetWaitableTimer 有个回调函数(其实是个过程), Windows 要求它的格式是:
    procedure TimerAPCProc(
      lpArgToCompletionRoutine: Pointer;
      dwTimerLowValue: DWORD;
      dwTimerHighValue: DWORD
    ); stdcall;
    

    函数名中有 APC 的字样, 指示这是个 APC 函数(尽管这个名称无所谓, 这是官方命名), 那什么是 APC 函数?
    APC(Asyncroneus Procedure Call): 异步过程调用.

    原来每个线程除了有单独的消息队列, 还有一个 APC 队列(等待执行的 APC 函数); 如果线程发现 APC 队列中有情况, 马上会跳过去执行, 执行完毕后才回来接着处理消息队列.

    说起来麻烦, 使用的时候只按上面格式传入函数指针就行; 不过能进入 APC 队列的回调函数和其他回调函数还有一个很大的不同:
    SetWaitableTimer 按格式调用 APC 函数后, 需要在 "当前线程" 见到一个 "等待", 此 APC 函数才可以进入队列.
    这好像很费解, 例说一下: APC 队列有那么高的优先级, 因为对资源的优先使用会对其他消息有很大的影响, 肯定不能随便进入, 这是不是像生活中的贵宾席或贵宾通道?
    也就是说, 要进入 APC 队列只有 SetWaitableTimer 的调用还不够, 还要通过 "等待函数" 介绍一下.

    WaitForSingleObject 吗? 不是, 它不够级别; 下面是 Windows 认可的、可以介绍 APC 入列的等待函数:
    SleepEx();
    WaitForSingleObjectEx();
    WaitForMultipleObjectsEx();
    MsgWaitForMultipleObjectsEx();
    SignalObjectAndWait();
    

    为什么是用等待函数来把关? 因为上面几个等待函数也可以等待是否有 APC 函数想入列.

    上面给出的几个等待函数, 就 SleepEx 的参数最少, 先用它吧:
    function SleepEx(
      dwMilliseconds: DWORD; {毫秒数}
      bAlertable: BOOL       {布尔值}
    ): DWORD; stdcall;
    
    //第一个参数和 Sleep 的那个参数是一样的, 是线程等待(或叫挂起)的时间, 时间一到不管后面参数如何都会返回.
    
    //第二个参数如果是 False, SleepEx 将不会关照 APC 函数是否入列;
    //若是 True, 只要有 APC 函数申请, SleepEx 不管第一个参数如何都会把 APC 推入队列并随 APC 函数一起返回.
    
    //注意: SetWaitableTimer 和 SleepEx 必须在同一个线程才可以.
    

    本例效果图:



    代码文件:
    unit Unit1;
    
    interface
    
    uses
      Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
      Dialogs, ExtCtrls, StdCtrls;
    
    type
      TForm1 = class(TForm)
        Button1: TButton;
        procedure Button1Click(Sender: TObject);
        procedure FormDestroy(Sender: TObject);
      end;
    
    var
      Form1: TForm1;
    
    implementation
    
    {$R *.dfm}
    
    var
      hTimer: THandle;
    
    {APC 函数(过程), 函数名和参数名可以不同, 格式必须如此}
    procedure TimerAPCProc(lpArgToCompletionRoutine: Pointer; dwTimerLowValue: DWORD;
      dwTimerHighValue: DWORD); stdcall;
    begin
      Form1.Text := IntToStr(StrToIntDef(Form1.Text, 0) + 1); {标题 + 1}
    end;
    
    procedure TForm1.Button1Click(Sender: TObject);
    var
      DueTime: Int64;
    begin
      hTimer := CreateWaitableTimer(nil, True, nil);
      DueTime := 0;
      if SetWaitableTimer(hTimer, DueTime, 0, @TimerAPCProc, nil, False) then
      begin
        SleepEx(INFINITE, True); {INFINITE 表示一直等}
      end;
    end;
    
    procedure TForm1.FormDestroy(Sender: TObject);
    begin
      CloseHandle(hTimer);
    end;
    
    end.
    

    窗体文件:
    object Form1: TForm1
      Left = 0
      Top = 0
      Caption = 'Form1'
      ClientHeight = 113
      ClientWidth = 203
      Color = clBtnFace
      Font.Charset = DEFAULT_CHARSET
      Font.Color = clWindowText
      Font.Height = -11
      Font.Name = 'Tahoma'
      Font.Style = []
      OldCreateOrder = False
      PixelsPerInch = 96
      TextHeight = 13
      object Button1: TButton
        Left = 64
        Top = 48
        Width = 75
        Height = 25
        Caption = 'Button1'
        TabOrder = 0
        OnClick = Button1Click
      end
    end
    

    在上面例子中, 每点一次鼠标, 那个回调函数才执行一次; 作为定时器, 如果想让它每秒执行一次怎么弄?

    但每一次执行那个 APC 函数, 都得有 SleepEx(当然不止它)给送进去, 那这样得反复调用 SleepEx 才可以.
    怎么调用, 用循环吗? 别说网上能找到的例子我没见到不用循环的(太笨了), 就在那个 APC 函数里调用不就完了.

    当然这时一般要设时间间隔的, 下面我们将设间隔为 1000(1秒).

    但接着问题又来了, 譬如把代码修改成:
    var
      hTimer: THandle;
    
    procedure TimerAPCProc(lpArgToCompletionRoutine: Pointer; dwTimerLowValue: DWORD;
      dwTimerHighValue: DWORD); stdcall;
    begin
      Form1.Text := IntToStr(StrToIntDef(Form1.Text, 0) + 1);
      SleepEx(INFINITE, True); {这里再次调用 SleepEx}
    end;
    
    procedure TForm1.Button1Click(Sender: TObject);
    var
      DueTime: Int64;
    begin
      hTimer := CreateWaitableTimer(nil, True, nil);
      DueTime := 0;
      {下面的参数 1000 表示间隔 1秒}
      if SetWaitableTimer(hTimer, DueTime, 1000, @TimerAPCProc, nil, False) then
      begin
        SleepEx(INFINITE, True);
      end;
    end;
    
    procedure TForm1.FormDestroy(Sender: TObject);
    begin
      CloseHandle(hTimer);
    end;
    

    任务能完成, 但窗体"死"了... 怎么办? 嘿, 现在学的不是多线程吗?

    下面例子中, 同时使用了 CancelWaitableTimer 来取消定时器, 很好理解; 效果图:



    代码文件:
    unit Unit1;
    
    interface
    
    uses
      Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
      Dialogs, ExtCtrls, StdCtrls;
    
    type
      TForm1 = class(TForm)
        Button1: TButton;
        Button2: TButton;
        procedure Button1Click(Sender: TObject);
        procedure Button2Click(Sender: TObject);
        procedure FormDestroy(Sender: TObject);
      end;
    
    var
      Form1: TForm1;
    
    implementation
    
    {$R *.dfm}
    
    var
      hTimer: THandle;
    
    {APC 函数}
    procedure TimerAPCProc(lpArgToCompletionRoutine: Pointer; dwTimerLowValue: DWORD;
      dwTimerHighValue: DWORD); stdcall;
    begin
      Form1.Text := IntToStr(StrToIntDef(Form1.Text, 0) + 1);
      SleepEx(INFINITE, True);
    end;
    
    {线程入口函数}
    function MyThreadFun(p: Pointer): Integer; stdcall;
    var
      DueTime: Int64;
    begin
      DueTime := 0;
      {SetWaitableTimer 必须与 SleepEx 在同一线程}
      if SetWaitableTimer(hTimer, DueTime, 1000, @TimerAPCProc, nil, False) then
      begin
        SleepEx(INFINITE, True);
      end;
      Result := 0;
    end;
    
    procedure TForm1.Button1Click(Sender: TObject);
    var
      ID: DWORD;
    begin
      {建立 WaitableTimer 对象}
      if hTimer = 0 then  hTimer := CreateWaitableTimer(nil, True, nil);
      CreateThread(nil, 0, @MyThreadFun, nil, 0, ID); {建立线程}
    end;
    
    procedure TForm1.Button2Click(Sender: TObject);
    begin
      CancelWaitableTimer(hTimer); {取消定时器}
    end;
    
    procedure TForm1.FormDestroy(Sender: TObject);
    begin
      CloseHandle(hTimer);
    end;
    
    end.
    

    窗体文件:
    object Form1: TForm1
      Left = 0
      Top = 0
      Caption = 'Form1'
      ClientHeight = 113
      ClientWidth = 203
      Color = clBtnFace
      Font.Charset = DEFAULT_CHARSET
      Font.Color = clWindowText
      Font.Height = -11
      Font.Name = 'Tahoma'
      Font.Style = []
      OldCreateOrder = False
      PixelsPerInch = 96
      TextHeight = 13
      object Button1: TButton
        Left = 55
        Top = 32
        Width = 97
        Height = 25
        Caption = #21551#21160#23450#26102#22120
        TabOrder = 0
        OnClick = Button1Click
      end
      object Button2: TButton
        Left = 55
        Top = 63
        Width = 97
        Height = 25
        Caption = #21462#28040#23450#26102#22120
        TabOrder = 1
        OnClick = Button2Click
      end
    end
    

    使用 APC 回调函数才是 WaitableTimer 的正途, 下次该是如何给这个函数传递参数了.

  • 相关阅读:
    I
    H
    G
    F
    E
    论js里面的for循环
    js常见问题之为什么点击弹出的i总是最后一个
    array类型的方法
    string类型的方法
    for in在对象和数组中的应用
  • 原文地址:https://www.cnblogs.com/del/p/1393553.html
Copyright © 2011-2022 走看看