Update: after manually clearing dispatcher's `callbacks` and `timers` and manually calling a GC the memory is properly freed, even when the `cycle` coroutine is still pending. So the question is: why do I have to do this manually? Shouldn't this be done automatically when the thread finishes?
The properly working code is below: import std/asyncdispatch import HeapQueue, deques type MyObj = object value: ptr int proc `=destroy`(x: MyObj) = if x.value == nil: return dealloc(cast[pointer](x.value)) echo "memory deallocated" proc newMyObj(x: int): MyObj = result.value = cast[ptr int](alloc0(100_000_000*sizeof(int))) echo "memory allocated" let p = cast[ptr UncheckedArray[int]](result.value) for i in 0..<100_000_000: p[i] = x + i proc cycler(x: ref MyObj) {.async.} = while true: x.value[] += 1 echo x.value[] await sleepAsync(200) proc main_async() {.async.} = let x = new(MyObj) x[] = newMyObj(3) let fut = cycler(x) await sleepAsync(1000) proc main() {.thread.} = echo "thread started" waitFor(main_async()) echo "thread finished" let p = getGlobalDispatcher() p.callbacks.clear() p.timers.clear() GC_fullCollect() while true: var th = Thread[void]() createThread(th, main) joinThreads(th) Run