* Alternat implementation based on event, from Derek (Bug ID 28831)

git-svn-id: trunk@33298 -
This commit is contained in:
michael 2016-03-20 16:51:27 +00:00
parent 8f9899e5c4
commit 6ab9435e44

View File

@ -23,12 +23,32 @@
A nice improvement would be an implementation that works A nice improvement would be an implementation that works
in all threads, such as the threadedtimer of IBX for linux. in all threads, such as the threadedtimer of IBX for linux.
Replaced SLEEP with TEvent for those platforms supporting threading:
Windows, Linux, BSD.
On the other platforms, use sleep. This unfortunately has a high overhead
resulting in drift. A five minute timer could be up to 40 seconds late
do to entering and returning (linux x64). MOdified to check the absolute
time every minute, has reduced that lag to about 0.100 second. This is
still greater than TEvent, where the delay is only a few milliseconds (0-3).
} }
unit fpTimer; unit fptimer;
{$mode objfpc}{$H+} {$mode objfpc}{$H+}
{
Windows, or any platform that uses Cthreads has TEvent with a timed wait
which can include android and embedded.
You can force the use of the Sleep() based timer by defining USESLEEP
}
{$IFNDEF USESLEEP}
{$if Defined(MSWINDOWS) or (Defined(UNIX) and not Defined(BEOS))}
{$define Has_EventWait}
{$endif}
{$ENDIF}
interface interface
uses uses
@ -36,20 +56,25 @@ uses
type type
TFPTimerDriver = Class; TFPTimerDriver = Class;
{ TFPCustomTimer }
TFPCustomTimer = class(TComponent) TFPCustomTimer = class(TComponent)
private private
FInterval: Integer;
FDriver : TFPTimerDriver; FDriver : TFPTimerDriver;
FOnTimer: TNotifyEvent; FOnStartTimer : TNotifyEvent;
FContinue: Boolean; FOnStopTimer : TNotifyEvent;
FRunning: Boolean; FOnTimer : TNotifyEvent;
FEnabled: Boolean; FInterval : Cardinal;
procedure SetEnabled(Value: Boolean ); FActive : Boolean;
FEnabled : Boolean;
FUseTimerThread : Boolean;
procedure SetEnabled(const AValue: Boolean );
procedure SetInterval(const AValue: Cardinal);
protected protected
property Continue: Boolean read FContinue write FContinue; property Active: Boolean read FActive write FActive;
procedure Timer; virtual;
Function CreateTimerDriver : TFPTimerDriver; Function CreateTimerDriver : TFPTimerDriver;
procedure Timer; virtual;
public public
Constructor Create(AOwner: TComponent); override; Constructor Create(AOwner: TComponent); override;
Destructor Destroy; override; Destructor Destroy; override;
@ -57,25 +82,36 @@ type
procedure StopTimer; virtual; procedure StopTimer; virtual;
protected protected
property Enabled: Boolean read FEnabled write SetEnabled; property Enabled: Boolean read FEnabled write SetEnabled;
property Interval: Integer read FInterval write FInterval; property Interval: Cardinal read FInterval write SetInterval;
property UseTimerThread: Boolean read FUseTimerThread write FUseTimerThread;
property OnTimer: TNotifyEvent read FOnTimer write FOnTimer; property OnTimer: TNotifyEvent read FOnTimer write FOnTimer;
property OnStartTimer: TNotifyEvent read FOnStartTimer write FOnStartTimer;
property OnStopTimer: TNotifyEvent read FOnStopTimer write FOnStopTimer;
end; end;
TFPTimer = Class(TFPCustomTimer) TFPTimer = Class(TFPCustomTimer)
Published Published
Property Enabled; Property Enabled;
Property Interval; Property Interval;
Property UseTimerThread;
Property OnTimer; Property OnTimer;
end; Property OnStartTimer;
Property OnStopTimer;
end;
{ TFPTimerDriver }
TFPTimerDriver = Class(TObject) TFPTimerDriver = Class(TObject)
Protected Protected
FTimer : TFPCustomTimer; FTimer : TFPCustomTimer;
FTimerStarted : Boolean;
procedure SetInterval(const AValue: Cardinal); virtual;
Public Public
Constructor Create(ATimer : TFPCustomTimer); virtual; Constructor Create(ATimer : TFPCustomTimer); virtual;
Procedure StartTimer; virtual; abstract; Procedure StartTimer; virtual; abstract;
Procedure StopTimer; virtual; abstract; Procedure StopTimer; virtual; abstract;
Property Timer : TFPCustomTimer Read FTimer; Property Timer : TFPCustomTimer Read FTimer;
property TimerStarted: Boolean read FTimerStarted;
end; end;
TFPTimerDriverClass = Class of TFPTimerDriver; TFPTimerDriverClass = Class of TFPTimerDriver;
@ -100,9 +136,8 @@ end;
destructor TFPCustomTimer.Destroy; destructor TFPCustomTimer.Destroy;
begin begin
If FEnabled then StopTimer;
StopTimer; FDriver.FTimer:=Nil;
FDriver.FTimer:=Nil;
FreeAndNil(FDriver); FreeAndNil(FDriver);
Inherited; Inherited;
end; end;
@ -114,34 +149,59 @@ begin
Result:=DefaultTimerDriverClass.Create(Self); Result:=DefaultTimerDriverClass.Create(Self);
end; end;
procedure TFPCustomTimer.SetEnabled(Value: Boolean); procedure TFPCustomTimer.SetEnabled(const AValue: Boolean);
begin begin
if Value <> FEnabled then if AValue <> FEnabled then
begin begin
if Value then FEnabled := AValue;
if FEnabled then
StartTimer StartTimer
else else
StopTimer; StopTimer;
end; end;
end; end;
procedure TFPCustomTimer.StartTimer; procedure TFPCustomTimer.SetInterval(const AValue: Cardinal);
begin begin
If FEnabled then if FInterval <> AValue then
Exit; begin
FEnabled:=True; fInterval := AValue;
FContinue:=True; if FActive and (fInterval > 0) then
If Not (csDesigning in ComponentState) then FDriver.SetInterval(AValue) // Allow driver to update Interval
else
StopTimer; // Timer not required
end;
end;
procedure TFPCustomTimer.StartTimer;
var
IsActive: Boolean;
begin
IsActive:=FEnabled and (fInterval > 0) and Assigned(FOnTimer);
If IsActive and not fActive and Not (csDesigning in ComponentState) then
begin
FDriver.StartTimer; FDriver.StartTimer;
if FDriver.TimerStarted then
begin
FActive := True;
if Assigned(OnStartTimer) then
OnStartTimer(Self);
end;
end;
end; end;
procedure TFPCustomTimer.StopTimer; procedure TFPCustomTimer.StopTimer;
begin begin
If Not FEnabled then if FActive then
Exit; begin
FEnabled:=False; FDriver.StopTimer;
FContinue:=False; if not FDriver.TimerStarted then
FDriver.StopTimer; begin
FActive:=False;
if Assigned(OnStopTimer) then
OnStopTimer(Self);
end;
end;
end; end;
procedure TFPCustomTimer.Timer; procedure TFPCustomTimer.Timer;
@ -149,14 +209,13 @@ procedure TFPCustomTimer.Timer;
begin begin
{ We check on FEnabled: If by any chance a tick comes in after it was { We check on FEnabled: If by any chance a tick comes in after it was
set to false, the user won't notice, since no event is triggered.} set to false, the user won't notice, since no event is triggered.}
If FEnabled and Assigned(FOnTimer) then If FActive and Assigned(FOnTimer) then
FOnTimer(Self); FOnTimer(Self);
end; end;
{ --------------------------------------------------------------------- { ---------------------------------------------------------------------
TFPTimerDriver TFPTimerDriver
---------------------------------------------------------------------} ---------------------------------------------------------------------}
Constructor TFPTimerDriver.Create(ATimer : TFPCustomTimer); Constructor TFPTimerDriver.Create(ATimer : TFPCustomTimer);
@ -164,116 +223,327 @@ begin
FTimer:=ATimer; FTimer:=ATimer;
end; end;
procedure TFPTimerDriver.SetInterval(const AValue: Cardinal);
begin
// Default implementation is to restart the timer on Interval change
if TimerStarted then
begin
StopTimer;
FTimerStarted := (AValue > 0);
if FTimerStarted then
StartTimer;
end;
end;
{ --------------------------------------------------------------------- { ---------------------------------------------------------------------
Default implementation. Threaded timer, one thread per timer. Default implementation. Threaded timer, one thread per timer.
---------------------------------------------------------------------} ---------------------------------------------------------------------}
const
cMilliSecs: Extended = 60.0 * 60.0 * 24.0 * 1000.0;
Type Type
{ TFPTimerThread }
TFPTimerThread = class(TThread) TFPTimerThread = class(TThread)
private private
FTimerDriver: TFPTimerDriver; FTimerDriver: TFPTimerDriver;
FStartTime : TDateTime;
{$ifdef Has_EventWait}
FWaitEvent: PEventState;
{$else}
fSignaled: Boolean;
{$endif}
fInterval: Cardinal;
Function Timer : TFPCustomTimer; Function Timer : TFPCustomTimer;
Function GetWakeTime(var AInterval,Counter : Int64; Out WakeInterval : Integer; Out WakeTime : TDateTime) : Boolean;
public public
procedure Execute; override; procedure Execute; override;
constructor CreateTimerThread(ATimerDriver: TFPTimerDriver); constructor CreateTimerThread(ATimerDriver: TFPTimerDriver);
procedure Terminate;
procedure SetInterval(const AValue: Cardinal);
end; end;
{ TFPThreadedTimerDriver }
TFPThreadedTimerDriver = Class(TFPTimerDriver) TFPThreadedTimerDriver = Class(TFPTimerDriver)
Private Private
FThread : TFPTimerThread; FThread : TFPTimerThread;
Procedure DoNilTimer(Sender : TObject); protected
Procedure SetInterval(const AValue: cardinal); override;
Public Public
Procedure StartTimer; override; Procedure StartTimer; override;
Procedure StopTimer; override; Procedure StopTimer; override;
end; end;
function _GetTickCount: Cardinal;
begin
Result := Cardinal(Trunc(Now * 24 * 60 * 60 * 1000));
end;
{ --------------------------------------------------------------------- { ---------------------------------------------------------------------
TFPTimerThread TFPTimerThread
---------------------------------------------------------------------} ---------------------------------------------------------------------}
constructor TFPTimerThread.CreateTimerThread(ATimerDriver: TFPTimerDriver); constructor TFPTimerThread.CreateTimerThread(ATimerDriver: TFPTimerDriver);
begin begin
inherited Create(True); inherited Create(True);
FTimerDriver:=ATimerDriver; FTimerDriver:=ATimerDriver;
{$ifdef Has_EventWait}
FWaitEvent := BasicEventCreate(nil,false,false,'');
{$else}
fSignaled := False;
{$endif}
fInterval := ATimerDriver.Timer.Interval;
FreeOnTerminate := True; FreeOnTerminate := True;
end; end;
procedure TFPTimerThread.Terminate;
begin
inherited Terminate;
{$ifdef Has_EventWait}
BasicEventSetEvent(fWaitEvent);
{$else}
fSignaled := True;
{$endif}
end;
procedure TFPTimerThread.SetInterval(const AValue: Cardinal);
begin
if fInterval <> AValue then
begin
fInterval := AValue;
{$ifdef Has_EventWait}
BasicEventSetEvent(fWaitEvent); // Wake thread
{$else}
fSignaled := True;
{$endif}
end;
end;
Function TFPTimerThread.Timer : TFPCustomTimer; Function TFPTimerThread.Timer : TFPCustomTimer;
begin begin
If Assigned(FTimerDriver) Then If Assigned(FTimerDriver) Then
Result:=FTimerDriver.FTimer Result:=FTimerDriver.FTimer
else else
Result:=Nil; Result:=Nil;
end; end;
procedure TFPTimerThread.Execute; Function TFPTimerThread.GetWakeTime(var AInterval,Counter : Int64; Out WakeInterval : Longint; Out WakeTime : TDateTime) : Boolean;
var
SleepTime: Integer;
S,Last: Cardinal; Var
T : TFPCustomTimer; Diff: Extended;
begin begin
while Not Terminated do { Use Counter*fInterval to avoid numerical errors resulting from adding
small values (AInterval/cMilliSecs) to a large real number (TDateTime),
even when using Extended precision }
WakeTime := FStartTime + (Counter*AInterval / cMilliSecs);
Diff := (WakeTime - Now);
if Diff > 0 then
begin begin
Last := _GetTickCount; WakeInterval := Trunc(Diff * cMilliSecs);
T:=Timer; if WakeInterval < 10 then
If Assigned(T) then WakeInterval := 10; // Provide a minimum wait time
begin end
SleepTime := T.FInterval - (_GetTickCount - Last); else
if SleepTime < 10 then begin
SleepTime := 10; WakeInterval:=MaxInt;
Repeat // Time has already expired, execute Timer and restart wait loop
S:=5; try
If S>SleepTime then if not Timer.UseTimerThread then
S:=SleepTime; Synchronize(@Timer.Timer) // Call user event
Sleep(S); else
Dec(Sleeptime,S); Timer.Timer;
until (SleepTime<=0) or Terminated; except
T:=Timer; // Trap errors to prevent this thread from terminating
If Assigned(T) and not terminated then end;
Synchronize(@T.Timer); Inc(Counter);
end Result:=True;
else
Terminate;
end; end;
end; end;
{$ifdef Has_EventWait}
procedure TFPTimerThread.Execute;
var
WakeTime, StartTime: TDateTime;
WakeInterval: Integer;
Counter: int64; { use Int64 to avoid overflow with Counter*fInterval (~49 days)}
AInterval: int64;
Diff: Extended;
Const
wrSignaled = 0;
wrTimeout = 1;
wrAbandoned= 2;
wrError = 3;
begin
WakeInterval := MaxInt;
Counter := 1;
AInterval := fInterval;
FStartTime := Now;
while not Terminated do
begin
if GetWakeTime(AInterval,Counter,WakeInterval,WakeTime) then
Continue;
if not Terminated then
case BasicEventWaitFor(WakeInterval,fWaitEvent) of
wrTimeout:
begin
if Terminated then
Break
else
begin
try
if not Timer.UseTimerThread then
// If terminate is called while here, then the Synchronize will be
// queued while the stoptimer is being processed.
// StopTimer cannot wait until thread completion as this would deadlock
Synchronize(@Timer.Timer) // Call user event
else
Timer.Timer;
except
// Trap errors to prevent this thread from terminating
end;
Inc(Counter); // Next interval
end;
end;
wrSignaled:
begin
if Terminated then
Break
else
begin // Interval has changed
Counter := 1; // Restart timer without creating new thread
AInterval := fInterval;
FStartTime := Now;
end;
end;
else
Break;
end
end;
BasicEventDestroy(fWaitEvent);
end;
{$ELSE Has_EventWait}
procedure TFPTimerThread.Execute;
var
WakeTime, StartTime: TDateTime;
WakeInterval: Integer;
Counter: int64; { use Int64 to avoid overflow with Counter*fInterval (~49 days)}
AInterval: int64;
Diff: Extended;
S,Last: Cardinal;
RecheckTimeCounter: integer;
const
cSleepTime = 500; // 0.5 second, better than every 5 milliseconds
cRecheckTimeCount = 120; // Recheck clock every minute, as the sleep loop can loose time
begin
WakeInterval := MaxInt;
Counter := 1;
AInterval := fInterval;
FStartTime := Now;
while not Terminated do
begin
if GetWakeTime(AInterval,Counter,WakeInterval,WakeTime) then
Continue;
if not Terminated then
begin
RecheckTimeCounter := cRecheckTimeCount;
s := cSleepTime;
repeat
if s > WakeInterval then
s := WakeInterval;
sleep(s);
if fSignaled then // Terminated or interval has changed
begin
if not Terminated then
begin
fSignaled := False;
Counter := 1; // Restart timer
AInterval := fInterval;
StartTime := Now;
end;
break; // Need to break out of sleep loop
end;
dec(WakeInterval,s); // Update total wait time
dec(RecheckTimeCounter); // Do we need to recheck current time
if (RecheckTimeCounter < 0) and (WakeInterval > 0) then
begin
Diff := (WakeTime - Now);
WakeInterval := Trunc(Diff * cMilliSecs);
RecheckTimeCounter := cRecheckTimeCount;
s := cSleepTime;
end;
until (WakeInterval<=0) or Terminated;
if WakeInterval <= 0 then
try
inc(Counter);
if not Timer.UseTimerThread then
// If terminate is called while here, then the Synchronize will be
// queued while the stoptimer is being processed.
// StopTimer cannot wait until thread completion as this would deadlock
Synchronize(@Timer.Timer) // Call user event
else
Timer.Timer;
except
// Trap errors to prevent this thread from terminating
end;
end
end;
end;
{$ENDIF Has_EventWait}
{ --------------------------------------------------------------------- { ---------------------------------------------------------------------
TFPThreadedTimerDriver TFPThreadedTimerDriver
---------------------------------------------------------------------} ---------------------------------------------------------------------}
Procedure TFPThreadedTimerDriver.DoNilTimer(Sender : TObject); procedure TFPThreadedTimerDriver.SetInterval(const AValue: cardinal);
begin begin
FThread:=Nil; if FThread <> nil then
begin
if AValue > 0 then
FThread.SetInterval(AValue)
else
StopTimer;
end;
end; end;
Procedure TFPThreadedTimerDriver.StartTimer; Procedure TFPThreadedTimerDriver.StartTimer;
begin begin
FThread:=TFPTimerThread.CreateTimerThread(Self); if FThread = nil then
FThread.OnTerminate:=@DoNilTimer; begin
FThread.Start; FThread:=TFPTimerThread.CreateTimerThread(Self);
FThread.Start;
FTimerStarted := True;
end;
end; end;
Procedure TFPThreadedTimerDriver.StopTimer; Procedure TFPThreadedTimerDriver.StopTimer;
begin begin
FThread.FTimerDriver:=Nil; if FThread <> nil then
FThread.Terminate; // Will free itself. begin
CheckSynchronize; // make sure thread is not stuck at synchronize call. try
If Assigned(FThread) then // Cannot wait on thread in case
Fthread.WaitFor; // 1. this is called in a Synchonize method and the FThread is
// about to run a synchronize method. In these cases we would have a deadlock
// 2. In a DLL and this is called as part of DLLMain, which never
// returns endthread (hence WaitFor) until DLLMain is exited
FThread.Terminate; // Will call FThread.Wake;
finally
FThread := nil;
end;
FTimerStarted := False;
end;
end; end;
Initialization Initialization
DefaultTimerDriverClass:=TFPThreadedTimerDriver; DefaultTimerDriverClass:=TFPThreadedTimerDriver;
end. end.