From 9168f22057a72822f0d2ca5b1053e97becd649fa Mon Sep 17 00:00:00 2001 From: Johannes Pfau Date: Thu, 25 Apr 2019 11:11:39 +0000 Subject: [PATCH] D: Implement GCC emutls in druntime * libdruntime/Makefile.am: Add emutls and gthread files. * libdruntime/Makefile.in: Regenerate. * libdruntime/gcc/emutls.d: New file. Implement GC-compatible emutls. * libdruntime/gcc/gthread.d: New file. * libdruntime/gcc/sections/elf_shared.d: Integrate emutls support. * testsuite/libphobos.allocations/tls_gc_integration.d: New test for TLS. From-SVN: r270568 --- libphobos/ChangeLog | 9 + libphobos/libdruntime/Makefile.am | 3 +- libphobos/libdruntime/Makefile.in | 13 +- libphobos/libdruntime/gcc/emutls.d | 316 ++++++++++++++++++ libphobos/libdruntime/gcc/gthread.d | 127 +++++++ .../libdruntime/gcc/sections/elf_shared.d | 95 ++++-- .../tls_gc_integration.d | 50 +++ 7 files changed, 573 insertions(+), 40 deletions(-) create mode 100644 libphobos/libdruntime/gcc/emutls.d create mode 100644 libphobos/libdruntime/gcc/gthread.d create mode 100644 libphobos/testsuite/libphobos.allocations/tls_gc_integration.d diff --git a/libphobos/ChangeLog b/libphobos/ChangeLog index 9d442f4e354..131d7f9168f 100644 --- a/libphobos/ChangeLog +++ b/libphobos/ChangeLog @@ -1,3 +1,12 @@ +2019-04-25 Johannes Pfau + + * libdruntime/Makefile.am: Add emutls and gthread files. + * libdruntime/Makefile.in: Regenerate. + * libdruntime/gcc/emutls.d: New file. Implement GC-compatible emutls. + * libdruntime/gcc/gthread.d: New file. + * libdruntime/gcc/sections/elf_shared.d: Integrate emutls support. + * testsuite/libphobos.allocations/tls_gc_integration.d: New test for TLS. + 2019-04-25 Iain Buclaw * testsuite/Makefile.am: Set PWD_COMMAND. diff --git a/libphobos/libdruntime/Makefile.am b/libphobos/libdruntime/Makefile.am index b981f233d71..bf9bff095ca 100644 --- a/libphobos/libdruntime/Makefile.am +++ b/libphobos/libdruntime/Makefile.am @@ -161,7 +161,8 @@ DRUNTIME_DSOURCES = core/atomic.d core/attribute.d core/bitop.d \ core/sync/config.d core/sync/exception.d core/sync/mutex.d \ core/sync/rwmutex.d core/sync/semaphore.d core/thread.d core/time.d \ core/vararg.d gcc/attribute.d gcc/backtrace.d gcc/builtins.d gcc/deh.d \ - gcc/sections/android.d gcc/sections/elf_shared.d gcc/sections/osx.d \ + gcc/emutls.d gcc/gthread.d gcc/sections/android.d \ + gcc/sections/elf_shared.d gcc/sections/osx.d \ gcc/sections/package.d gcc/sections/win32.d gcc/sections/win64.d \ gcc/unwind/arm.d gcc/unwind/arm_common.d gcc/unwind/c6x.d \ gcc/unwind/generic.d gcc/unwind/package.d gcc/unwind/pe.d object.d \ diff --git a/libphobos/libdruntime/Makefile.in b/libphobos/libdruntime/Makefile.in index eb290b6a14f..19ee94fc370 100644 --- a/libphobos/libdruntime/Makefile.in +++ b/libphobos/libdruntime/Makefile.in @@ -203,10 +203,10 @@ am__objects_1 = core/atomic.lo core/attribute.lo core/bitop.lo \ core/sync/exception.lo core/sync/mutex.lo core/sync/rwmutex.lo \ core/sync/semaphore.lo core/thread.lo core/time.lo \ core/vararg.lo gcc/attribute.lo gcc/backtrace.lo \ - gcc/builtins.lo gcc/deh.lo gcc/sections/android.lo \ - gcc/sections/elf_shared.lo gcc/sections/osx.lo \ - gcc/sections/package.lo gcc/sections/win32.lo \ - gcc/sections/win64.lo gcc/unwind/arm.lo \ + gcc/builtins.lo gcc/deh.lo gcc/emutls.lo gcc/gthread.lo \ + gcc/sections/android.lo gcc/sections/elf_shared.lo \ + gcc/sections/osx.lo gcc/sections/package.lo \ + gcc/sections/win32.lo gcc/sections/win64.lo gcc/unwind/arm.lo \ gcc/unwind/arm_common.lo gcc/unwind/c6x.lo \ gcc/unwind/generic.lo gcc/unwind/package.lo gcc/unwind/pe.lo \ object.lo rt/aApply.lo rt/aApplyR.lo rt/aaA.lo rt/adi.lo \ @@ -757,7 +757,8 @@ DRUNTIME_DSOURCES = core/atomic.d core/attribute.d core/bitop.d \ core/sync/config.d core/sync/exception.d core/sync/mutex.d \ core/sync/rwmutex.d core/sync/semaphore.d core/thread.d core/time.d \ core/vararg.d gcc/attribute.d gcc/backtrace.d gcc/builtins.d gcc/deh.d \ - gcc/sections/android.d gcc/sections/elf_shared.d gcc/sections/osx.d \ + gcc/emutls.d gcc/gthread.d gcc/sections/android.d \ + gcc/sections/elf_shared.d gcc/sections/osx.d \ gcc/sections/package.d gcc/sections/win32.d gcc/sections/win64.d \ gcc/unwind/arm.d gcc/unwind/arm_common.d gcc/unwind/c6x.d \ gcc/unwind/generic.d gcc/unwind/package.d gcc/unwind/pe.d object.d \ @@ -1104,6 +1105,8 @@ gcc/attribute.lo: gcc/$(am__dirstamp) gcc/backtrace.lo: gcc/$(am__dirstamp) gcc/builtins.lo: gcc/$(am__dirstamp) gcc/deh.lo: gcc/$(am__dirstamp) +gcc/emutls.lo: gcc/$(am__dirstamp) +gcc/gthread.lo: gcc/$(am__dirstamp) gcc/sections/$(am__dirstamp): @$(MKDIR_P) gcc/sections @: > gcc/sections/$(am__dirstamp) diff --git a/libphobos/libdruntime/gcc/emutls.d b/libphobos/libdruntime/gcc/emutls.d new file mode 100644 index 00000000000..461f20d9e28 --- /dev/null +++ b/libphobos/libdruntime/gcc/emutls.d @@ -0,0 +1,316 @@ +// GNU D Compiler emulated TLS routines. +// Copyright (C) 2019 Free Software Foundation, Inc. + +// GCC is free software; you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 3, or (at your option) any later +// version. + +// GCC is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. + +// Under Section 7 of GPL version 3, you are granted additional +// permissions described in the GCC Runtime Library Exception, version +// 3.1, as published by the Free Software Foundation. + +// You should have received a copy of the GNU General Public License and +// a copy of the GCC Runtime Library Exception along with this program; +// see the files COPYING3 and COPYING.RUNTIME respectively. If not, see +// . + +// This code is based on the libgcc emutls.c emulated TLS support. + +module gcc.emutls; + +import core.atomic, core.stdc.stdlib, core.stdc.string, core.sync.mutex; +import rt.util.container.array, rt.util.container.hashtab; +import core.internal.traits : classInstanceAlignment; +import gcc.builtins, gcc.gthread; + +version (GNU_EMUTLS): private: + +alias word = __builtin_machine_uint; +alias pointer = __builtin_pointer_uint; +alias TlsArray = Array!(void**); + +/* + * TLS control data emitted by GCC for every TLS variable. + */ +struct __emutls_object +{ + word size; + word align_; + union + { + pointer offset; + void* ptr; + } + + ubyte* templ; +} + +// Per-thread key to obtain the per-thread TLS variable array +__gshared __gthread_key_t emutlsKey; +// Largest, currently assigned TLS variable offset +__gshared pointer emutlsMaxOffset = 0; +// Contains the size of the TLS variables (for GC) +__gshared Array!word emutlsSizes; +// Contains the TLS variable array for single-threaded apps +__gshared TlsArray singleArray; +// List of all currently alive TlsArrays (for GC) +__gshared HashTab!(TlsArray*, TlsArray*) emutlsArrays; + +// emutlsMutex Mutex + @nogc handling +enum mutexAlign = classInstanceAlignment!Mutex; +enum mutexClassInstanceSize = __traits(classInstanceSize, Mutex); +__gshared align(mutexAlign) void[mutexClassInstanceSize] _emutlsMutex; + +@property Mutex emutlsMutex() nothrow @nogc +{ + return cast(Mutex) _emutlsMutex.ptr; +} + +/* + * Global (de)initialization functions + */ +extern (C) void _d_emutls_init() nothrow @nogc +{ + memcpy(_emutlsMutex.ptr, typeid(Mutex).initializer.ptr, _emutlsMutex.length); + (cast(Mutex) _emutlsMutex.ptr).__ctor(); + + if (__gthread_key_create(&emutlsKey, &emutlsDestroyThread) != 0) + abort(); +} + +__gshared __gthread_once_t initOnce = GTHREAD_ONCE_INIT; + +/* + * emutls main entrypoint, called by GCC for each TLS variable access. + */ +extern (C) void* __emutls_get_address(shared __emutls_object* obj) nothrow @nogc +{ + pointer offset; + if (__gthread_active_p()) + { + // Obtain the offset index into the TLS array (same for all-threads) + // for requested var. If it is unset, obtain a new offset index. + offset = atomicLoad!(MemoryOrder.acq, pointer)(obj.offset); + if (__builtin_expect(offset == 0, 0)) + { + __gthread_once(&initOnce, &_d_emutls_init); + emutlsMutex.lock_nothrow(); + + offset = obj.offset; + if (offset == 0) + { + offset = ++emutlsMaxOffset; + + emutlsSizes.ensureLength(offset); + // Note: it's important that we copy any data from obj and + // do not keep an reference to obj itself: If a library is + // unloaded, its tls variables are not removed from the arrays + // and the GC will still scan these. If we then try to reference + // a pointer to the data segment of an unloaded library, this + // will crash. + emutlsSizes[offset - 1] = obj.size; + + atomicStore!(MemoryOrder.rel, pointer)(obj.offset, offset); + } + emutlsMutex.unlock_nothrow(); + } + } + // For single-threaded systems, don't synchronize + else + { + if (__builtin_expect(obj.offset == 0, 0)) + { + offset = ++emutlsMaxOffset; + + emutlsSizes.ensureLength(offset); + emutlsSizes[offset - 1] = obj.size; + + obj.offset = offset; + } + } + + TlsArray* arr; + if (__gthread_active_p()) + arr = cast(TlsArray*) __gthread_getspecific(emutlsKey); + else + arr = &singleArray; + + // This will always be false for singleArray + if (__builtin_expect(arr == null, 0)) + { + arr = mallocTlsArray(offset); + __gthread_setspecific(emutlsKey, arr); + emutlsMutex.lock_nothrow(); + emutlsArrays[arr] = arr; + emutlsMutex.unlock_nothrow(); + } + // Check if we have to grow the per-thread array + else if (__builtin_expect(offset > arr.length, 0)) + { + (*arr).ensureLength(offset); + } + + // Offset 0 is used as a not-initialized marker above. In the + // TLS array, we start at 0. + auto index = offset - 1; + + // Get the per-thread pointer from the TLS array + void** ret = (*arr)[index]; + if (__builtin_expect(ret == null, 0)) + { + // Initial access, have to allocate the storage + ret = emutlsAlloc(obj); + (*arr)[index] = ret; + } + + return ret; +} + +// 1:1 copy from libgcc emutls.c +extern (C) void __emutls_register_common(__emutls_object* obj, word size, word align_, ubyte* templ) nothrow @nogc +{ + if (obj.size < size) + { + obj.size = size; + obj.templ = null; + } + if (obj.align_ < align_) + obj.align_ = align_; + if (templ && size == obj.size) + obj.templ = templ; +} + +// 1:1 copy from libgcc emutls.c +void** emutlsAlloc(shared __emutls_object* obj) nothrow @nogc +{ + void* ptr; + void* ret; + enum pointerSize = (void*).sizeof; + + /* We could use here posix_memalign if available and adjust + emutls_destroy accordingly. */ + if ((cast() obj).align_ <= pointerSize) + { + ptr = malloc((cast() obj).size + pointerSize); + if (ptr == null) + abort(); + (cast(void**) ptr)[0] = ptr; + ret = ptr + pointerSize; + } + else + { + ptr = malloc(obj.size + pointerSize + obj.align_ - 1); + if (ptr == null) + abort(); + ret = cast(void*)((cast(pointer)(ptr + pointerSize + obj.align_ - 1)) & ~cast( + pointer)(obj.align_ - 1)); + (cast(void**) ret)[-1] = ptr; + } + + if (obj.templ) + memcpy(ret, cast(ubyte*) obj.templ, cast() obj.size); + else + memset(ret, 0, cast() obj.size); + + return cast(void**) ret; +} + +/* + * When a thread has finished, remove the TLS array from the GC + * scan list emutlsArrays, free all allocated TLS variables and + * finally free the array. + */ +extern (C) void emutlsDestroyThread(void* ptr) nothrow @nogc +{ + auto arr = cast(TlsArray*) ptr; + emutlsMutex.lock_nothrow(); + emutlsArrays.remove(arr); + emutlsMutex.unlock_nothrow(); + + foreach (entry; *arr) + { + if (entry) + free(entry[-1]); + } + + free(arr); +} + +/* + * Allocate a new TLS array, set length according to offset. + */ +TlsArray* mallocTlsArray(pointer offset = 0) nothrow @nogc +{ + static assert(TlsArray.alignof == (void*).alignof); + void[] data = malloc(TlsArray.sizeof)[0 .. TlsArray.sizeof]; + if (data.ptr == null) + abort(); + + static immutable TlsArray init = TlsArray.init; + memcpy(data.ptr, &init, data.length); + (cast(TlsArray*) data).length = 32; + return cast(TlsArray*) data.ptr; +} + +/* + * Make sure array is large enough to hold an entry for offset. + * Note: the array index will be offset - 1! + */ +void ensureLength(Value)(ref Array!(Value) arr, size_t offset) nothrow @nogc +{ + // index is offset-1 + if (offset > arr.length) + { + auto newSize = arr.length * 2; + if (offset > newSize) + newSize = offset + 32; + arr.length = newSize; + } +} + +// Public interface +public: +void _d_emutls_scan(scope void delegate(void* pbeg, void* pend) nothrow cb) nothrow +{ + void scanArray(scope TlsArray* arr) nothrow + { + foreach (index, entry; *arr) + { + auto ptr = cast(void*) entry; + if (ptr) + cb(ptr, ptr + emutlsSizes[index]); + } + } + + __gthread_once(&initOnce, &_d_emutls_init); + emutlsMutex.lock_nothrow(); + // this code is effectively nothrow + try + { + foreach (arr, value; emutlsArrays) + { + scanArray(arr); + } + } + catch (Exception) + { + } + emutlsMutex.unlock_nothrow(); + scanArray(&singleArray); +} + +// Call this after druntime has been unloaded +void _d_emutls_destroy() nothrow @nogc +{ + if (__gthread_key_delete(emutlsKey) != 0) + abort(); + + (cast(Mutex) _emutlsMutex.ptr).__dtor(); + destroy(emutlsArrays); +} diff --git a/libphobos/libdruntime/gcc/gthread.d b/libphobos/libdruntime/gcc/gthread.d new file mode 100644 index 00000000000..580fdcb9b0d --- /dev/null +++ b/libphobos/libdruntime/gcc/gthread.d @@ -0,0 +1,127 @@ +// GNU D Compiler thread support for emulated TLS routines. +// Copyright (C) 2019 Free Software Foundation, Inc. + +// GCC is free software; you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 3, or (at your option) any later +// version. + +// GCC is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. + +// Under Section 7 of GPL version 3, you are granted additional +// permissions described in the GCC Runtime Library Exception, version +// 3.1, as published by the Free Software Foundation. + +// You should have received a copy of the GNU General Public License and +// a copy of the GCC Runtime Library Exception along with this program; +// see the files COPYING3 and COPYING.RUNTIME respectively. If not, see +// . + +module gcc.gthread; +import gcc.config; + +extern (C) nothrow @nogc: + +alias GthreadDestroyFn = extern (C) void function(void*); +alias GthreadOnceFn = extern (C) void function(); + +static if (GNU_Thread_Model == ThreadModel.Posix) +{ + import core.sys.posix.pthread; + + alias __gthread_key_create = pthread_key_create; + alias __gthread_key_delete = pthread_key_delete; + alias __gthread_getspecific = pthread_getspecific; + alias __gthread_setspecific = pthread_setspecific; + alias __gthread_once = pthread_once; + alias __gthread_key_t = pthread_key_t; + alias __gthread_once_t = pthread_once_t; + enum GTHREAD_ONCE_INIT = PTHREAD_ONCE_INIT; + + // TODO: FreeBSD and Solaris exposes a dummy POSIX threads + // interface that will need to be handled here. + extern (D) int __gthread_active_p() + { + return 1; + } +} +else static if (GNU_Thread_Model == ThreadModel.Single) +{ + alias __gthread_key_t = int; + alias __gthread_once_t = int; + enum GTHREAD_ONCE_INIT = 0; + + extern (D) int __gthread_key_create(__gthread_key_t*, GthreadDestroyFn) + { + return 0; + } + + extern (D) int __gthread_key_delete(__gthread_key_t) + { + return 0; + } + + extern (D) void* __gthread_getspecific(__gthread_key_t) + { + return null; + } + + extern (D) int __gthread_setspecific(__gthread_key_t, void*) + { + return 0; + } + + extern (D) int __gthread_once(__gthread_once_t*, GthreadOnceFn) + { + return 0; + } + + extern (D) int __gthread_active_p() + { + return 0; + } +} +else static if (GNU_Thread_Model == ThreadModel.Win32) +{ + struct __gthread_once_t + { + INT done; + LONG started; + } + + int __gthr_win32_key_create(__gthread_key_t* keyp, GthreadDestroyFn dtor); + int __gthr_win32_key_delete(__gthread_key_t key); + void* __gthr_win32_getspecific(__gthread_key_t key); + int __gthr_win32_setspecific(__gthread_key_t key, const void* ptr); + int __gthr_win32_once(__gthread_once_t* once, GthreadOnceFn); + + alias __gthread_key_create = __gthr_win32_key_create; + alias __gthread_key_delete = __gthr_win32_key_delete; + alias __gthread_getspecific = __gthr_win32_getspecific; + alias __gthread_setspecific = __gthr_win32_setspecific; + alias __gthread_once = __gthr_win32_once; + enum GTHREAD_ONCE_INIT = __gthread_once_t(0, -1); + alias __gthread_key_t = c_ulong; + + version (MinGW) + { + // Mingw runtime >= v0.3 provides a magic variable that is set to nonzero + // if -mthreads option was specified, or 0 otherwise. + extern __gshared int _CRT_MT; + } + + extern (D) int __gthread_active_p() + { + version (MinGW) + return _CRT_MT; + else + return 1; + } +} +else +{ + static assert(false, "Not implemented"); +} diff --git a/libphobos/libdruntime/gcc/sections/elf_shared.d b/libphobos/libdruntime/gcc/sections/elf_shared.d index d92e4cd9c4e..3a2c85cba64 100644 --- a/libphobos/libdruntime/gcc/sections/elf_shared.d +++ b/libphobos/libdruntime/gcc/sections/elf_shared.d @@ -222,8 +222,16 @@ version (Shared) void scanTLSRanges(Array!(ThreadDSO)* tdsos, scope ScanDG dg) nothrow { - foreach (ref tdso; *tdsos) - dg(tdso._tlsRange.ptr, tdso._tlsRange.ptr + tdso._tlsRange.length); + version (GNU_EMUTLS) + { + import gcc.emutls; + _d_emutls_scan(dg); + } + else + { + foreach (ref tdso; *tdsos) + dg(tdso._tlsRange.ptr, tdso._tlsRange.ptr + tdso._tlsRange.length); + } } // interface for core.thread to inherit loaded libraries @@ -310,8 +318,16 @@ else void scanTLSRanges(Array!(void[])* rngs, scope ScanDG dg) nothrow { - foreach (rng; *rngs) - dg(rng.ptr, rng.ptr + rng.length); + version (GNU_EMUTLS) + { + import gcc.emutls; + _d_emutls_scan(dg); + } + else + { + foreach (rng; *rngs) + dg(rng.ptr, rng.ptr + rng.length); + } } } @@ -519,6 +535,11 @@ extern(C) void _d_dso_registry(CompilerDSOData* data) _handleToDSO.reset(); } finiLocks(); + version (GNU_EMUTLS) + { + import gcc.emutls; + _d_emutls_destroy(); + } } } } @@ -805,40 +826,46 @@ void scanSegments(in ref dl_phdr_info info, DSO* pdso) nothrow @nogc break; case PT_TLS: // TLS segment - safeAssert(!pdso._tlsSize, "Multiple TLS segments in image header."); - static if (OS_Have_Dlpi_Tls_Modid) + version (GNU_EMUTLS) { - pdso._tlsMod = info.dlpi_tls_modid; - pdso._tlsSize = phdr.p_memsz; - } - else version (Solaris) - { - struct Rt_map - { - Link_map rt_public; - const char* rt_pathname; - c_ulong rt_padstart; - c_ulong rt_padimlen; - c_ulong rt_msize; - uint rt_flags; - uint rt_flags1; - c_ulong rt_tlsmodid; - } - - Rt_map* map; - version (Shared) - dlinfo(handleForName(info.dlpi_name), RTLD_DI_LINKMAP, &map); - else - dlinfo(RTLD_SELF, RTLD_DI_LINKMAP, &map); - // Until Solaris 11.4, tlsmodid for the executable is 0. - // Let it start at 1 as the rest of the code expects. - pdso._tlsMod = map.rt_tlsmodid + 1; - pdso._tlsSize = phdr.p_memsz; } else { - pdso._tlsMod = 0; - pdso._tlsSize = 0; + safeAssert(!pdso._tlsSize, "Multiple TLS segments in image header."); + static if (OS_Have_Dlpi_Tls_Modid) + { + pdso._tlsMod = info.dlpi_tls_modid; + pdso._tlsSize = phdr.p_memsz; + } + else version (Solaris) + { + struct Rt_map + { + Link_map rt_public; + const char* rt_pathname; + c_ulong rt_padstart; + c_ulong rt_padimlen; + c_ulong rt_msize; + uint rt_flags; + uint rt_flags1; + c_ulong rt_tlsmodid; + } + + Rt_map* map; + version (Shared) + dlinfo(handleForName(info.dlpi_name), RTLD_DI_LINKMAP, &map); + else + dlinfo(RTLD_SELF, RTLD_DI_LINKMAP, &map); + // Until Solaris 11.4, tlsmodid for the executable is 0. + // Let it start at 1 as the rest of the code expects. + pdso._tlsMod = map.rt_tlsmodid + 1; + pdso._tlsSize = phdr.p_memsz; + } + else + { + pdso._tlsMod = 0; + pdso._tlsSize = 0; + } } break; diff --git a/libphobos/testsuite/libphobos.allocations/tls_gc_integration.d b/libphobos/testsuite/libphobos.allocations/tls_gc_integration.d new file mode 100644 index 00000000000..44eb40c366d --- /dev/null +++ b/libphobos/testsuite/libphobos.allocations/tls_gc_integration.d @@ -0,0 +1,50 @@ +import core.memory, core.thread, core.bitop; + +/* + * This test repeatedly performs operations on GC-allocated objects which + * are only reachable from TLS storage. Tests are performed in multiple threads + * and GC collections are triggered repeatedly, so if the GC does not properly + * scan TLS memory, this provokes a crash. + */ +class TestTLS +{ + uint a; + void addNumber() + { + auto val = volatileLoad(&a); + val++; + volatileStore(&a, val); + } +} + +TestTLS tlsPtr; + +static this() +{ + tlsPtr = new TestTLS(); +} + +void main() +{ + void runThread() + { + for (size_t i = 0; i < 100; i++) + { + Thread.sleep(10.msecs); + tlsPtr.addNumber(); + GC.collect(); + } + } + + Thread[] threads; + for (size_t i = 0; i < 20; i++) + { + auto t = new Thread(&runThread); + threads ~= t; + t.start(); + } + runThread(); + + foreach (thread; threads) + thread.join(); +}