CMSDK 2.0.1
Cross-platform C++ base library and SDK for the Psyclone AIOS platform
Loading...
Searching...
No Matches
ThreadManager.cpp
Go to the documentation of this file.
1
15#include "ThreadManager.h"
16#include "PsyTime.h"
17#include "Utils.h"
18#include "UnitTestFramework.h"
19
20namespace cmlabs {
21
23
24// Single-teardown guard. shutdown() force-terminates and joins the registered
25// worker threads while holding the registry mutex. If a SECOND driver enters
26// shutdown() concurrently on another thread - e.g. a signal-driven exit() runs
27// CleanShutdown::~CleanShutdown() -> ThreadManager::Shutdown() on a worker while
28// main is already tearing the node down - the two collide on the mutex/join and
29// deadlock. This flag lets only the FIRST caller run the teardown; a concurrent
30// second caller returns immediately. It is scoped to the in-flight teardown (set
31// on entry, cleared on exit), NOT a permanent latch, so sequential teardowns
32// (e.g. one per CMSDK network unit test in fork=0 mode) still work.
33static volatile int32 g_threadMgrTearingDown = 0;
34
35// Atomic test-and-set: returns true iff it flipped 0 -> 1 (i.e. WE won the race
36// and now own the teardown). Mirrors the WINDOWS / __sync idiom used in Utils.cpp.
38 #if defined WINDOWS
39 return (_InterlockedCompareExchange((volatile long*)&g_threadMgrTearingDown, 1, 0) == 0);
40 #else
41 return __sync_bool_compare_and_swap(&g_threadMgrTearingDown, 0, 1);
42 #endif
43}
44
45static void EndThreadMgrTeardown() {
46 #if defined WINDOWS
47 _InterlockedExchange((volatile long*)&g_threadMgrTearingDown, 0);
48 #else
49 __sync_lock_test_and_set(&g_threadMgrTearingDown, 0);
50 #endif
51}
52
54 ThreadManager* threadManager = new ThreadManager();
55 if (!threadManager->init()) {
56 //fprintf(stderr, "ThreadManager init() failed...\n");
57 delete(threadManager);
58 return false;
59 }
60 return true;
61}
62
63// Static
64// Create a new thread and start it
65bool ThreadManager::CreateThread(THREAD_FUNCTION func, void* args, uint32& newID, uint32 reqID) {
68 return false;
69 return ThreadManager::Singleton->createThread(func, args, newID, reqID);
70}
71
72// Static
73// Pause the thread wherever it is now
77 return false;
78 return ThreadManager::Singleton->pauseThread(id);
79}
80
81// Static
82// Allow the thread to continue running
86 return false;
87 return ThreadManager::Singleton->continueThread(id);
88}
89
90// Static
91// Terminate the thread and restart it from its base location
95 return false;
96 return ThreadManager::Singleton->interruptThread(id);
97}
98
99// Static
100// Terminate and destroy the thread
103 if (!CreateThreadManager())
104 return false;
105 return ThreadManager::Singleton->terminateThread(id);
106}
107
108
109// Static
110// Terminate and destroy the thread
113 if (!CreateThreadManager())
114 return false;
115 return ThreadManager::Singleton->isThreadRunning(id);
116}
117
118
119// Static
120// Wait for a thread to have COMPLETELY finished, then release it - exactly once
123 if (!CreateThreadManager())
124 return false;
125 return ThreadManager::Singleton->joinThread(id);
126}
127
128
129// Static
130// Copy out the OS-level ThreadHandle for a manager slot
133 if (!CreateThreadManager())
134 return false;
135 return ThreadManager::Singleton->getThreadHandle(id, out);
136}
137
138
139// Static
140// Add thread stats for local thread - needed for pthreads
143 if (!CreateThreadManager())
144 return false;
145 return ThreadManager::Singleton->addLocalThreadStats();
146}
147
148// Static
149// Add thread stats for local thread - needed for pthreads
152 if (!CreateThreadManager())
153 return false;
154 return ThreadManager::Singleton->getLocalThreadID(id);
155}
156
157
158// Static
159// Get the statistics for local thread
161 ThreadStats stats;
162 stats.created = 0;
164 if (!CreateThreadManager())
165 return stats;
166 uint32 id;
168 return stats;
169 return ThreadManager::Singleton->getThreadStats(id);
170}
171
172// Static
173// Get the statistics for a thread
175 ThreadStats stats;
176 stats.created = 0;
178 if (!CreateThreadManager())
179 return stats;
180 return ThreadManager::Singleton->getThreadStats(id);
181}
182
183// Static
184// Get the statistics for all threads
187 if (!CreateThreadManager())
188 return NULL;
189 return ThreadManager::Singleton->getAllThreadStats(count);
190}
191
192// Static
193// Shutdown and delete the ThreadManager
196 return true;
198 return true;
201 return (ThreadManager::Singleton == NULL);
202}
203
204
205
206
208 // Initially allocate space for 1024 threads
209 data = NULL;
210 // Make the registry lock cancel-safe: teardown force-terminates worker threads
211 // via pthread_cancel, and a cancel landing on a thread that holds this lock
212 // (e.g. mid createThread/addLocalThreadStats) would orphan it and hang every
213 // later terminate/isRunning call for 30s. Disabling cancellation while the lock
214 // is held closes that window. See utils::Mutex::setCancelSafe().
215 mutex.setCancelSafe(true);
217 shouldContinue = true;
218 isRunning = false;
219}
220
222 if (ThreadManager::Singleton == NULL)
223 return;
224 if (!mutex.enter())
225 return;
226 shutdown();
227 // Deallocate data
229 free(data);
230 data = NULL;
231 mutex.leave();
232}
233
235 if (!mutex.enter())
236 return false;
237 // Startup the root thread for Windows only
238 uint32 newID = 0;
240 if ((!createThread(ThreadMonitoring, NULL, newID, 0)) || (newID != 0)) {
241 mutex.leave();
242 return false;
243 }
245 header->activeCount = 1;
246 mutex.leave();
247 return true;
248}
249
251 // Single-teardown guard: if another thread is already tearing the registry
252 // down, do NOT run a second concurrent teardown (it would collide on the
253 // registry mutex / thread joins and deadlock). Return false so the static
254 // Shutdown() wrapper skips deleting the singleton out from under the
255 // in-flight teardown. See g_threadMgrTearingDown.
257 return false;
258 stop();
259 if (!mutex.enter()) {
261 return false;
262 }
263 // Give running threads a chance to exit on their own before resorting to
264 // utils::TerminateThread(). Forcibly terminating a thread that is right now
265 // exiting naturally (worker loops poll their stop flags every <=1s) can kill
266 // it inside DLL_THREAD_DETACH / CRT teardown while it holds the loader or
267 // heap lock. That abandoned lock then blocks every NEWLY created thread
268 // before it reaches its thread function - the cause of the intermittent
269 // CMSDK processmemory/network test failures on Windows.
271 ThreadStats* stats;
272#ifdef WINDOWS
273 // The natural-exit wait is only needed on Windows, where the OS
274 // TerminateThread() can abandon the loader/heap lock. On POSIX,
275 // utils::TerminateThread() is pthread_cancel()+pthread_join(), which is
276 // safe to apply immediately (and waiting merely shifts where the
277 // cancellation lands, which destabilised the network tests on Linux).
278 uint64 waitStart = GetTimeNow();
279 bool anyRunning = true;
280 while (anyRunning && GetTimeAgeMS(waitStart) < 2500) {
281 anyRunning = false;
282 stats = GETTHREADSTATS(data,0);
283 for (uint32 n=0; n<header->count; n++, stats++) {
284 if (stats->status > THREAD_TERMINATED &&
285 !utils::TryReapThread(stats->hThread)) {
286 anyRunning = true;
287 break;
288 }
289 }
290 if (anyRunning)
291 utils::Sleep(50);
292 }
293#endif
294 // Terminate whatever is genuinely still running, mark the rest terminated
295 stats = GETTHREADSTATS(data,0);
296 for (uint32 n=0; n<header->count; n++) {
297 if (stats->status > THREAD_TERMINATED) {
298 terminateThread(stats->id);
299 }
300 stats += 1;
301 }
302 header->activeCount = 0;
303 mutex.leave();
305 return true;
306}
307
308
309
310
311// Create a new thread and start it
312bool ThreadManager::createThread(THREAD_FUNCTION func, void* args, uint32& newID, uint32 reqID) {
313// fprintf(stderr, "ThreadManager creating thread enter...\n");
314 if (!mutex.enter())
315 return false;
316// fprintf(stderr, "ThreadManager creating thread got mutex...\n");
318 if (header->activeCount == header->count) {
319 // we need to resize Thread storage
320 if (!resizeThreadStorage(header->count * 4)) {
321 mutex.leave();
322 return false;
323 }
324 header = (ThreadDataHeader*)data;
325 }
326
327// fprintf(stderr, "ThreadManager creating thread 1...\n");
328 ThreadStats* stats;
329 if (reqID == 0) {
330 // A bit cheeky as root = 0, but it works well for the root thread as this is the first one requested anyway.
331 if (!utils::GetFirstFreeBitLoc(((const char*)data+sizeof(ThreadDataHeader)), header->bitFieldSize, newID) || (newID >= header->count)) {
332 mutex.leave();
333 return false;
334 }
335 stats = GETTHREADSTATS(data, newID);
336 stats->id = newID;
337 }
338 else {
339 newID = reqID;
340 stats = GETTHREADSTATS(data, newID);
341 stats->id = newID;
342 }
343
344// fprintf(stderr, "ThreadManager creating thread 2...\n");
345 stats->created = GetTimeNow();
346 stats->status = THREAD_INIT;
347 // We assume the stats are 0 already
348 // Create the thread
349 if (!utils::CreateThread(func, args, stats->hThread, stats->osID)) {
350 newID = 0;
351 stats->created = 0;
352 stats->status = THREAD_NONE;
353 mutex.leave();
354 return false;
355 }
356
357 // printf("Create thread %u = %u\n", stats->id, (uint32)stats->hThread);
358 utils::SetBit(stats->id, BITOCCUPIED, ((char*)data+sizeof(ThreadDataHeader)), header->bitFieldSize);
359 stats->func = func;
360 stats->args = args;
361 stats->status = THREAD_RUNNING;
362 header->activeCount++;
363 //fprintf(stderr, "ThreadManager creating thread ID %u, trace:\n%s", stats->osID);
364 //fprintf(stderr, "ThreadManager creating thread ID %u, trace:\n%s", stats->osID, utils::PrintProgramTrace(3, 6).c_str());
365 mutex.leave();
366// fprintf(stderr, "ThreadManager creating thread 4...\n");
367 return true;
368}
369
370// Pause the thread wherever it is now
372 if (!mutex.enter())
373 return false;
374 ThreadStats* stats = GETTHREADSTATS(data, id);
375 // Pause the thread
376 if (!utils::PauseThread(stats->hThread)) {
377 mutex.leave();
378 return false;
379 }
380 stats->status = THREAD_PAUSED;
381 mutex.leave();
382 return true;
383}
384
385// Allow the thread to continue running
387 if (!mutex.enter())
388 return false;
389 ThreadStats* stats = GETTHREADSTATS(data, id);
390 // Continue the thread
391 if (!utils::ContinueThread(stats->hThread)) {
392 mutex.leave();
393 return false;
394 }
395 stats->status = THREAD_RUNNING;
396 mutex.leave();
397 return true;
398}
399
400// Terminate the thread and restart it from its base location
402 if (!mutex.enter())
403 return false;
404 ThreadStats* stats = GETTHREADSTATS(data, id);
405 stats->status = THREAD_INTERRUPTED;
406 // Terminate the thread
407 // Terminate the thread
408 if (!utils::TerminateThread(stats->hThread)) {
409 mutex.leave();
410 return false;
411 }
413 header->activeCount--; // it will be increased again in createThread
414 // Create the thread again
415 uint32 newID = 0;
416 if (!createThread(stats->func, stats->args, newID, id)) {
417 stats->status = THREAD_TERMINATED;
418 stats->func = NULL;
419 stats->created = 0;
420 header->activeCount--;
421 mutex.leave();
422 return false;
423 }
424 mutex.leave();
425 return false;
426}
427
428// Terminate and destroy the thread
430 if (!mutex.enter())
431 return false;
433 ThreadStats* stats = GETTHREADSTATS(data, id);
434 // Only force-terminate a thread that is actually still running. Killing a
435 // thread that already exited (or is mid-exit) can abandon the loader/heap
436 // lock and silently break all subsequent thread creation (see shutdown()).
437 if (!utils::TryReapThread(stats->hThread)) {
438#ifdef WINDOWS
439 // brief grace period for threads that are just about to exit; killing a
440 // thread inside its exit path can abandon the loader/heap lock (Windows)
441 uint64 graceStart = GetTimeNow();
442 while (!utils::TryReapThread(stats->hThread) && GetTimeAgeMS(graceStart) < 250)
443 utils::Sleep(25);
444 if (stats->hThread) {
445 utils::TerminateThread(stats->hThread); // terminates+closes (Windows)
446 stats->hThread = 0;
447 }
448#else
449 if (stats->hThread) {
450 // Never cancel+join the CALLING thread. If teardown is being driven from
451 // a registered worker (e.g. exit() -> CleanShutdown -> Shutdown() runs on
452 // a bake/network worker), shutdown()'s loop will reach that worker's own
453 // slot; pthread_cancel+pthread_join on self is undefined / self-deadlocks.
454 // Just detach the handle and mark it terminated; the thread is on its way
455 // out through exit() anyway.
456 if (pthread_equal(stats->hThread, pthread_self())) {
457 stats->hThread = 0;
458 }
459 else {
460 // The blocking part of TerminateThread on POSIX is pthread_cancel +
461 // pthread_join. It MUST NOT run while we hold the registry mutex: the
462 // target may be blocked in Mutex::enter() for this same lock, and while
463 // the lock is held it cannot acquire it - so joining here would deadlock
464 // main against the worker. Snapshot the handle, zero it in the slot so no
465 // other caller races the same join (double-join is undefined behaviour),
466 // release the mutex, then cancel+join. Re-acquire and re-fetch afterwards
467 // because the storage block may have been resized meanwhile.
468 ThreadHandle h = stats->hThread;
469 stats->hThread = 0;
470 mutex.leave();
471 utils::TerminateThread(h); // cancels + joins - NO registry lock held
472 if (!mutex.enter())
473 return false;
474 header = (ThreadDataHeader*) data;
475 stats = GETTHREADSTATS(data, id);
476 }
477 }
478#endif
479 }
480 //if (utils::IsThreadRunning(stats->hThread)) {
481 //if (!utils::TerminateThread(stats->hThread)) {
482 // mutex.leave();
483 // return false;
484 //}
485 //}
486 // Finalize idempotently: another terminate/monitor pass may have already
487 // reaped this slot while the mutex was released above.
488 finaliseSlot(stats, header);
489 mutex.leave();
490 return true;
491}
492
493
494// Has the thread terminated or exited
496 if (!mutex.enter())
497 return false;
499 ThreadStats* stats = GETTHREADSTATS(data, id);
500 if (stats->hThread == 0) {
501 mutex.leave(); // was leaking the manager mutex on this early return
502 return false;
503 }
504 bool res = utils::IsThreadRunning(stats->hThread);
505 mutex.leave();
506 return res;
507}
508
509
510// Copy out the OS-level ThreadHandle for a slot (by value, under the mutex)
512 if (!mutex.enter())
513 return false;
514 ThreadStats* stats = GETTHREADSTATS(data, id);
515 if (stats->hThread == 0) {
516 mutex.leave();
517 return false;
518 }
519 out = stats->hThread;
520 mutex.leave();
521 return true;
522}
523
524
525// Wait for a thread to have COMPLETELY finished, then release it - exactly once.
526// This is the ONLY sound "that thread is entirely gone" signal. A flag the worker sets
527// itself is necessarily set BEFORE it returns from its thread function, and long before
528// the runtime tears down its stack/TSD - so a reaper that frees on such a flag always
529// races the thread's own exit path (observed: a worker still inside pthread_exit/TSD
530// cleanup after its owning object had been freed).
532 if (!mutex.enter())
533 return false;
535 ThreadStats* stats = GETTHREADSTATS(data, id);
536 ThreadHandle h = stats->hThread;
537 if (h == 0) {
538 // Already reaped - by terminateThread(), the monitoring loop, or an earlier
539 // join. Nothing left to wait for.
540 mutex.leave();
541 return true;
542 }
543 #ifndef WINDOWS
544 // NEVER join the CALLING thread: teardown can be driven from a registered worker
545 // (exit() -> CleanShutdown -> Shutdown()), and pthread_join on self is undefined
546 // / self-deadlocks. Leave the handle for the reaper that outlives us.
547 if (pthread_equal(h, pthread_self())) {
548 mutex.leave();
549 return true;
550 }
551 #endif
552 // TAKE EXCLUSIVE OWNERSHIP before joining: zero the slot so no other joiner - a
553 // concurrent terminateThread(), or the monitoring loop's TryReapThread() (Linux
554 // only; the loop exits immediately when statColPolicy is THREAD_STATS_OFF, which
555 // is the case on macOS) - can ever join the same pthread_t twice. Double-join is
556 // undefined behaviour and corrupts the heap. Correctness rests on THIS, not on
557 // the monitor being absent.
558 stats->hThread = 0;
559 // Release the registry mutex BEFORE joining: the target may itself be blocked in
560 // Mutex::enter() for this very lock, and while we hold it the target can never
561 // acquire it - so joining here would deadlock us against the thread we wait for.
562 mutex.leave();
563 #ifdef WINDOWS
564 // ReapThread() only CloseHandle()s on Windows - it does NOT wait - so the wait
565 // must be explicit first. Timeout 0 = infinite WaitForSingleObject.
568 #else
569 // POSIX: pthread_join both waits and releases the thread's resources, and
570 // ReapThread() zeroes its local copy of the handle afterwards.
571 // UNBOUNDED on purpose: a bounded join (pthread_timedjoin_np) that expires
572 // after we have zeroed the slot would leave the thread joinable by nobody -
573 // silently dropping the guarantee. Callers bound the COOPERATIVE phase
574 // (shouldContinue + a bounded isRunning wait + a loud log on overrun) and
575 // only then call this.
577 #endif
578 // Re-acquire and RE-FETCH: the storage block is malloc/memcpy'd and may have been
579 // resized while the mutex was released, so the earlier pointers can be stale.
580 if (!mutex.enter())
581 return true; // the thread IS joined; only the bookkeeping below is missed
582 header = (ThreadDataHeader*) data;
583 stats = GETTHREADSTATS(data, id);
584 // Finalise idempotently: terminateThread() or a monitor pass may already have
585 // marked this slot while our mutex was released. This RELEASES THE SLOT as well
586 // as marking it - see finaliseSlot(). It used to be an inline copy of
587 // terminateThread's block that did not free the bit, so every join leaked a
588 // registry slot: the leak survived its own fix on the path that matters most,
589 // since all four NetworkChannel teardown sites reap via JoinThread and on macOS
590 // the monitor loop never reaps at all. We have already zeroed stats->hThread
591 // above, so finaliseSlot's handle guard is always satisfied here.
592 finaliseSlot(stats, header);
593 mutex.leave();
594 return true;
595}
596
597
598// Mark a slot terminated and release it back to the allocation bitfield.
599// Caller must hold the registry mutex, with stats/header re-fetched after any
600// release of it. Shared by terminateThread() and joinThread() so the slot-release
601// cannot be present in one and missing in the other. @see finaliseSlot() docs.
603 if (!stats || !header)
604 return;
605 if (stats->status == THREAD_TERMINATED)
606 return; // already finalised by another terminate/join/monitor pass
607 stats->status = THREAD_TERMINATED;
608 stats->func = NULL;
609 stats->created = 0;
610 header->activeCount--;
611 // Release the registry slot back to the bitfield, or the process leaks one slot
612 // per thread lifetime: createThread only ever SetBit(BITOCCUPIED) and nothing
613 // used to clear it, so after `count` create/terminate cycles GetFirstFreeBitLoc
614 // had nothing left and thread creation failed forever. resizeThreadStorage could
615 // not rescue it either - it triggers on activeCount == count, and activeCount
616 // DOES decrement here, so the registry looked like it had spare capacity while
617 // being unable to allocate.
618 //
619 // ORDERING IS LOAD-BEARING: only release the bit once hThread is zero. Freeing it
620 // while the OS thread is still unwinding would let a concurrent createThread hand
621 // out this slot and overwrite ThreadStats under the dying thread - trading a
622 // capacity bug for a use-after-free. Callers either zero the handle themselves or
623 // went through TryReapThread (which zeroes it), but assume nothing: a non-zero
624 // handle here means an unreaped thread, so keep the slot occupied and leak it,
625 // which is the old, safe behaviour.
626 if (!stats->hThread)
627 utils::SetBit(stats->id, BITFREE, ((char*)data+sizeof(ThreadDataHeader)), header->bitFieldSize);
628}
629
630
631// Get the statistics for a thread
633 ThreadStats stats;
634 stats.created = 0;
635 if (!mutex.enter()) {
636 mutex.leave();
637 return stats;
638 }
639 stats = *GETTHREADSTATS(data, id);
640 mutex.leave();
641 return stats;
642}
643
644// Get the statistics for all threads
646 if (!mutex.enter())
647 return NULL;
649 count = header->activeCount;
650 ThreadStats* allStats = new ThreadStats[count];
651 uint32 p=0;
652 ThreadStats* stats = GETTHREADSTATS(data,0);
653 for (uint32 n=0; n<header->count; n++) {
654 if (stats->status > THREAD_TERMINATED)
655 allStats[p++] = *stats;
656 stats += 1;
657 if (p >= count) break;
658 }
659 mutex.leave();
660 return allStats;
661}
662
663// Resize the ThreadStats storage to be able to contain more threads
665
666 uint32 bitFieldSize = utils::Calc32BitFieldSize(newCount);
667 uint32 size = sizeof(ThreadDataHeader) + bitFieldSize + newCount*sizeof(ThreadStats);
668 unsigned char *newData = (unsigned char*) malloc(size);
669 memset(newData, 0, size);
670 ThreadDataHeader* header = (ThreadDataHeader*) newData;
671 header->bitFieldSize = bitFieldSize;
672 header->count = newCount;
673 header->size = size;
675
676 uint32 statsOffset = sizeof(header->size)+sizeof(header->count)+sizeof(header->activeCount)+sizeof(header->statColPolicy);
677
678 if (data == NULL) {
679 header->activeCount = 0;
680 // Set summary stats to 0
681 memset(newData+statsOffset, 0, 10*2*(sizeof(uint64)));
682 // Set the bitfield to unused
683 memset(newData+sizeof(ThreadDataHeader), 255, bitFieldSize);
684 // Set all thread stats to 0
685 ThreadStats* firstStat = GETTHREADSTATS(newData,0);
686 memset(firstStat, 0, newCount*sizeof(ThreadStats));
687 }
688 else {
689 ThreadDataHeader* oldHeader = (ThreadDataHeader*) data;
690 header->activeCount = oldHeader->activeCount;
691 // Copy old summary stats
692 memcpy(newData+statsOffset, data+statsOffset, 10*2*(sizeof(uint64)));
693 // Initially, set the bitfield to unused
694 memset(newData+sizeof(ThreadDataHeader), 255, bitFieldSize);
695 // Copy old bitfield
696 memcpy(newData+sizeof(ThreadDataHeader), data+sizeof(ThreadDataHeader), oldHeader->bitFieldSize);
697 ThreadStats* firstStat = GETTHREADSTATS(newData,0);
698 ThreadStats* oldFirstStat = GETTHREADSTATS(data,0);
699 memcpy(firstStat, oldFirstStat, oldHeader->count*sizeof(ThreadStats));
700 free(data);
701 }
702 data = newData;
703 return true;
704}
705
706// Add thread stats for local thread - needed for pthreads
708 if (!mutex.enter())
709 return false;
710
711 uint32 id = 0;
712 if (!getLocalThreadID(id)) {
713 mutex.leave();
714 return false;
715 }
716 ThreadStats* stats = GETTHREADSTATS(data,id);
717 if ( (stats == NULL) || (stats->created == 0) ) {
718 mutex.leave();
719 return false;
720 }
721
722 uint32 timeOffset = sizeof(stats->id) + sizeof(stats->created) + sizeof(stats->status)
723 + sizeof(stats->hThread) + sizeof(stats->func) + sizeof(stats->args);
724 uint32 cpuUsageOffset = timeOffset + 10 * sizeof(uint64);
725
726 ThreadDataHeader* header = NULL;
727
728// uint32 headerTimeOffset = sizeof(header->size)+sizeof(header->count)+sizeof(header->activeCount)+sizeof(header->statColPolicy);
729// uint32 headerCPUUsageOffset = headerTimeOffset + 10 * sizeof(uint64);
730
731 // Shift the previously stored values Time
732 memcpy(stats+timeOffset, stats+timeOffset+sizeof(uint64), 9*sizeof(uint64));
733 // CPU Usage
734 memcpy(stats+cpuUsageOffset, stats+cpuUsageOffset+sizeof(uint64), 9*sizeof(uint64));
735 // Read current stats from the thread
736 if (utils::GetCPUTicks(stats->currentCPUTicks[0])) {
737 stats->time[0] = GetTimeNow();
738 // add these to the header stat sums
739 header = (ThreadDataHeader*) data;
740 header->time[0] = stats->time[0];
741 header->currentCPUTicks[0] += stats->currentCPUTicks[0];
742 // but do not rotate
743 }
744 else {
745 stats->time[0] = stats->currentCPUTicks[0] = 0;
746 mutex.leave();
747 return false;
748 }
749
750 mutex.leave();
751 return true;
752}
753
754// Get the local thread ID
756 if (!mutex.enter())
757 return false;
758
759 uint32 osID;
761 mutex.leave();
762 return false;
763 }
765
766 ThreadStats* stats = GETTHREADSTATS(data,0);
767 for (uint32 n=0; n<header->count; n++) {
768 if (stats->osID == osID) {
769 id = stats->id;
770 mutex.leave();
771 return true;
772 }
773 stats += 1;
774 }
775
776 // for pthreads one has to delete the handle afterwards
777 //#ifndef WINDOWS
778 // delete(hThread);
779 //#endif
780 mutex.leave();
781 return false;
782}
783
784
785// Root Thread, for Windows only until pthreads support getrusage from other threads
787
788 #ifdef WINDOWS
789 #else
790 #ifndef Darwin
791 sigset_t cancel;
792 sigemptyset(&cancel);
793 sigaddset(&cancel, SIGQUIT);
794 pthread_sigmask(SIG_UNBLOCK, &cancel, NULL);
795 #endif
796 #endif
797
799 return 0;
800
801 thread_ret_val(ThreadManager::Singleton->threadMonitoring());
802}
803
805
806 isRunning = true;
807
808 ThreadStats* stats = NULL;
809 uint32 timeOffset = sizeof(stats->id) + sizeof(stats->created) + sizeof(stats->status)
810 + sizeof(stats->hThread) + sizeof(stats->func) + sizeof(stats->args);
811 uint32 cpuUsageOffset = timeOffset + 10 * sizeof(uint64);
812
814 if (header->statColPolicy == THREAD_STATS_OFF) {
815 isRunning = false;
816 return 0; // do not continue as nothing is done anyway
817 }
818
819 uint32 headerTimeOffset = sizeof(header->size)+sizeof(header->count)+sizeof(header->activeCount)+sizeof(header->statColPolicy);
820 uint32 headerCPUUsageOffset = headerTimeOffset + 10 * sizeof(uint64);
821
822 uint64 sumUsage;
823 uint32 interval = 1000000, n;
824
825 uint64 lastCalc = 0, t;
826 while (shouldContinue) {
827 if ( (t = GetTimeNow()) - lastCalc > interval ) {
828 lastCalc = t;
829
830 if (!mutex.enter()) {
831 isRunning = false;
832 return -1;
833 }
834
835 header = (ThreadDataHeader*) data;
836 stats = GETTHREADSTATS(data,0);
837
838 if (header->statColPolicy == THREAD_STATS_ADHOC) {
839 // Threads have to call the stat gathering themselves
840 // so we just rotate the global stats
841
842 for (n=0; n<header->count; n++) {
843 if (stats->status > THREAD_TERMINATED) {
844 // check if it is still actually running
845 if (utils::TryReapThread(stats->hThread)) { // reaps exactly once; zeroes handle
846 stats->status = THREAD_TERMINATED;
847 LogPrint(0, LOG_PROCESS, 2, "Thread finished: ID %u (OSID %u) age: %s", stats->id, stats->osID, PrintTimeDifString((uint32)GetTimeAge(stats->created)).c_str());
848 }
849 }
850 stats += 1;
851 }
852
853 // Shift the previously stored values Time
854 memcpy(header+headerTimeOffset, header+headerTimeOffset+sizeof(uint64), 9*sizeof(uint64));
855 // CPU Usage
856 memcpy(header+headerCPUUsageOffset, header+headerCPUUsageOffset+sizeof(uint64), 9*sizeof(uint64));
857
858 header->time[0] = header->currentCPUTicks[0] = 0;
859 }
860 else {
861 // We have to gather the stats, sum them up and rotate all buffers
862 sumUsage = 0;
863
864 for (n=0; n<header->count; n++) {
865 if (stats->status > THREAD_TERMINATED) {
866 // Shift the previously stored values Time
867 memcpy(stats+timeOffset, stats+timeOffset+sizeof(uint64), 9*sizeof(uint64));
868 // CPU Usage
869 memcpy(stats+cpuUsageOffset, stats+cpuUsageOffset+sizeof(uint64), 9*sizeof(uint64));
870 // Read current stats from the thread
871 if (utils::GetCPUTicks(stats->hThread, stats->currentCPUTicks[0])) {
872 stats->time[0] = GetTimeNow();
873 sumUsage += stats->currentCPUTicks[0];
874 }
875 else {
876 stats->time[0] = stats->currentCPUTicks[0] = 0;
877 }
878 // check if it is still actually running
879 if (utils::TryReapThread(stats->hThread)) { // reaps exactly once; zeroes handle
880 stats->status = THREAD_TERMINATED;
881 LogPrint(0, LOG_PROCESS, 2, "Thread finished: ID %u (OSID %u) age: %s", stats->id, stats->osID, PrintTimeDifString((uint32)GetTimeAge(stats->created)).c_str());
882 }
883 }
884 stats += 1;
885 }
886
887 // Shift the previously stored values Time
888 memcpy(header+headerTimeOffset, header+headerTimeOffset+sizeof(uint64), 9*sizeof(uint64));
889 // CPU Usage
890 memcpy(header+headerCPUUsageOffset, header+headerCPUUsageOffset+sizeof(uint64), 9*sizeof(uint64));
891
892 header->time[0] = GetTimeNow();
893 header->currentCPUTicks[0] = sumUsage;
894 }
895
896 mutex.leave();
897 }
898
899 utils::Sleep(100);
900 }
901
902 isRunning = false;
903 return 0;
904}
905
906
907// ################# Unit test #################
908
909namespace {
910
911// Shared state for the ThreadManager unit test. Each worker thread does a
912// bounded amount of work (a fixed number of increments), accumulates its
913// result into a shared total under a mutex, then marks itself finished.
914struct TMTestState {
915 utils::Mutex mutex;
916 uint64 total; // sum of all per-thread work, protected by mutex
917 uint32 finishedCount; // number of worker threads that ran to completion
918 TMTestState() : total(0), finishedCount(0) {}
919};
920
921// Each worker performs this many increments. Kept modest so the whole test
922// finishes in well under a second, but large enough to be measurable.
923static const uint64 TM_ITERATIONS_PER_THREAD = 200000ULL;
924
925static THREAD_RET THREAD_FUNCTION_CALL TMTestWorker(THREAD_ARG arg) {
926 TMTestState* state = (TMTestState*)arg;
927 uint64 localSum = 0;
928 for (uint64 i = 0; i < TM_ITERATIONS_PER_THREAD; i++)
929 localSum += 1;
930 if (state->mutex.enter()) {
931 state->total += localSum;
932 state->finishedCount++;
933 state->mutex.leave();
934 }
936}
937
938// Worker for the slot-recycling test: exits immediately, so the test can churn
939// many thread LIFETIMES cheaply rather than doing work.
940static THREAD_RET THREAD_FUNCTION_CALL TMSlotChurnWorker(THREAD_ARG arg) {
941 (void)arg;
943}
944
945} // anonymous namespace
946
948
949 // 1. Bring up the ThreadManager singleton.
950 unittest::progress(5, "create thread manager");
952 unittest::fail("ThreadManager test: CreateThreadManager() failed");
953 return false;
954 }
955
956 const uint32 NUMTHREADS = 8;
957 TMTestState state;
958 uint32 ids[NUMTHREADS];
959 for (uint32 n = 0; n < NUMTHREADS; n++)
960 ids[n] = 0;
961
962 // 2. Start the worker threads.
963 unittest::progress(20, "start worker threads");
964 uint64 t0 = GetTimeNow();
965 uint32 started = 0;
966 for (uint32 n = 0; n < NUMTHREADS; n++) {
967 if (!ThreadManager::CreateThread(TMTestWorker, &state, ids[n])) {
968 unittest::fail("ThreadManager test: CreateThread() failed for worker %u", n);
970 return false;
971 }
972 unittest::detail("started worker %u with id %u", n, ids[n]);
973 started++;
974 }
975 if (started != NUMTHREADS) {
976 unittest::fail("ThreadManager test: started %u of %u threads", started, NUMTHREADS);
978 return false;
979 }
980
981 // 3. Wait (bounded) for every worker to finish its work. We poll the shared
982 // finished counter and also the OS-level running state, with a hard
983 // timeout so the test can never hang.
984 unittest::progress(50, "wait for workers");
985 const uint32 TIMEOUTMS = 5000;
986 uint32 waitedMs = 0;
987 bool allFinished = false;
988 while (waitedMs < TIMEOUTMS) {
989 uint32 done = 0;
990 if (state.mutex.enter()) {
991 done = state.finishedCount;
992 state.mutex.leave();
993 }
994 if (done >= NUMTHREADS) {
995 allFinished = true;
996 break;
997 }
998 utils::Sleep(5);
999 waitedMs += 5;
1000 }
1001
1002 if (!allFinished) {
1003 unittest::fail("ThreadManager test: only %u of %u workers finished within %ums",
1004 state.finishedCount, NUMTHREADS, TIMEOUTMS);
1006 return false;
1007 }
1008
1009 // 4. Verify the threads actually all ran and produced the expected total.
1010 unittest::progress(75, "verify results");
1011 uint64 expected = (uint64)NUMTHREADS * TM_ITERATIONS_PER_THREAD;
1012 if (state.total != expected) {
1013 unittest::fail("ThreadManager test: total work %llu != expected %llu",
1014 state.total, expected);
1016 return false;
1017 }
1018 if (state.finishedCount != NUMTHREADS) {
1019 unittest::fail("ThreadManager test: finishedCount %u != %u",
1020 state.finishedCount, NUMTHREADS);
1022 return false;
1023 }
1024
1025 // 5. Sanity-check the statistics API for one of the threads.
1027 if (stats.created == 0) {
1028 unittest::fail("ThreadManager test: GetThreadStats returned no creation time for id %u", ids[0]);
1030 return false;
1031 }
1032
1033 double us = (double)(GetTimeNow() - t0);
1034 if (us > 0.0) {
1035 unittest::metric("threads_per_sec", (double)NUMTHREADS / us * 1e6, "threads/s", true);
1036 unittest::metric("total_run_time", us, "us", false);
1037 }
1038
1039 // 6. Tear everything down cleanly. No threads remain after this point.
1040 unittest::progress(90, "shutdown");
1041 if (!ThreadManager::Shutdown()) {
1042 unittest::fail("ThreadManager test: Shutdown() failed");
1043 return false;
1044 }
1045
1046 unittest::progress(100, "done");
1047 return true;
1048}
1049
1050// Registry slot recycling: create and reap MORE threads than the registry has
1051// slots, in ONE process, and require that creation keeps working.
1052//
1053// This is the test that would have caught the slot leak by itself. createThread()
1054// marks a slot BITOCCUPIED, and until the D1 fix nothing ever cleared it, so after
1055// `count` lifetimes GetFirstFreeBitLoc had nothing left and thread creation failed
1056// permanently - while activeCount kept decrementing, so the registry reported spare
1057// capacity. Every existing suite test forks per run and creates a handful of
1058// threads, which is exactly why this stayed invisible.
1059//
1060// It deliberately reaps via ThreadManager::JoinThread(), NOT terminateThread():
1061// the initial D1 fix released the slot only in terminateThread's finalize block,
1062// and JoinThread had a second, divergent copy that did not - so a terminateThread
1063// based test would have passed against the leak. JoinThread is also the path every
1064// NetworkChannel teardown uses, and on macOS it is the ONLY reaper, because the
1065// monitoring loop returns early when statColPolicy is THREAD_STATS_OFF.
1067 unittest::progress(5, "create thread manager");
1069 unittest::fail("slot recycling: CreateThreadManager() failed");
1070 return false;
1071 }
1072
1073 // The registry starts with 1024 slots. Churn comfortably past that so an
1074 // unreleased slot per lifetime must exhaust the bitfield.
1075 const uint32 CYCLES = 1300;
1076 uint32 created = 0, failedAt = 0;
1077 uint32 maxID = 0;
1078
1079 unittest::progress(20, "churn thread lifetimes");
1080 for (uint32 n = 0; n < CYCLES; n++) {
1081 uint32 id = 0;
1082 if (!ThreadManager::CreateThread(TMSlotChurnWorker, NULL, id)) {
1083 failedAt = n;
1084 break;
1085 }
1086 created++;
1087 if (id > maxID)
1088 maxID = id;
1089 // Reap it. JoinThread waits for the OS thread and finalises the slot, which
1090 // is where the release has to happen.
1092 }
1093
1094 unittest::progress(80, "verify");
1095 if (failedAt) {
1096 unittest::fail("slot recycling: CreateThread() failed after %u lifetimes "
1097 "(highest slot id seen %u) - registry slots are not being released",
1098 failedAt, maxID);
1100 return false;
1101 }
1102 if (created != CYCLES) {
1103 unittest::fail("slot recycling: completed %u of %u lifetimes", created, CYCLES);
1105 return false;
1106 }
1107 // Slots must be REUSED, not merely available: if ids climb monotonically past
1108 // the registry size then something grew the storage instead of recycling.
1109 if (maxID >= CYCLES) {
1110 unittest::fail("slot recycling: highest slot id %u >= %u lifetimes - ids are "
1111 "not being recycled", maxID, CYCLES);
1113 return false;
1114 }
1115 unittest::detail("%u thread lifetimes, highest slot id %u", created, maxID);
1116 unittest::metric("thread_lifetimes", (double)created, "", true);
1117 unittest::metric("highest_slot_id", (double)maxID, "", false);
1118
1119 unittest::progress(90, "shutdown");
1120 if (!ThreadManager::Shutdown()) {
1121 unittest::fail("slot recycling: Shutdown() failed");
1122 return false;
1123 }
1124 unittest::progress(100, "done");
1125 return true;
1126}
1127
1130 "Thread manager create/run/join of multiple worker threads", "core");
1132 "Thread manager releases and reuses registry slots across >1024 lifetimes", "core");
1133}
1134
1135} // namespace cmlabs
CMSDK time: µs-resolution 64-bit timestamps and the Time Mapping Constant (TMC).
Process-wide thread registry and lifecycle manager: the concurrency core of CMSDK.
#define BITOCCUPIED
Definition Types.h:34
#define BITFREE
Definition Types.h:33
Small, dependency-free unit test harness used by all CMSDK object tests.
Cross-platform utility toolbox for CMSDK: threading, synchronization, shared memory,...
#define THREAD_NONE
Definition Utils.h:93
#define thread_ret_val(ret)
Definition Utils.h:131
#define THREAD_STATS_ADHOC
Definition Utils.h:102
#define THREAD_STATS_OFF
Definition Utils.h:100
#define LOG_PROCESS
Definition Utils.h:200
#define THREAD_RET
Definition Utils.h:127
#define THREAD_TERMINATED
Definition Utils.h:94
#define THREAD_FUNCTION_CALL
Definition Utils.h:129
#define LogPrint
Definition Utils.h:313
#define THREAD_PAUSED
Definition Utils.h:97
#define THREAD_RUNNING
Definition Utils.h:96
THREAD_RET(* THREAD_FUNCTION)(void *)
Definition Utils.h:128
#define THREAD_INTERRUPTED
Definition Utils.h:98
#define ThreadHandle
Definition Utils.h:125
#define THREAD_INIT
Definition Utils.h:95
#define THREAD_ARG
Definition Utils.h:130
bool isRunning
Set by the worker while its loop is active.
virtual bool stop(uint32 timeout=200)
Ask the worker loop to finish and wait for it to do so.
bool shouldContinue
Loop-continuation flag; cleared by stop().
Singleton registry that creates, controls and profiles all CMSDK threads.
static bool UnitTestSlotRecycling()
Churn MORE thread lifetimes than the registry has slots, in one process.
bool createThread(THREAD_FUNCTION func, void *args, uint32 &newID, uint32 reqID)
Instance-side worker for CreateThread(); requires and manages the mutex internally.
bool resizeThreadStorage(uint32 newCount)
Grow the storage block to hold newCount slots.
bool addLocalThreadStats()
Instance-side worker for AddLocalThreadStats() (pthreads self-reporting).
static bool CreateThreadManager()
Create and initialise the singleton (including its monitoring thread).
bool pauseThread(uint32 id)
Instance-side worker for PauseThread().
bool getLocalThreadID(uint32 &id)
Instance-side worker for GetLocalThreadID(): linear scan of slots for the calling thread's OS ID.
static bool GetLocalThreadID(uint32 &id)
Look up the manager slot ID of the calling thread.
bool continueThread(uint32 id)
Instance-side worker for ContinueThread().
bool joinThread(uint32 id)
Instance-side worker for JoinThread().
ThreadManager()
Construct the manager and pre-allocate storage for 1024 thread slots. Prefer CreateThreadManager().
bool interruptThread(uint32 id)
Instance-side worker for InterruptThread().
ThreadStats * getAllThreadStats(uint32 &count)
Instance-side worker for GetAllThreadStats(): allocates and fills a snapshot array (caller frees with...
static bool InterruptThread(uint32 id)
Kill the thread and restart it from its original entry function.
static bool GetThreadHandle(uint32 id, ThreadHandle &out)
Copy out the OS-level handle of the thread in a manager slot.
~ThreadManager()
Destructor: shuts down all threads, frees the storage block and clears the singleton pointer.
bool getThreadHandle(uint32 id, ThreadHandle &out)
Instance-side worker for GetThreadHandle().
void finaliseSlot(ThreadStats *stats, ThreadDataHeader *header)
Mark a slot terminated and RELEASE it back to the allocation bitfield.
bool shutdown()
Stop the monitoring loop and forcibly terminate all remaining threads.
utils::Mutex mutex
Single lock serialising all registry and statistics access.
static ThreadStats GetLocalThreadStats()
Get a copy of the statistics record for the calling thread.
bool terminateThread(uint32 id)
Instance-side worker for TerminateThread().
static ThreadStats GetThreadStats(uint32 id)
Get a copy of the statistics record for a specific thread.
static bool CreateThread(THREAD_FUNCTION func, void *args, uint32 &newID, uint32 reqID=0)
Create a new native thread and start it immediately.
int32 threadMonitoring()
Body of the monitoring thread (slot 0).
static bool JoinThread(uint32 id)
Wait for a thread to have COMPLETELY finished, then release it - exactly once.
unsigned char * data
Contiguous storage block: ThreadDataHeader + bitfield + ThreadStats[].
ThreadStats getThreadStats(uint32 id)
Instance-side worker for GetThreadStats(): copies the slot under the mutex.
static ThreadManager * Singleton
The process-wide instance; NULL until CreateThreadManager() (or any lazy static call) runs.
static bool UnitTest()
Self-contained unit test (create/run/join worker threads, verify stats).
static bool IsThreadRunning(uint32 id)
Check whether the thread is still alive at the OS level.
bool init()
Second-phase init: registers this instance as the singleton and starts the monitoring thread in slot ...
static bool TerminateThread(uint32 id)
Forcibly terminate the thread and release its slot.
static bool AddLocalThreadStats()
Sample and record CPU statistics for the calling thread itself.
static bool Shutdown()
Terminate all managed threads, then destroy the singleton.
static ThreadStats * GetAllThreadStats(uint32 &count)
Snapshot the statistics of all live threads.
static bool PauseThread(uint32 id)
Suspend the thread at whatever point it is currently executing.
static bool ContinueThread(uint32 id)
Resume a thread previously suspended with PauseThread().
bool isThreadRunning(uint32 id)
Instance-side worker for IsThreadRunning().
static UnitTestRunner & instance()
Access the singleton (created on first use).
void registerTest(const char *name, UnitTestFunc func, const char *description="", const char *category="", bool inDefaultRun=true)
Register a test with the runner.
Recursive mutual-exclusion lock, optionally named for cross-process use.
Definition Utils.h:463
bool leave()
Release the mutex.
Definition Utils.cpp:1330
bool enter()
Block until the mutex is acquired.
Definition Utils.cpp:1158
uint64 GetTimeNow()
Return the current absolute time (µs since year 0) according to the TMC.
Definition PsyTime.cpp:69
std::string PrintTimeDifString(uint64 t, bool us=true, bool ms=true)
Definition PsyTime.cpp:722
int32 GetTimeAgeMS(uint64 t)
Age of a timestamp relative to now, in milliseconds.
Definition PsyTime.cpp:35
int64 GetTimeAge(uint64 t)
Age of a timestamp relative to now.
Definition PsyTime.cpp:25
static THREAD_RET THREAD_FUNCTION_CALL ThreadMonitoring(void *arg)
Entry function of the monitoring thread; delegates to ThreadManager::threadMonitoring().
#define GETTHREADSTATS(data, id)
Compute the address of thread slot id inside storage block data.
uint32 GetThreadStatColAbility()
Report this platform's capability for per-thread CPU statistics collection.
Definition Utils.cpp:3584
bool WaitForThreadToFinish(ThreadHandle hThread, uint32 timeoutMS=0)
Join a thread.
Definition Utils.cpp:3309
bool Sleep(uint32 ms)
Suspend the calling thread.
Definition Utils.cpp:3121
bool PauseThread(ThreadHandle hThread)
Suspend a thread's execution.
Definition Utils.cpp:3484
bool IsThreadRunning(ThreadHandle hThread)
Check whether a thread is still alive.
Definition Utils.cpp:3554
bool CreateThread(THREAD_FUNCTION func, void *args, ThreadHandle &thread, uint32 &osID)
Start a new OS thread.
Definition Utils.cpp:3187
bool ReapThread(ThreadHandle &hThread)
Definition Utils.cpp:3267
bool TerminateThread(ThreadHandle hThread)
Forcibly kill a thread.
Definition Utils.cpp:3359
bool GetCurrentThreadUniqueID(uint32 &tid)
Get a process-unique id for the calling thread.
Definition Utils.cpp:3504
bool ContinueThread(ThreadHandle hThread)
Resume a thread paused with PauseThread().
Definition Utils.cpp:3493
bool GetFirstFreeBitLoc(const char *bitfield, uint32 bytesize, uint32 &loc)
Find the first 0 (free) bit.
Definition Utils.cpp:2733
bool GetCPUTicks(ThreadHandle hThread, uint64 &ticks)
Get accumulated CPU time of a specific thread.
Definition Utils.cpp:3596
bool SetBit(uint32 loc, bit value, char *bitfield, uint32 bytesize)
Set bit loc to value.
Definition Utils.cpp:2888
bool TryReapThread(ThreadHandle &hThread)
Definition Utils.cpp:3283
uint32 Calc32BitFieldSize(uint32 bitsize)
Compute the byte size needed for a bitfield of bitsize bits, rounded up to a 32-bit boundary.
Definition Utils.cpp:2723
void fail(const char *fmt,...)
Set an explanatory reason shown on the FAIL line.
void metric(const char *name, double value, const char *unit="", bool higherIsBetter=true)
Record a performance metric.
void detail(const char *fmt,...)
Verbose-only indented diagnostic line (shown only when verbose=1).
void progress(int percent, const char *action)
Report progress with a short description of the current action.
static bool TryBeginThreadMgrTeardown()
static volatile int32 g_threadMgrTearingDown
static void EndThreadMgrTeardown()
void Register_ThreadManager_Tests()
Header at the start of the ThreadManager's contiguous storage block.
uint32 size
Total size of the storage block in bytes.
uint32 statColPolicy
Statistics collection policy (THREAD_STATS_OFF / ADHOC / central), from utils::GetThreadStatColAbilit...
uint32 count
Capacity: number of ThreadStats slots allocated.
uint32 bitFieldSize
Size in bytes of the slot-occupancy bitfield that follows this header.
uint32 activeCount
Number of slots currently holding a live (non-terminated) thread.
Bookkeeping record for a single managed thread.
uint32 status
Lifecycle state (THREAD_NONE/INIT/RUNNING/PAUSED/INTERRUPTED/TERMINATED).
ThreadHandle hThread
Native handle (HANDLE on Windows, pthread_t wrapper on POSIX).
void * args
Argument passed to func, retained for restart.
uint64 currentCPUTicks[10]
Rolling CPU-tick readings matching time (newest first).
uint64 time[10]
Rolling timestamps of the last 10 statistics samples (newest first).
uint32 id
ThreadManager-assigned slot ID (index into the stats array).
uint32 osID
Operating-system thread ID (as reported by the OS, not the slot ID).
THREAD_FUNCTION func
Entry function, retained so InterruptThread() can restart the thread.
uint64 created
Creation time (microseconds, GetTimeNow()); 0 = slot unused/invalid.