1

I have a small problem with a waitable timer object when I use with absolute time and waitformultipleobjects ().

Once the timer gets signaled I need to reset the object, and the only way I can find is by using setwaitabletimer again with a unrealistic due time in the future. That'll keep the timer active in the OS waketimers for no reason at all. I need to keep the handle because I need to reactivate the timer in some situations and I use the timer object in the waitformultipleobjects array, so closing the FWaitTimer handle will not be good.

Do I understand this correct or is there a better way to do this?

My full code looks like this, please disregard the other waitfor objects, it's only the FWaitTimer object.

unit frmMain;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ComCtrls, Vcl.WinXPickers, cxGraphics, cxControls, cxLookAndFeels, cxLookAndFeelPainters, dxSkinsCore,
  dxSkinTheAsphaltWorld, dxSkinsDefaultPainters, cxCalendar, Vcl.ExtCtrls, cxContainer, cxEdit, dxCore, cxDateUtils, cxDropDownEdit, cxTextEdit, cxMaskEdit,
  cxSpinEdit, cxTimeEdit, Vcl.Menus, Vcl.StdCtrls, cxButtons, dateutils, syncObjs, UTimer;

const
  WM_WATINGDONE = WM_USER + 100;

type

  TMyWaitThread = class(TThread)
  private
    FWaitTimer : THandle;
    FTerminateEvent : TEvent;
    FPeriodicTimer : TADTimer;
    FWaitTime: TDateTime;
    procedure SetWaitTime(const Value: TDateTime);
    property WaitTime : TDateTime read FWaitTime write SetWaitTime;
  public
    constructor Create; overload;
    constructor Create(CreateSuspended: Boolean); overload;
    destructor Destroy; override;
    procedure execute; override;
    procedure Terminate;
  end;



  TForm1 = class(TForm)
    cxClock1: TcxClock;
    Timer1: TTimer;
    cxDateEdit1: TcxDateEdit;
    btnWait: TcxButton;
    procedure Timer1Timer(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure btnWaitClick(Sender: TObject);
  private
    FMyWaitThread : TMyWaitThread;
    Fwaiting: Boolean;
    procedure WM_WAITINGDONE(var msg : TMessage); message WM_WATINGDONE;
    procedure Setwaiting(const Value: Boolean);
    { Private declarations }
    property waiting : Boolean read Fwaiting write Setwaiting;
    procedure WatingDone;


  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.btnWaitClick(Sender: TObject);
var
  dt : TDateTime;
begin
  dt := cxDateEdit1.EditValue;
  FMyWaitThread.SetWaitTime(dt);
  waiting := True;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  cxClock1.Time := now;
  Timer1.Enabled := True;
  waiting := False;

  FMyWaitThread := TMyWaitThread.Create;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  FMyWaitThread.Terminate;
  FreeAndNil(FMyWaitThread);
end;

procedure TForm1.Setwaiting(const Value: Boolean);
begin
  Fwaiting := Value;
  case Fwaiting of
    True : begin
      btnWait.Enabled := False;
    end;

    false : begin
      btnWait.Enabled := True;
      cxDateEdit1.EditValue := IncMinute(Now, 1);
    end;
  end;
end;

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  cxClock1.Time := now;
end;

procedure TForm1.WatingDone;
begin
  waiting := False;
end;

procedure TForm1.WM_WAITINGDONE(var msg: TMessage);
begin
  waiting := False;
end;

{ TMyWaitThread }

function GetGUID : string;
var
  uid : TGUID;
  r : Integer;
begin
  Result := '';
  r := CreateGuid(Uid);
  if r = S_OK then
  begin
    Result := StringReplace(GuidToString(Uid),'{', '', [rfReplaceAll]);
    Result := StringReplace(result, '}', '', [rfReplaceAll]);
  end;
end;

constructor TMyWaitThread.Create(CreateSuspended: Boolean);
begin
  inherited Create(CreateSuspended);
  FWaitTimer := CreateWaitableTimer(nil, True, PWideChar(GetGUID)); // change the 'MatrixTimer' to something unique...
  FTerminateEvent := TEvent.Create;
  FPeriodicTimer := TADTimer.Create;
  FPeriodicTimer.Interval := 10000;
end;

destructor TMyWaitThread.Destroy;
begin
  FPeriodicTimer.StopTimer;
  FreeAndNil(FPeriodicTimer);
  FreeAndNil(FTerminateEvent);
  CloseHandle(FWaitTimer);
  inherited;
end;

procedure TMyWaitThread.execute;
var
  EventArr : array of THandle;
begin
  SetLength(EventArr, 3);

  EventArr[0] := FTerminateEvent.Handle;
  EventArr[1] := FWaitTimer;
  EventArr[2] := FPeriodicTimer.Handle;

  while not Terminated do
  begin
    try
      case WaitForMultipleObjects(Length(EventArr), @EventArr[0], False, INFINITE) of

        WAIT_OBJECT_0 : begin // terminate
          OutputDebugString('Terminating.....');
          Exit;
        end;

        WAIT_OBJECT_0+1 : begin // wait timer
          OutputDebugString('Wait timer was triggered');

          WaitTime := IncMinute(now, 1);

          // How do I reset the FWaitTimer - I would like to keep the FWaitTimer handle so closing it is no good.

          PostMessage(Form1.Handle, WM_WATINGDONE, 0, 0);
        end;

        WAIT_OBJECT_0+2 : begin // periodic timer
          OutputDebugString('Periodic timer was triggered');
        end;

      end;

    except on E: Exception do
      // keep any exceptions inside the loop
    end;
  end;
end;

procedure TMyWaitThread.SetWaitTime(const Value: TDateTime);
var
  WakeUpTime: LARGE_INTEGER;
  SysTime : _SystemTime;
  FTime : _FileTime;
begin
  FWaitTime := Value;
  DateTimeToSystemTime(FWaitTime, SysTime);
  SystemTimeToFileTime(SysTime, FTime);
  LocalFileTimeToFileTime(FTime, FTime);
  WakeUpTime.LowPart := FTime.dwLowDateTime;
  WakeUpTime.HighPart := FTime.dwHighDateTime;
  SetWaitableTimer(FWaitTimer, WakeUpTime.quadpart, 0, nil, nil, True);
end;

procedure TMyWaitThread.Terminate;
begin
  FTerminateEvent.SetEvent;
  inherited;
end;

constructor TMyWaitThread.Create;
begin
  Create(False);
end;

end.
hhaumann
  • 272
  • 2
  • 19
  • Possible duplicate of [Resetting Waitable Timers](https://stackoverflow.com/questions/251189/resetting-waitable-timers) – whosrdaddy Jun 04 '19 at 11:03
  • 2
    you need dynamic change objects in your array, for which you wait. after timer is signaled - remove it from array which you pass to `WaitForMultipleObjects` or `MsgWaitForMultipleObjectsEx`. when it need again - again include it to array – RbMm Jun 04 '19 at 13:16
  • 1
    the easist way in your case put the `FWaitTimer` at the 2(last) place in array - `EventArr[2] := FWaitTimer` and pass 3 or 2 as first argument to `WaitForMultipleObjects` in your code.very easy and clean solution – RbMm Jun 04 '19 at 13:22

1 Answers1

3

Quote from the documentation (emphasis mine):

The thread calls the SetWaitableTimer function to activate the timer. Note the use of the following parameters for SetWaitableTimer: Use the lpDueTime parameter to specify the time at which the timer is to be set to the signaled state. When a manual-reset timer is set to the signaled state, it remains in this state until SetWaitableTimer establishes a new due time. When a synchronization timer is set to the signaled state, it remains in this state until a thread completes a wait operation on the timer object.

So the only way is to call SetWaitableTimer again to reset the signaled state of the timer.

There are 2 ways to solve your problem, use the current hack but add CancelWaitableTimer() after SetWaitableTimer() to cancel the timer (it's state will stay unsignaled)

A better solution is not to wait for the timer when it is not in use, Implement as @RbMm suggests:

put the FWaitTimer at the 2(last) place in array - EventArr[2] := FWaitTimer and pass 3 or 2 as first argument to WaitForMultipleObjects

whosrdaddy
  • 11,720
  • 4
  • 50
  • 99
  • Ok, thanks both of you, I understand the two options here and will probably choose "current hack" as you express it :) In future project I will see if I can redesign for the "better" solution. – hhaumann Jun 07 '19 at 09:49