From time to time I got error reports about "error 1400" in OTL and my response is always the same: "You are using it wrong." I never wrote anything about that, though. My bad :(
This time, however, I received an excellent error report with a minimal reproducible example (thanks!), which gave me an excellent excuse for this blog post ;)
Let me start with a little demonstration...
The MRE code (which I will not reproduce entirely here) was just a small VCL app that created a worker thread TWorker which in turn runs OTL's Background Worker abstraction:
destructor TWorker.Destroy;
begin
inherited;
FBackgroundWorker.Terminate(30000);
FBackgroundWorker := nil;
end;
procedure TWorker.Execute;
begin
FBackgroundWorker :=
Parallel.BackgroundWorker.NumTasks(1)
.Execute(
procedure (const AItem: IOmniWorkItem)
begin
end)
.OnRequestDone_Asy(
procedure (const Sender: IOmniBackgroundWorker; const AItem: IOmniWorkItem)
begin
end);
FBackgroundWorker.Schedule(FBackgroundWorker.CreateWorkItem(42));
while not Terminated do
Sleep(10);
end;
At a first look, nothing is wrong with this code. IOmniBackgroundWorker gets created, does some work, and is destroyed together with the worker thread. Running this, however, results in exception EOSError with message 'System error. Code: 1400. Invalid window handle' inside TOmniContainerWindowsMessageObserverImpl.PostWithRetry (in unit OtlContainerObserver).
Why?
In this case (and there may be other reasons for the 1400 error) the reason is a hidden window, created by the OTL. This window is created when you run tasks in Unobserved mode - and all OTL Parallel abstrations do exactly that. It is created when the background worker is created - in the TWorker thread. This window is associated with the current (TWorker) thread by the Windows. And when a thread is terminated, all windows that were created in that thread are closed!
Let me repeat that, because it is important:
A window (on Windows) is associated with the thread it has been created in! When a thread is destroyed, associated windows are closed!
In the MRE above, the following happens:
- The worker thread starts a background worker.
- OTL creates a hidden window, which gets associated with the thread.
- The worker thread is terminated (not shown in the code above). SafeExecute notices that the Terminated flag is set and exits.
- Background worker is still alive as the reference to it is kept in the FBackgroundWorker field.
- The worker thread is destroyed. Because of that, the hidden window is also destroyed.
- TWorker.Destroy is called and tries to terminate the background worker. This in turn causes a message to be sent to the hidden window (created in step 2) and that fails with Windows error 1400.
Solution? Destroy the background worker in the same thread it was created in.
procedure TWorker.Execute;
begin
FBackgroundWorker :=
Parallel.BackgroundWorker.NumTasks(1)
.Execute(
procedure (const AItem: IOmniWorkItem)
begin
end)
.OnRequestDone_Asy(
procedure (const Sender: IOmniBackgroundWorker; const AItem: IOmniWorkItem)
begin
end);
FBackgroundWorker.Schedule(FBackgroundWorker.CreateWorkItem(42));
while not Terminated do
Sleep(10);
FBackgroundWorker.Terminate(30000);
FBackgroundWorker := nil;
end;
This should be considered as a general reccomendation for using OTL tasks: They should be created in destroyed in the same thread. This is not an absolute requirement but in prevents problems as the one described above.
Multithreading is hard! Why I feel myself somewhere in the middle of 1990's with threads? :) Even when given simple abtractions, it is still hard in many aspects. Maybe in future other execution models will be more widely used other than "multiple threads with common memory" (message-passing parallelism, coroutines with channels, actors model or something else). The paradox is in that threads model appeared much more earlier than very-many-core processors appreared.
ReplyDelete