EurekaLog
Menu

The EurekaLog Developer Guide

21. Multi-threaded applications

A background task fails, the UI keeps running, and no report appears. Work through three separate questions: was EurekaLog active in that worker, who owns the exception, and who handles it? Creating a thread, enabling tracing and delivering a failure to the caller are separate actions.

Design the failure path to work without EurekaLog: stop the failed operation, release its resources, and notify the caller. Then add reporting at that boundary. Do not let the caller proceed as if a failed task succeeded simply because a dialog was shown.

For most new code, use TThreadEx or BeginThreadEx from EBase, pass an explicit activation choice, and choose one exception-handling owner. For framework workers, adapt the framework's completion or error callback instead. Chapter 6 gives the core calls; chapter 15 explains metadata ownership and snapshots.

21.1 Where the processing happens, and what a thread carries

Exception processing can run in the background thread handling the exception, including EurekaLog event handlers. It is not automatically marshalled to the GUI thread, and several threads can process exceptions concurrently. Avoid direct GUI access and protect shared state appropriately.

For GUI applications, handling a completed worker's FatalException in the main thread is often the clearest design (section 21.4). Consecutive processing serializes exception processing with a global lock; it does not move it to the main thread or make shared application data thread-safe. It also does not prevent reentrant exceptions on one thread while a dialog pumps messages.

The second thing a thread carries is state that has to be set per thread: EurekaLog has to be enabled for background threads. A thread is not covered because the application is covered - a thread that was never switched on produces no report, however well the application itself is configured. The other piece of per-thread state is the name, which the documentation says is shown in the debugger's Threads window and used as the thread caption in bug reports. Both are set by two routines from EBase, a unit that can be included in any application without the full EurekaLog code - which is what makes these calls safe in a build that has no EurekaLog:

procedure NameThread(const AName: String; const AThreadID: Cardinal = 0);   // EBase
function SetEurekaLogStateInThread(const AThreadID: Cardinal;
  const AActivate: Boolean): Boolean;   // EBase

The name is an arbitrary string - it does not have to be unique - and it may be set more than once, the most recently set value winning. SetEurekaLogStateInThread switches EurekaLog on or off in one thread (a thread id of 0 means the current thread) and returns the previous state, so the caller can put it back instead of assuming what it was. This is per-thread state; the global flag is what SetEurekaLogState changes, and the two are independent switches - EurekaLog starts up enabled for the main thread alone, and a thread produces a report only when both are on. Chapter 6 covers the routines.

21.2 Choosing how to create the thread

CreateThread - the one the documentation tells you not to use

CreateThread is the basic system function for creating threads; every other method is built on it, and the documentation warns against using it directly: at least BeginThread, and preferably BeginThreadEx, because those functions contain the RTL thread support code. The reason is what happens when the thread fails. A thread function passed to CreateThread is not protected by any exception handler, and the documentation gives both endings: under the debugger, a "Project ... faulted with message ..." notification and then termination of the application; in a free run, the system's "... has stopped working..." dialog and then termination. EurekaLog installs a wrapper for any CreateThread thread function to catch unhandled exceptions - as long as the "Use low-level hooks" option is enabled and the corresponding threading option is set - but the wrapper changes only the quality of the report, not the termination.

BeginThread - better, still not the recommendation

BeginThread is the RTL's basic function for creating threads from Delphi and C++ Builder code, a simple wrapper for CreateThread, and the documentation recommends BeginThreadEx instead. Over the system call it adds three things: it makes other RTL calls thread-safe (memory and heap operations, for instance), it handles an unhandled thread exception by passing it to SysUtils.ShowException, and it uses a more Pascal-friendly prototype. EurekaLog installs a wrapper for a BeginThread thread function too, again provided low-level hooks are enabled, and again with the same ending.

Without those hooks you get BeginThread's own default handling, whose documented drawbacks are: a low-level error dialog showing the raw exception address; termination of the application after an exception in a thread; and unexpected behaviour outside the debugger, where a system error dialog is followed by the SysUtils.ShowException dialog and possibly more dialogs during termination. Default handling is therefore usually not good enough, and the instruction is to catch all exceptions in the thread yourself - which is also what the RTL's BeginThread documentation asks for when it says the thread function should handle all of its own exceptions.

BeginThreadEx is one of the two helpers EurekaLog provides to simplify thread management (the other is the TThreadEx class); both come from EBase and both are usable without EurekaLog enabled. Its signature is BeginThread's with two arguments added at the end:

function BeginThreadEx(SecurityAttributes: Pointer; StackSize: LongWord; ThreadFunc: TThreadFunc;
  Parameter: Pointer; CreationFlags: LongWord; var ThreadId: TThreadID;
  const AThreadName: String = ''; const AEnableEL: Boolean = Boolean(2)): Integer;   // EBase

The extra arguments name the thread and control its EurekaLog state. The state argument is ignored without EurekaLog, but the name is not. Boolean(2) means inherit the current thread's state. Pass True explicitly when reporting must not depend on the creator's per-thread state.

BeginThreadEx wraps the callback in an exception handler regardless of EurekaLog activation. An unhandled exception invokes the default handler, produces a non-zero thread exit code and does not itself terminate the application. With EurekaLog active, the handler uses its report processing; without it, the default error handling still runs. Reporting does not undo partial application work.

Use EBase for the function and Windows for handle operations. Keep the returned handle if you need to wait or read GetExitCodeThread; close it exactly once when you no longer need it. Closing a handle does not cancel its thread. The callback keeps the BeginThread signature; see chapter 6 for the complete creation/cleanup pattern.

The migration is easy: the thread function keeps the same signature as with BeginThread, which is why the documentation suggests adding the "Ex" suffix to your existing BeginThread calls or running a search-and-replace of BeginThread to BeginThreadEx. The exit code also becomes a success/failure signal - non-zero when there was an exception, otherwise the thread function's result (usually zero) - which is what GetExitCodeThread reads back from a handle you kept.

TThread - correct, but silent by design

TThread captures an exception escaping Execute in FatalException without invoking an error handler by default. The thread object owns that exception until you explicitly transfer ownership. A task can therefore fail quietly even though the exception was caught correctly by the RTL.

The Auto-handle TThread exceptions option changes this compatibility behavior. It can interfere with third-party code that expects to consume FatalException, has no effect when EurekaLog is disabled, and depends on low-level hooks. Prefer an explicit handling owner rather than turning the option on to compensate for a missing completion path.

TThreadEx descends from TThread and adds naming, activation and optional exception handling. All five handling strategies in section 21.4 still apply. The named constructors' activation default is Boolean(2), inheriting the creator's state. Pass an explicit Boolean when you need a particular activation state rather than inheritance.

// Constructor forms for an existing TMyThread descendant of TThreadEx:
Thread := TMyThread.Create('Thread Name', True);  // running, explicitly enabled
Thread := TMyThread.Create('Thread Name', False); // running, explicitly disabled
Thread := TMyThread.Create(True, 'Thread Name', True); // suspended; configure, then Start

Create suspended when setting AutoHandleException, OnTerminate, FreeOnTerminate or worker inputs before execution. This startup use of "suspended" is not permission to suspend an arbitrary running thread; section 21.6 explains the lock hazards of doing that.

In a TThreadEx.Execute override, call inherited Execute before your work. The API reference requires it even though some introductory examples omit it. A long-running worker must also check Terminated and cooperate with the application's shutdown protocol.

The class also supports anonymous threads through its own CreateAnonymousThread class method, recommended over TThread.CreateAnonymousThread, and adds a property that saves the ceremony of an OnTerminate handler: Thread.AutoHandleException := True; makes the class automatically invoke the HandleException routine for the thread's FatalException. Processing happens in that worker, not automatically in the caller or main thread. FatalException is not cleared by automatic handling, so an owner can still inspect it to determine success; do not also submit it through a second reporting path. A failed worker returns a non-zero exit code. With AutoHandleException left at its default False, the class retains TThread's behavior.

This complete failure-path example belongs in a unit using SysUtils and EBase. Call StartReportingProbe from a development-only action in an initialized EurekaLog application; do not wire it into normal production startup. It intentionally fails once and reports in the worker.

uses
  SysUtils, EBase;

type
  TReportingProbe = class(TThreadEx)
  protected
    procedure Execute; override;
  end;

procedure TReportingProbe.Execute;
begin
  inherited Execute;
  raise Exception.Create('Controlled background-thread failure');
end;

procedure StartReportingProbe;
var
  Worker: TReportingProbe;
begin
  Worker := TReportingProbe.Create(True, 'Reporting probe', True);
  Worker.AutoHandleException := True;
  Worker.FreeOnTerminate := True;
  Worker.Start;
  // Never read or free Worker after Start: it may already have destroyed itself.
end;

The expected result is one handled worker exception, identified as Reporting probe; dialog and delivery behavior depend on the configured application policy. No report means you should trace activation and the chosen handler, not enable every system thread. In real work, notify completion through your application's lifetime-safe mechanism; this fire-and-forget probe is not a pattern for recovering a task result.

Anonymous threads work the same way. An anonymous thread is a wrapper for TThread that simplifies the "fire-and-forget" pattern and, in the documentation's words, has one additional advantage: it closes over local variables, so you do not have to declare a thread class and copy the values into it. Internally it is an instance of TAnonymousThread, a descendant of TThread, whose Execute calls the supplied procedure, and it inherits TThread's error handling - with one exception: since you did not declare a class, you cannot override DoTerminate. Two facts are easy to trip over: anonymous threads are created in suspended state, so you must run them yourself with Start (which is what allows you to set properties before running the thread), and they are created with FreeOnTerminate set to True, which means - in the documentation's words - that you should never access the thread variable after you have called Start.

Frameworks and thread pools

Threading frameworks - OmniThreadLibrary and AsyncCalls are the documentation's examples, and a threading library can be part of a larger framework, as Indy's threading sub-library or JCL's synchronisation primitives are - typically create threads you never see, so BeginThreadEx and TThreadEx are usually not available. The job reduces to two tasks: enable EurekaLog for the threads the framework creates (manually, via SetEurekaLogStateInThread; see section 21.3) and call EurekaLog's exception handler for their unhandled exceptions. For the second, the documentation lists the common shapes: if the framework calls the default exception handler (ApplicationHandleException) or re-raises the exception in the caller thread, there is nothing to do; if it offers an OnTerminate-like event, register your function there and analyse the termination state as section 21.4 does for TThread; otherwise wrap the thread code in an explicit try/except block and call the handler yourself. A framework whose threads are also a thread pool requires the thread's state to be preserved - one thread may serve many tasks, so priority, FPU state, thread name and EurekaLog's per-thread state must be put back. For OmniThreadLibrary there is a documented extra: include the EOTL unit, which the vendor ships in the installation's Source\Extras folder; thread names from the library then reach EurekaLog while its internal service calls are filtered out of the call stack.

A thread pool reuses workers for multiple callbacks. Do not assume that a later task will run on the same thread, or that thread-local storage will be empty when your task starts. Do not terminate the pool's thread, leave its FPU/priority/activation state changed, or allow exceptions to escape the callback. Calls requiring a persistent dedicated thread need a different design. Restore every piece of thread state your task changes before returning control to the pool.

21.3 Switching EurekaLog on for the thread

EurekaLog offers two ways to get the per-thread activation flag set, and it recommends one of them emphatically. The automatic way is the "Default EurekaLog state in new threads" option, whose documented default is "Disabled (recommended)" and which can also be "Enabled" or "Enabled for RTL threads, disabled for Windows threads". Automatic enabling is highly not recommended, and the reason is a census of a real application: besides your own main thread and the one background thread you wrote, the process contains threads from the system thread pool, service threads for background tasks such as asynchronous WinSock work, threads created by the frameworks you use (Indy, for instance), and even threads created by the IDE for debugger tasks. Those were launched with some exception handling in mind, and their creator expects them to behave in a specific way - injecting EurekaLog processing and handling into every thread would be risky and unnecessary. Enable EurekaLog only for threads you know: threads you created, or that run your code, such as pool threads.

The manual way is therefore the rule - BeginThreadEx instead of BeginThread, TThreadEx instead of TThread, and SetEurekaLogStateInThread where you cannot choose how the thread was made, thread pools being the documentation's own example. Two cases are conceded to the automatic option: when you do not want to change your code at all, and when you cannot control the thread's code but still want reports from it - which works only where that code's own handler ends up calling the default exception handler EurekaLog hooks. Even then, prefer "Enabled for RTL threads, disabled for Windows threads", and never turn the option to "Enabled" until it is really needed.

For a dedicated BeginThread-style callback, name and enable the thread before the application work begins, and handle failures at the callback boundary. The callback uses EBase, Classes and SysUtils; its application work must follow the project's normal cleanup policy.

For a pool callback, preserve activation instead of unconditionally resetting it to False. This helper is complete for a task that does not change the thread's name or other thread state. Put it in a unit using EBase; pass an ordinary parameterless procedure (not an anonymous closure) whose data remains valid until completion. The callback must use the Boolean result to record task success or failure in its own completion mechanism.

type
  TPoolWork = procedure;

function RunReportedPoolWork(const Work: TPoolWork): Boolean;
var
  PreviousState: Boolean;
begin
  PreviousState := SetEurekaLogStateInThread(0, True);
  try
    try
      Work;
      Result := True;
    except
      HandleException;
      Result := False;
    end;
  finally
    SetEurekaLogStateInThread(0, PreviousState);
  end;
end;

False means the work raised and reporting was requested; it is not a successful task result. As with any reporting boundary, an error in the handler itself still needs the framework's outer failure policy. If the framework already reports callback exceptions, do not install a second reporting owner: enable/restore activation around its normal task path instead.

In a dedicated TThread.Execute, the naming and activation calls belong before the actual thread work. ApplicationHandleException(nil) from Classes handles the active exception from inside an except block; HandleException from EBase also works without EurekaLog and can handle a saved exception explicitly. Use the latter for FatalException when it has not been re-raised.

If a pooled task changes its thread name, save and restore the old name too, using the current thread's metadata (chapter 15). Do not reset it to an empty string unless that is genuinely the state the pool contract requires.

21.4 Getting the exception out of the thread

For a plain TThread - and therefore for anything derived from it, including TThreadEx with AutoHandleException left off - an exception that escapes Execute is not raised anywhere by the time you learn about it: it sits in FatalException, waiting to be read. The documentation lists five ways to deal with a thread's exceptions, marking the first as recommended; the first four start from that property, the fifth catches the exception inside Execute before it ever gets there. What separates the five is which thread ends up processing the exception.

  1. Re-raise it in the caller thread (recommended). Take the exception out of the thread object, clear the property so the thread object does not destroy it, and raise it in the caller, where the handler you already have deals with it. Two documented side effects: the caller's normal execution stops, so your code does not continue as if the thread had succeeded; and although the caller raises it, the recorded call stack is still the background thread's, even though that thread has terminated.

    Thread.WaitFor;                             // analyse only after the thread has finished
    E := Thread.FatalException;
    PPointer(@Thread.FatalException)^ := nil;   // clear, so it won't be deleted with the thread
    if Assigned(E) then
      raise E;
    if Thread.ReturnValue <> 0 then             // no exception, but the thread still failed
      Abort;
    

    Here Thread must be a completed, caller-owned worker with FreeOnTerminate = False, and E is a local TObject. The owner must free Thread in an outer try/finally. Clearing FatalException transfers ownership of the exception away from the thread; never free E after raising it. The pointer assignment is the documented low-level transfer technique, not a normal property setter. Do not use it while the worker is still running.

    WaitFor belongs only where blocking is acceptable. In a GUI, prefer completion notification or OnTerminate over waiting in an event handler; a raw wait can block UI messages, and a worker that needs main-thread synchronization makes the waiting design especially important.

  2. Handle it in the caller thread. Hand the stored exception to HandleException in place - useful when you want a report without disturbing the caller's flow - and abort the normal path yourself, or the caller carries on as if nothing had happened. A detail about which routine is usable here: ApplicationHandleException always analyses the active exception, so a saved, non-raised one has to go to HandleException.

  3. Handle it in an OnTerminate event handler. The documented handler runs within Synchronize, from the main thread, so the exception is processed there. In an OnTerminate(Sender: TObject) handler with Classes and EBase in scope, use: if Assigned(TThread(Sender).FatalException) then HandleException(TThread(Sender).FatalException);. Do not free the exception in this form: the thread object still owns it.

  4. Handle it by overriding DoTerminate. The same analysis, invoked by the class instead of an assigned event, with one difference the documentation emphasises: DoTerminate is called by a background thread and is not synchronised into the main thread, so the exception is processed by the thread itself.

  5. Handle it with an explicit try/except block in Execute. Call HandleException inside the except block and stop the failed work; preserve cleanup in finally blocks. Processing then happens in the background thread.

One version limit is mentioned in passing: in Delphi 4 only method 5 is available, because TThread there has no FatalException property at all. Two remarks apply to all five methods: the documentation's own preference is TThreadEx, for the AutoHandleException described in section 21.2; and EurekaLog has to be enabled for the background thread, or none of the handlers above will produce a report - they still run, but without EurekaLog HandleException falls back to the standard error dialog. One practical detail runs through the samples: when the thread is created with FreeOnTerminate set to True - the norm for anonymous threads - the thread variable must not be touched after Start, which is one more reason the OnTerminate handler and AutoHandleException appear so often.

21.5 What EurekaLog keeps for each thread

The name this chapter has been setting is one of the things EurekaLog keeps for a thread, and the unit EThreadsManager exposes that bookkeeping as classes - the same place holds everything else EurekaLog knows about the thread. The key objects are:

Item Purpose
TEurekaThreadData Thread information.
TEurekaThreadDataList List of threads.
AcquireThreadData Returns thread information (any thread).
ThreadData Returns thread information (current thread only).
ThreadDataEnum Enumerates running threads.
TThreadKind Indicates thread kind.

The three accessors are not variations on one call: one answers for the thread you are in, one for an arbitrary thread, and one for all of them at once - and they differ in whether what you get back is the live object or a copy you have to release. Chapter 15, EThreadsManager: thread bookkeeping, catalogues them. What matters here is the direction of the dependency: the name set with NameThread is what the object carries and what a report prints as the thread's caption, and the same object supplies the thread's identity and kind - which is how a report can still name the thread that failed instead of listing a bare id.

21.6 The pitfalls, collected

Use these checks when reports disappear, shutdown stalls, or adding reporting changes timing:

  • Never create a thread with CreateThread in an application you control. Its thread function is unprotected, and an unhandled exception there terminates the process.
  • TThread stores exceptions without reporting them by default. Inspect FatalException through a chosen completion path. The auto-handle compatibility option can interfere with that path; it is not a substitute for deciding who owns failure handling.
  • A thread's EurekaLog state is not the application's. EurekaLog has to be enabled for background threads, and not for every thread in the process: system pool threads, service threads, framework threads and IDE threads have their own exception-handling expectations.
  • Low-level hook changes affect bookkeeping as well as timing. Without interception of ExitThread, finished-thread records can remain until process shutdown unless the thread path performs EurekaLog's cleanup. Prefer the supported TThreadEx/BeginThreadEx wrappers. The internal routines _NotifyThreadGone and _CleanupFinishedThreads are not public integration points; do not insert guessed calls into arbitrary framework code.
  • Event handlers can run concurrently in background threads. Synchronize shared data and marshal GUI work. Consecutive processing only serializes reporting; it does not replace either rule.
  • Pool threads are reused. Restore every state you change and route callback failures through the pool's error contract; never let an exception escape a raw system callback.
  • Never touch the thread variable after Start when FreeOnTerminate is set - use an OnTerminate handler or AutoHandleException instead.
  • Suspending threads can cause the reporting deadlock you are investigating. Keep Pause all EurekaLog-enabled/RTL/Windows threads during exception handling off unless you have a specific, reviewed need. A suspended thread may own the memory manager lock; even allocating report text can then block forever. Don't pause main thread is needed if handlers synchronize with it, but it does not remove other lock dependencies. Do not confuse these options with PauseFreezeCheck, which pauses checks rather than application threads.
  • Stop workers cooperatively before application finalization. A report or a thread handle being closed does not stop them. Terminate threads on shutdown can itself hang shutdown when it terminates a thread holding a global lock; it is not a cleanup design.

When the problem is thread-creation overhead, compare the same workload with the same build and configuration, recording how often threads start and stop. KnowHow article 92 identifies rapid thread churn as a diagnostic branch and recommends a pool for many short tasks. Temporarily changing Advanced / Low-level injection hooks from Allow all to Allow for current module only or Disabled can isolate hook-related cost; restore the setting afterward. A faster run does not prove it is safe to ship with lost coverage or cleanup. Check whether internal diagnostic logging is enabled too (chapter 5), rather than blaming ordinary ELogging entries indiscriminately.