Search code examples
multithreadingdelphiwinapiformatdatetime

Delphi Win API CreateTimerQueueTimer threads and thread safe FormatDateTime crashes


This is a bit of a long question, but here we go. There is a version of FormatDateTime that is said to be thread safe in that you use

GetLocaleFormatSettings(3081, FormatSettings); 

to get a value and then you can use it like so;

FormatDateTime('yyyy', 0, FormatSettings); 

Now imagine two timers, one using TTimer (interval say 1000ms) and then another timer created like so (10ms interval);

CreateTimerQueueTimer
(
  FQueueTimer, 
  0, 
  TimerCallback, 
  nil, 
  10, 
  10, 
  WT_EXECUTEINTIMERTHREAD
);

Now the narly bit, if in the call back and also the timer event you have the following code;

for i := 1 to 10000 do
begin
  FormatDateTime('yyyy', 0, FormatSettings);
end;

Note there is no assignment. This produces access violations almost immediatley, sometimes 20 minutes later, whatever, at random places. Now if you write that code in C++Builder it never crashes. The header conversion we are using is the JEDI JwaXXXX ones. Even if we put locks in the Delphi version around the code, it only delays the inevitable. We've looked at the original C header files and it all looks good, is there some different way that C++ uses the Delphi runtime? The thread safe version of FormatDatTime looks to be re-entrant. Any ideas or thoughts from anyone who may have seen this before.

UPDATE:

To narrow this down a bit, FormatSettings is passed in as a const, so does it matter if they use the same copy (as it turns out passing a local version within the function call yeilds the same problem)? Also the version of FormatDateTime that takes the FormatSettings doesn't call GetThreadLocale, because it already has the Locale information in the FormatSettings structure (I double checked by stepping through the code).

I made mention of no assignment to make it clear that no shared storage is being accessed, so no locking is required.

WT_EXECUTEINTIMERTHREAD is used to simplify the problem. I was under the impression you should only use it for very short tasks because it may mean it'll miss the next interval if it is running something long?

If you use a plain old TThread the problem doesn't occur. What I am getting at here I suppose is that using a TThread or a TTimer works but using a thread created outside the VCL doesn't, that's why I asked if there was a difference in the way C++ Builder uses the VCL/Delphi RTL.

As an aside this code as mentioned before also fails (but takes longer), after a while, CS := TCriticalSection.Create;

  CS.Acquire;
  for i := 1 to LoopCount do
  begin
    FormatDateTime('yyyy', 0, FormatSettings);
  end;
  CS.Release;

And now for the bit I really don't understand, I wrote this as suggested;

function ReturnAString: string;
begin
  Result := 'Test';
  UniqueString(Result);
end;

and then inside each type of timer the code is;

  for i := 1 to 10000 do
  begin
    ReturnAString;
  end;

This causes the same kinds of failiures, as I said before the fault is never in the same place inside the CPU window etc. Sometimes it's an access violation and sometimes it might be an invalid pointer operation. I am using Delphi 2009 btw.

UPDATE 2:

Roddy (below) points out the Ontimer event (and unfortunately also Winsock, i.e. TClientSocket) use the windows message pump (as an aside it would be nice to have some nice Winsock2 components using IOCP and Overlapping IO), hence the push to get away from it. However does anyone know how to see what sort of thread local storage is setup on the CreateQueueTimerQueue?

Thanks for taking the time to think and answer this problem.


Solution

  • I am not sure if it is good form to post an "Answer" to my own question but it seemed logical, let me know if that is uncool.

    I think I have found the problem, the thread local storage idea lead me to follow a bunch of leads and I found this magical line;

    IsMultiThread := True;

    From the help;

    "IsMultiThread is set to true to indicate that the memory manager should support multiple threads. IsMultiThread is set to true by BeginThread and class factories."

    This of course is not set by using a single Main VCL thread using a TTimer, However it is set for you when you use TThread. If I set it manually the problem goes away.

    In C++Builder, I do not use a TThread but it appears by using the following code;

    if (IsMultiThread) {
      ShowMessage("IsMultiThread is True!");
    }
    

    that is it set for you somewhere automatically.

    I am really glad for peoples input so that I could find this and I hope vainly it might help someone else.