I've been wondering if something like Java's ThreadLocal (which I use a massively in (Java) server-side code), could be defined in Nim.
I tried those two approaches, but neither is valid code. It seems to me it isn't possible.
Attempt #1:
type
ThreadLocal*[T] = object
## Represents a thread-local, similar to ThreadLocals in Java.
initialised*: bool
## Is this thread local already initialised?
value*: T
## The thread local value.
InitThreadLocalProc*[T] = proc(): T {.nimcall, gcsafe.}
## Type of a proc that lazy initialises a ThreadLocal.
proc getValue*[T](init: InitThreadLocalProc[T]): var T =
## Returns the value of a thread-local.
var myThreadVar {.threadvar.}: ThreadLocal[T]
if not myThreadVar.initialised:
myThreadVar.value = init()
myThreadVar.initialised = true
return myThreadVar.value
proc Returns42(): int =
42
echo("TL: ",getValue(Returns42))
Attempt #2 (would be preferable to #1, because you are not limited to 1 ThreadLocal per type):
type
InitThreadLocalProc*[T] = proc(): T {.nimcall, gcsafe.}
## Type of a proc that lazy initialises a ThreadLocal.
ThreadLocal*[T] = object
## Represents a thread-local, similar to ThreadLocals in Java.
lazyInit: InitThreadLocalProc[T]
## The lazy initialisation proc.
initialised {.threadvar.}: bool
## Is this thread local already initialised?
value {.threadvar.}: T
## The thread local value.
proc initThreadLocal*[T](init: InitThreadLocalProc[T]): ThreadLocal[T] =
## Initialises a ThreadLocal.
result.lazyInit = init
proc getValue*[T](tl: ThreadLocal[T]): var T =
## Returns the value of a thread-local.
if not tl.initialised:
tl.value = tl.lazyInit()
tl.initialised = true
return tl.value
proc Returns42(): int =
42
var testThreadLocal = initThreadLocal[int](Returns42)
echo("TL: ",getValue(testThreadLocal))
Mayby, the better solution is just to do explicit "eager" initialisation of threadvars, assuming the dev can insert code at all thread creation points. This seems to work:
import locks
type
InitThreadLocalsProc* = proc(): void {.nimcall, gcsafe.}
## Type of a proc that initialises threadvars of some module in a new Thread.
const MAX_INIT_PROCS = 100
## Maximum number of registered InitThreadLocalsProc.
var allInitThreadLocalsProcs: array[MAX_INIT_PROCS,InitThreadLocalsProc]
## All registered InitThreadLocalsProc
var registeredThreadLocalsProcs: int
## Number of registered InitThreadLocalsProc.
var used: bool
## Was some thread already initialised?
var arrayLock: Lock
## Lock to allow safe multi-threaded use.
initLock(arrayLock)
proc registerThreadLocalInitialiser*(init: InitThreadLocalsProc) =
## Registers a InitThreadLocalsProc.
## This cannot be called anymore, after the first call to
## runThreadLocalInitialisers().
if init.isNil:
raise newException(Exception, "init is nil!")
acquire(arrayLock)
try:
if used:
raise newException(Exception, "New Threads already initialised!")
if registeredThreadLocalsProcs == MAX_INIT_PROCS:
raise newException(Exception, "Too many ThreadLocal Initialisers!")
allInitThreadLocalsProcs[registeredThreadLocalsProcs] = init
inc registeredThreadLocalsProcs
finally:
release(arrayLock)
proc runThreadLocalInitialisers*() =
## Runs all InitThreadLocalsProcs.
## Only call this after all initialisers have been registered.
var initialisers: array[MAX_INIT_PROCS,InitThreadLocalsProc]
var count: int
acquire(arrayLock)
try:
used = true
initialisers = allInitThreadLocalsProcs
count = registeredThreadLocalsProcs
finally:
release(arrayLock)
for i in 0..<count:
initialisers[i]()
And a test:
import threadlocal
proc initA() {.gcsafe.} =
echo("Running initA() in thread ",getThreadId())
proc initB() {.gcsafe.} =
echo("Running initB() in thread ",getThreadId())
registerThreadLocalInitialiser(initA)
registerThreadLocalInitialiser(initB)
proc whatever() {.thread.} =
echo("In Thread ",getThreadId())
runThreadLocalInitialisers()
echo("Thread ",getThreadId(), " done")
var thread: Thread[void]
createThread[void](thread, whatever)
joinThread(thread)
echo("DONE")