EurekaLog
Menu

The EurekaLog Developer Guide

15. EThreadsManager: thread bookkeeping

Use thread bookkeeping when a report says that a worker failed, but you need to identify its role, creator or entry point. EThreadsManager exposes the information EurekaLog keeps about known threads through TEurekaThreadData.

TEurekaThreadData = class(TCheckedInterfacedObject);   // EThreadsManager

Choose the accessor before writing the code: one returns the live current-thread object; the others return snapshots. A snapshot is safe to inspect without locking EurekaLog's internal list, but it does not keep the target thread or its application-owned objects alive.

Need Use Ownership
Read or change this thread's dynamic metadata ThreadData Borrowed; never free it
Inspect another currently running thread AcquireThreadData(ID) New copy; free it
Inspect known threads, or enumerate all running threads ThreadDataEnum(False/True) Free the list, not individual items

Name and activate workers at their entry points (chapters 6 and 21). Neither acquiring thread data nor asking for additional stacks activates exception handling in a thread.

The property listings below describe values, not compilable declarations: no full public property types are specified. Method and function signatures are shown where available.

15.1 Getting the object for a thread

Three accessors, one for each question you can ask: "this thread", "that thread", "all threads". They differ in far more than convenience - whether you own the result, and whether the result is even the live object, changes with the one you pick.

ThreadData

function ThreadData: TEurekaThreadData;   // EThreadsManager

Information about the current thread, and the only accessor that returns the live object. Chapter 6 describes it with the rest of the main routines; it is listed here because it is the third way into the same data.

Two of its documented points matter when you compare it with the two calls below. You must not destroy the returned object - it belongs to EurekaLog and lives until the thread terminates - and it is the only accessor through which a change to a dynamic property has any effect.

AcquireThreadData

function AcquireThreadData(const AThreadID: Cardinal;
  const AForce: Boolean = False): TEurekaThreadData;   // EThreadsManager

Information about any running thread, given its thread id.

This is a copy, not the live object: a new object is created for each call, and you have to destroy it. The documentation is explicit about the ownership - you have to release the returned object, because a new one is created for every call - and equally explicit that editing it is pointless: any change you make to a dynamic property (the documentation names Name, LastException and LastHTMLPage as examples) will be ignored. To change a dynamic property, use the ThreadData function.

The optional AForce argument decides what happens when there is no answer. With the documented default of False, the call returns nil for an empty result. With True, it returns a dummy object instead, so the return value is always non-nil. The nil cases the page names are rare ones: calling before or after EThreadsManager's initialization or finalization, or passing an id that does not identify a valid running thread.

One limitation is stated in a note rather than in the parameter list: the function returns information only about running threads, and the return value will be empty if the id identifies a thread that has already terminated. The object exists for the thread's lifetime; this call is not an archive of dead threads.

ThreadDataEnum

function ThreadDataEnum(const AForceAllActive: Boolean = False): TEurekaThreadDataList;   // EThreadsManager

Enumerates running threads and returns them as a TEurekaThreadDataList - a snapshot list, described in section 15.6. You have to delete the returned object after use; a new object is created for every call.

AForceAllActive is the interesting part, because the two settings answer two different questions:

  • False (the default) returns a copy of EurekaLog's internal thread list, which contains all known threads. The page warns that known threads may not represent all threads in your application - for example, if you did not install low-level hooks, or if EurekaLog is disabled in system threads. This is the answer that reflects what EurekaLog has actually been tracking.
  • True builds and returns a list of all running threads in the application - only active, still-running threads. Use this when the internal known-thread list is not enough; enumeration does not imply that full creation metadata was captured for every thread.

The same two caveats as above apply: no locking is needed, because the list is created for your call alone, and changes to dynamic properties are ignored - use the ThreadData function to modify a thread's dynamic properties.

For an on-demand diagnostic, this helper writes known threads into an already-open application log (chapter 13). Put it in a unit using SysUtils, EThreadsManager and ELogging; call it after EurekaLog initialization, not after its units have finalized:

uses
  SysUtils, EThreadsManager, ELogging;

procedure LogKnownThreads;
var
  Threads: TEurekaThreadDataList;
  I: Integer;
begin
  Threads := ThreadDataEnum(False);
  try
    for I := 0 to Threads.Count - 1 do
      ELog('Known thread',
        IntToStr(Threads[I].ID) + ': ' + Threads[I].Name);
  finally
    Threads.Free;
  end;
end;

An absent thread in this list means it was not in the known-thread snapshot; it does not prove the process had no such worker. Use ThreadDataEnum(True) when you need an all-running-threads snapshot. Neither mode is a history of terminated threads, and a thread can terminate before you finish reading the returned data. Do not keep an item reference after freeing its list.

15.2 Which thread is this?

The identity of a thread is not the same thing as its handle, and the documents make a point of saying so. This group is the set of members that answer "which thread", in the terms EurekaLog uses internally and in the terms the operating system uses.

ID and ParentID

property ID;        // the thread's TID
property ParentID;  // the id of the thread that created this one; 0 for the main thread

ID stores the TID that uniquely identifies the thread in a running application, and the page adds the detail that matters for the rest of the API: the TID is used as a key for thread lists. That is the value AcquireThreadData takes and the value the list's ThreadData method takes.

ParentID is the id of the thread which created this one - the thread which invoked CreateThread/BeginThread for it - and it is 0 for the main thread, which had no creating thread.

Both are detected on the thread's creation and are not changed during its lifetime.

Handle

property Handle;   // an opened thread handle, with THREAD_QUERY_INFORMATION, THREAD_TERMINATE or SYNCHRONIZE rights or better

The handle EurekaLog opened for the thread - usually with full access. It is opened when the thread starts and closed when the thread terminates, and it does not change while the thread lives.

Two documented cautions, and both are about treating this as an identity:

  • It is not equal to the value returned by CreateThread/BeginThread, nor to TThread.Handle.
  • More strongly, the page carries a warning: a handle could not be used to identify threads, because a single thread may have several different handles opened. Use the ID property to identify threads.

This is EurekaLog's handle, not one you own. Do not close it or assume a copied numeric handle remains usable after the target exits. For an operation that needs a live OS handle, retain one under your application's own thread-lifetime protocol.

IsMainThread, IsRTLThread and IsWindowsThread

property IsMainThread;    // True for the main thread, False for background threads
property IsRTLThread;     // True for RTL threads, False for Windows threads
property IsWindowsThread; // True for Windows threads, False for RTL threads

Three Boolean questions with very different definitions behind them.

IsMainThread identifies the thread whose TID equals the MainThreadID global variable. There is only one such thread in a running application.

IsRTLThread and IsWindowsThread are, in the page's words, "the opposite" of one another, and they are really one distinction with two sides: a thread created with BeginThread or the TThread class is an RTL thread; a thread created with CreateThread is a Windows thread. The distinction is about who owns the thread's entry point, not about what the thread does.

One note follows from that definition and is easy to get wrong when you read code that tests these properties: the main thread is also considered to be an RTL thread.

As with the rest of the identity group, all three are detected on the thread's creation and do not change during its lifetime.

Kind, and the TThreadKind type

property Kind;   // the thread's kind

type
  TThreadKind = (
    tkUnknown,
    tkGeneric,        // Windows thread (created with CreateThread)
    tkBeginThread,    // RTL thread (created with BeginThread)
    tkBeginThreadEx,  // RTL thread (created with BeginThreadEx function)
    tkTThread,        // RTL thread (created with TThread's descendant)
    tkTThreadEx);     // RTL thread (created with TThreadEx class)

Kind is the value behind the RTL/Windows pair above: it indicates the thread kind, "Windows/RTL thread", and it is detected on the thread's creation and never changed.

TThreadKind is the enumeration it holds, and it is finer-grained than the two Booleans suggest: it records how the thread was made, not just which family it belongs to. The identifiers compare in their declared order. No public semantics are specified for tkUnknown.

So the four RTL values all report IsRTLThread = True and IsWindowsThread = False, while tkGeneric is the one that reports the reverse; tkTThreadEx keeps a thread created with the TThreadEx class distinct from one created with a plain TThread descendant.

15.3 What the report calls the thread

Name

property Name;   // optional thread name (description); arbitrary string, default empty

An arbitrary string that describes the thread. It is what makes a multi-threaded report readable: the page says thread names are "used in bug reports to describe thread", which is why naming threads simplifies debugging.

The rules the page lays down:

  • It is optional, and it does not have to be unique - two threads may carry the same name.
  • The default value is an empty string.
  • The name of the main thread is always 'Main thread'.
  • It is a dynamic property: it can be changed while the thread is running. The recommended practice the page gives is to set the thread's name immediately after the thread's start and not change it later.
  • A name can be set more than once, and the most recently set value is used.

The page names two functions for setting it - NameThread and NameThreadForDebugging - and both belong to EBase, not to this unit; NameThread is covered in chapter 6. Because Name is dynamic, it is also the property at the centre of the ownership rule that runs through this whole chapter: setting it through an object obtained from one of the two copying accessors does nothing. The documentation's phrasing, repeated on AcquireThreadData, ThreadDataEnum and the list's ThreadData method, is that any change for dynamic properties is ignored and that the ThreadData function is what modifies a thread's dynamic properties. In practice: read a copied object's name freely; write the name from the thread itself.

LastException and LastHTMLPage are also dynamic properties. No additional public contract is specified for them here; their dynamic status does not establish ownership or lifetime guarantees.

15.4 Where the thread started, and who started it

This group is the plumbing for stack reconstruction. The object stores the thread's entry points - the usual one and the real one - and the place that created the thread, each with a human-readable description derived from the debug information, plus the raw argument the entry point was given. The Starting/RealStarting pair exists because for an RTL thread the two are not the same function, and EurekaLog keeps both.

StartingAddress and StartingFunction

property StartingAddress;   // a pointer to the usual thread function
property StartingFunction;  // a human-readable description of StartingAddress

StartingAddress points to the usual thread function - the function passed directly to CreateThread, regardless of the thread's kind. StartingFunction is the description of that address, and the page is precise about where the name comes from: it is the function name identified in StartingAddress, taken from debug information.

The page's note ties this pair to the next one: for Windows threads StartingAddress equals RealStartingAddress, and for RTL threads it differs. For a thread created with BeginThread or TThread, the address handed to CreateThread is an RTL wrapper, so the "usual" function is the wrapper and the real one is elsewhere.

Both values are detected on the thread's creation and are not changed during its lifetime.

RealStartingAddress and RealStartingFunction

property RealStartingAddress;   // a pointer to the real thread function
property RealStartingFunction;  // a human-readable description of RealStartingAddress

RealStartingAddress points to the real thread function - the thread function for CreateThread/BeginThread/TThread.Execute. In the page's words, the usual thread function is the one passed to CreateThread, and that value is stored in StartingAddress; this property is the one behind it. RealStartingFunction is its description, again taken from debug information.

When you are looking at a report of an RTL thread and want the routine the developer actually wrote, this is the pair to read.

Parameter and RealParameter

property Parameter;      // the RAW pointer argument for the StartingAddress thread function
property RealParameter;  // the RAW pointer argument for the RealStartingAddress thread function

The arguments the two entry points were called with. Both pages describe them in the same terms - a RAW pointer argument for the thread function identified by the corresponding address property - which is the honest description: the value is whatever was passed, with no knowledge of what it points to. Interpret it only if you know what the thread function expects.

Both are detected on the thread's creation and are not changed during its lifetime.

CallerAddress and CallerFunction

property CallerAddress;   // a pointer into the code that called CreateThread/BeginThread/TThread.Create
property CallerFunction;  // a human-readable description of CallerAddress

Who created the thread. The page is careful about what the pointer means: it points to code which called CreateThread/BeginThread/TThread.Create, and it points somewhere inside a function - it does not point to a function. CallerFunction is its description, with the function name taken from debug information.

The documented purpose is worth knowing, because it explains why the property exists: CallerAddress is used to identify the thread's creator, and it is inserted as the very first (bottom) entry in the thread's call stack to indicate where the thread came from. That is what turns the stack of a worker thread into something with a visible beginning instead of a bare list of frames.

Like the rest of this group, CallerAddress is detected on the thread's creation and never changed.

Thread and ThreadClassName

property Thread;           // the thread class instance for the thread, or nil
property ThreadClassName;  // the class name of the class of that object

Thread stores the actual thread object - typically a TThread instance - and the page instructs you to typecast it to the appropriate thread class before using it. What it holds depends on the kind of thread:

  • It is nil for Windows threads, and for threads created with a naked BeginThread function.
  • It is not nil for TThread threads.

ThreadClassName is the class name of the object in Thread - usually the ClassName of a TThread descendant - and it is detected on the thread's creation and does not change.

The two properties differ in one subtle way, and the page says it: the object reference itself is detected on the thread's creation and is not changed during the thread's lifetime, but the properties of the thread object may change. Knowing which object represents the thread does not make that object's state immutable.

StackTop

property StackTop;   // a pointer to the beginning of the stack (top high address)

The top of the thread's stack - the page's value text is "a pointer to the beginning of the stack (top high address)", and it says plainly what the value is for: it is used for call stack creation.

15.5 Priority

public
function Priority: Integer;   // TEurekaThreadData

Retrieves the priority value for the thread. The page gives the mechanism and the range: the return value is the one returned by GetThreadPriority, and the possible range is -15..15. The value, together with the priority class of the thread's process, determines the thread's base-priority level. The number is therefore relative to that class rather than absolute: the same value means different base priorities in processes whose priority classes differ.

The page lists the standard values, useful because the raw number is not self-explanatory:

  • -15 (THREAD_PRIORITY_IDLE): base priority of 1 for usual processes, or 16 for REALTIME_PRIORITY_CLASS processes.
  • -2 (THREAD_PRIORITY_LOWEST): 2 points below the priority class.
  • -1 (THREAD_PRIORITY_BELOW_NORMAL): 1 point below the priority class.
  • 0 (THREAD_PRIORITY_NORMAL): normal priority for the priority class.
  • 1 (THREAD_PRIORITY_ABOVE_NORMAL): 1 point above the priority class.
  • 2 (THREAD_PRIORITY_HIGHEST): 2 points above the priority class.
  • 15 (THREAD_PRIORITY_TIME_CRITICAL): base-priority level of 15 for usual processes, or 31 for REALTIME_PRIORITY_CLASS processes.

These are the ordinary priority levels, not a guarantee that every integer between them is a valid priority setting. Priority is a query, not a setter; do not use it to establish a worker's scheduling or lifetime.

15.6 All threads at once: TEurekaThreadDataList

TEurekaThreadDataList is the "List of threads" the unit's class index promises, and it is what ThreadDataEnum returns. Like TEurekaThreadData itself, it is declared as class(TCheckedInterfacedObject). It is a snapshot, not a window onto EurekaLog's own list: the list and all of its items are created for your call, which is also why the pages repeat that no locking is needed and that changing anything dynamic through it is ignored.

Count

property Count;   // count of threads in the list

The number of threads in the current list. The page gives the intended use directly: use this property to enumerate all threads via the Items property. If you are interested in a particular thread instead, use the ThreadData method.

Items

property Items[AIndex];   // access to individual threads in the list; AIndex in 0..Count-1

Access to the individual threads in the list, and the default property of the class - so the list can be indexed directly. The page states the parameter's contract: AIndex is the index of a thread in the list and should be in the 0..Count-1 range.

That is the whole enumeration recipe: Count for the bound, Items for the elements.

TEurekaThreadDataList.ThreadData

property TEurekaThreadDataList.ThreadData[AThreadID];   // returns information about a particular thread

Use this indexed property to retrieve information for a thread ID. It shares its name with the unit's ThreadData function, so take care which one you are calling.

The contract, in the page's own words:

  • The returned object must not be destroyed, and there is no need to release it.
  • The value could be nil if AThreadID does not identify a valid running thread.
  • No locking is needed, because the list and all of its items are created upon the ThreadDataEnum call.
  • Changes to dynamic properties are ignored - the page names Name, LastException and LastHTMLPage - and the ThreadData function is what modifies a thread's dynamic properties.

The last two points describe the same design from two sides: the list and all of its items are created for your call, and nothing written through them reaches the objects EurekaLog keeps for the threads themselves.

15.7 Lifetimes, ownership and locking

Before keeping a value beyond the immediate lookup, decide what owns it and what can disappear.

Who owns the object. Exactly one accessor hands out the live object: ThreadData (chapter 6), for the current thread, and it comes with two instructions - do not destroy the returned object, and there is no need to release it. The other two hand out objects you own:

Call Object What you must do
ThreadData live object, current thread do not destroy
AcquireThreadData a new copy, per call destroy it
ThreadDataEnum a new list, per call delete it
TEurekaThreadDataList.ThreadData an item of a list you already own do not destroy

AcquireThreadData creates a new object for each call; release that object after use. ThreadDataEnum similarly creates a new list that you must delete after use. The list's items, by contrast, belong to their list.

When the object stops existing. ThreadData and AcquireThreadData both document that they can return an empty result in rare cases, and both name the time before EThreadsManager's initialization or after its finalization as an example - an example, not an exhaustive list. No corresponding empty-result case is specified for ThreadDataEnum. Outside those rare cases, the live object returned by ThreadData exists until the current thread terminates. AcquireThreadData returns information only about running threads; its return value is empty for an id that identifies an already terminated thread. If you need a non-nil result whatever happens, AcquireThreadData's AForce argument gives you a dummy object instead - but note what that means: a non-nil return no longer tells you that the thread existed.

Locking. No locking is needed for the copying accessors, because the informational object is individual to the call. For ThreadData the reason is different: the object for the current thread exists until that thread terminates, and most thread properties are read-only and do not require synchronisation. The restriction on the live object concerns writing, not reading: dynamic properties such as Name, LastException and LastHTMLPage should be modified by the current thread only.

Read versus write. The copying accessors are read-only in effect: changes to their dynamic properties are ignored. Use ThreadData to modify the current thread's own dynamic properties. Threads created elsewhere are best set up by the thread itself, as soon as it starts; name the thread immediately.

Two things not to confuse. A thread's Handle is not its identity - a thread may have several handles, and the documentation forbids using the handle to identify threads; ID is the identity, and it is the key of every lookup shown here. And a thread's Thread object is often nil: it is nil for Windows threads and for threads created with a naked BeginThread, and non-nil for TThread threads, so code that reads it must test it.

Metadata is not a suspended stack. ThreadDataEnum does not freeze the application or capture all stacks. For report-time stacks, use Features / Multi-threading / Include stacks of EurekaLog-enabled threads; this adds collection work but does not enable handlers. For an on-demand stack, chapter 11 describes the stack-building API. BuildForThread requires a handle with THREAD_GET_CONTEXT, THREAD_QUERY_INFORMATION and THREAD_SUSPEND_RESUME rights; do not pass the current thread to that background-thread API. The target resumes execution and its actual stack can differ immediately after capture.

The snapshot's Thread pointer is not an ownership transfer. A non-nil pointer is not permission to dereference a FreeOnTerminate object without a lifetime agreement. Likewise, raw Parameter and RealParameter pointers say nothing about whether their pointed-to data still exists.

Thread state. Whether EurekaLog is active in a thread is a different question from anything on this object, and it is answered by IsEurekaLogActiveInThread and changed with SetEurekaLogStateInThread, both covered in chapter 6.