Morning all!
While learning Nim, I want to understand better how await is handled underneath.
simple.nim:
import chronos
template initializeMyLibrary() =
when declared(setupForeignThreadGc): setupForeignThreadGc()
when declared(nimGC_setStackBottom):
var locals {.volatile, noinit.}: pointer
locals = addr(locals)
nimGC_setStackBottom(locals)
proc do_sth*() {.dynlib, exportc, async.} =
initializeMyLibrary()
echo "Hi!"
await sleepAsync(0.milliseconds)
while true:
discard do_sth()
Which can be run with
nim c -r --threads:on simple.nim
That keeps consuming memory forever.
Why that happens and what would be the best approach to prevent this?
Thanks in advance!
No idea what you're actually trying to do, but it's not surprising that this doesn't work. You discard the future returned by do_sth which means that it never gets checked for completion (and you probably never tick the async dispatcher) so you just keep adding futures to the async dispatcher indefinitely without ever removing them. Also not sure why you have the dynlib and exportc pragmas added here, async won't work from a C library unless you do some serious hijinks in your C code. Last but not least --threads:on wouldn't really do anything for async, but I'm guessing that with the weird pragmas you where maybe expecting to call this from a threaded environment?
In short, lots of misconceptions about how async works, possibly some funky stuff going on with calling code from a third party threaded environment.
Thanks so much for the rapid response! Super interesting comments!
You probably never tick the async dispatcher You mean that for instance, to call "poll()", right?
Sorry for the confusion around "dynlib, exportc". I am working on exporting a Nim library to other languages (NodeJs, Golang, etc), and asynchronicity is a key concept in that library. But first of all, I am trying with tiny examples.
Super interesting the article! I like it a lot and I will share it with my colleagues.
On the other hand, the next two examples work better, i.e. it keeps memory low. And I think are equivalents, isn't it?
import chronos
proc do_sth*() {.dynlib, exportc, async.} =
echo "Hi!"
await sleepAsync(0.milliseconds)
while true:
waitFor do_sth()
import chronos
proc do_sth() {.async.} =
echo "Hi!"
await sleepAsync(0.milliseconds)
while true:
asyncSpawn do_sth()
poll()
You are correct in that poll is one way to tick the dispatcher. It is however rather low-level, but that might be what you need if you want to interface with other languages.
Your two examples are almost equivalent, however they have one subtle difference. waitFor will wait for the completion of the Future returned by do_sth (or whatever the Chronos equivalent is called). The asyncSpawn/poll solution however will only perform a single tick of the dispatcher. In this case it probably doesn't matter as long as the dispatcher runs every registration which has timed out, but if your while true loop were ever broken there is no guarantee that there aren't any stuck registrations. A true equivalent would be something like
let fut = do_sth()
while not fut.completed:
poll()
Note that I'm not sure what the procedure to check if a Future is completed is called in Chronos, but you hopefully get the point.
If your program has an async entrypoint, you should be using waitFor do_sth() to ensure that the event loop is running. Async is very complicated and async/await hides the complexity, but it's still there.
Your use of sleepAsync may actually have additional unexpected behavior, because without any actual events happening (events being fed to the process from the OS), your sleep call may not ever complete. I know that in the case of asyncdispatch (the async runtime that comes with the stdlib, which Chronos is based on), sleepAsync adds a timer to the event loop's timer list, but doesn't register an OS timer, and therefore will not receive OS events. This is fine when you have at least one OS event going on (like registering a timer or doing a socket read), but if you have no actual async things going on, the event loop may never wake up and be able to process your sleep timer. This may or may not be a problem with Chronos, but that's how it works in asyncdispatch.