Hi all! So - I always wondered if I understood the memory management in nim correctly. I had some surprising moments in the past and I was never sure how to access variables, when using threads. And I probably wrote a lot of bad code or used unnecessary structures. The documentation said that Nim threads had its own heap but I had usually no idea how to handle each heap or how to create workarounds. The new memory model "arc/orc" also didn't simplify it, as it just documented to work different, faster with a shared heap, but trying to use arc/orc sometimes resulted in strange crashes that I didn't had before... So I created some sample code - just to try out - and I wanted to share it with you.
Just some early observations - correct me if I'm wrong: * when using the standard mm * `every thread is using its own heap`, mostly means that objects are copied when using "ref". You still can easily access objects when using ptr references but should then use `GC_ref` and `GC_unref` to control the object lifetime. * the destructor is called later when the gc is involved * arc/orc * no deep copying of objects when using "ref", but accessing shared heap objects as "ref" isn't safe if the scope of the object is left before the thread stops * destructor of objects is called early when leaving the scope - similar as it happens for normal stack objects. GC is just used in orc to free cyclic objects And here is the sample code import std/os import std/strformat type TestObj* = object name*: string proc getAddr(p: TestObj | pointer): string = let intVal = cast[uint64](p.unsafeaddr) return $intVal # 0x & $(intVal.toHex()) causes early crash proc getAddr(p: ref TestObj | ptr TestObj ): string = let intVal = cast[uint64](p[].unsafeaddr) return $intVal proc `=destroy`*(x: var TestObj) = echo fmt"destroying '{x.name}' {getAddr(x.unsafeaddr)} ({getAddr(x)})" proc newObj(name: string): ref TestObj = new result result.name = name echo fmt"creating new obj '{result.name}' {getAddr(result)} ({getAddr(result[])})" when compileOption("threads"): proc threadFunc[T](someObj: T) = echo fmt"starting thread with '{someObj.name}' " sleep(1_000) echo fmt"Accessing '{someObj.name}' {getAddr(someObj)} from thread" proc main() = echo "x will not be deleted and can be accessed safely as pointer" echo "y is a ref and could be traced" # echo "z is referenced as ptr and access is therefore dangerous" echo "" let x = newObj("x") GC_ref(x) # avoid deletion of x let y = newObj("y") # let z = newObj("z") when compileOption("threads"): var thread1: Thread[ptr TestObj] var thread2: Thread[ref TestObj] var thread3: Thread[ptr TestObj] createThread(thread1, threadFunc[ptr TestObj], x[].addr) createThread(thread2, threadFunc[ref TestObj], y) # createThread(thread3, threadFunc[ptr TestObj], z[].addr) sleep(200) echo "end of main" # GC_unref(x) when isMainModule: main() echo "all scope objects destroyed" GC_fullCollect() sleep(3_000) # wait for threads to finish - not using thread join Run output of `nim c -r -d:release --threads:on --gc:orc .\gcref.nim` (causes access violation on linux): x will not be deleted and can be accessed safely as pointer y is a ref and could be traced creating new obj 'x' 10485840 (6552720) creating new obj 'y' 10485872 (6552720) starting thread with 'x' starting thread with 'y' end of main destroying 'y' 10485872 (6552736) all scope objects destroyed Accessing 'x' 10485840 from thread Accessing '' 10485872 from thread Run output of `nim c -r -d:release --threads:on .\gcref.nim` (no access violation, as referenced object is different): x will not be deleted and can be accessed safely as pointer y is a ref and could be traced creating new obj 'x' 10481744 (6552464) creating new obj 'y' 10481776 (6552464) starting thread with 'x' starting thread with 'y' end of main all scope objects destroyed destroying 'y' 10481776 (6552656) Accessing 'y' 17367120 from thread Accessing 'x' 10481744 from thread Run