luvit/luv

Luv Async not performing as expected

miversen33 opened this issue · 7 comments

Hello! This is likely due simply to a lack of my understanding of how async is expected to work in luv, but I have the below code that appears to be running synchronously, while I would expect it to run synchronously. I don't know what I am missing and the docs are a bit light on how to use the async handles effectively. The code

local luv = require("luv")
local async1, async2 = nil, nil

async1 = luv.new_async(function()
    local max_loop = 1000000000
    local start_time = luv.hrtime()
    local _ = 0
    while _ < max_loop do _ = _ + 1 end
    print(string.format("Async1 complete. Long running operation took %s ms to finish", (luv.hrtime() - start_time) / 1000000))
    async1:close()
end)
async2 = luv.new_async(function()
    local max_loop = 1000
    local start_time = luv.hrtime()
    local _ = 0
    while _ < max_loop do _ = _ + 1 end
    print(string.format("Async2 complete. Long running operation took %s ms to finish", (luv.hrtime() - start_time) / 1000000))
    async2:close()
end)

print("Starting Async Functions")
async1:send()
async2:send()
print("Running long operation in foreground while we wait")
local max_loop = 1000
local _ = 0
luv.run()
while _ < max_loop do
    _ = _ + 1
end
print("Completed foreground operation")

With this code, I would expect the print output to look something like

Starting Async Functions
Running long operation in foreground while we wait
Async2 complete. Long running operation took 0.004763 ms to finish
Completed foreground operation
Async1 complete. Long running operation took 3809.573251 ms to finish

As async1 is a very long running dry loop. However, this doesn't seem to be the case, instead this completes in order of async calls. IE, async1 completes first, then async2 then the foreground task. I am not quite sure what I am doing wrong here. Below is my "test" output as it stands right now. Thoughts?

Starting Async Functions
Running long operation in foreground while we wait
Async1 complete. Long running operation took 3809.573251 ms to finish
Async2 complete. Long running operation took 0.004763 ms to finish
Completed foreground operation

So, this is mostly just an artifact of how libuv's async_t works. Async callbacks are called on the event loop.

This leads to a few different things:
a) async callbacks are called on the main thread.
b) async callbacks are queued in the event loop, so they must be called synchronously.
c) async callbacks will not be called until the event loop starts running.
d) async callbacks will prevent run from returning.

The description of async_t hints at this behavior:

Async handles allow the user to "wakeup" the event loop and get a callback called from another thread.

If the event loop is sleeping (like waiting for io), async_send will cause a tick so that the callback can be called nearly immediately (on the tick).
However, its mostly intended for other (not main) threads to call a function on the main thread.

An Addendum

If you're looking for a function to run completely separately (on a different thread), then you can use either:

  • work_t: which will offload the function to libuv's threadpool (and may not be called immediately if its very busy).
  • thread_t: which will spawn a new os-level thread to call the function.

Just be aware that nearly all lua implementations (lua-lanes notwithstanding) are not re-entrant, so you can only pass threadargs between them (which notably does not include tables or coroutines).

I had a feeling that is what async was doing, the first handful of points I knew. I didn't know async prevents run from returning, though that does explain the behavior I am seeing here.

Spawning a OS thread is an option though IMO a very expensive one. I will investigate the threadpool stuff :)

The documentation for work_t is also scarce, is there further documentation on it somewhere? Mainly I want to know, can I "join" against a work_t handle? IE, start it and then wait for it to complete? I am aware that goes against trying to run work asynchronously, I am more thinking in a "pool"/"group" context where I have multiple work_t handles that I start up, and then I wait for them all to complete before returning

Its a bit more complicated and involved. new_work takes a work_callback (which is what does the actual work) and a after_work_callback (which gets called after the work is done).

work_callback will be called on a different thread. (with the arguments to queue_work)
after_work_callback will be called on the main thread. (with the returns from the work callback)

Using this you could create a bit of code that yields the current coroutine when you queue the work and then resume it in the after_work_callback.

That makes sense to me. I appreciate you! I will close this out as I have the answers I am looking for.

So I hate to be that person who re-opens issues, I thought I had what I needed with our conversation earlier. However, after dinner and testing this a bit, I am running into very similar behavior to what I was seeing with the async code above.

My code

local luv = require("luv")

local func1 = function(params)
    print(params)
    local max_loop = 1000000000
    local _ = 0
    while _ < max_loop do _ = _ + 1 end
    return 'dun'
end
local func2 = function(params)
    print(params)
    local max_loop = 1000
    local _ = 0
    while _ < max_loop do _ = _ + 1 end
    return 'dun'
end

local func1work = luv.new_work(func1, function(result) print(string.format("func1 complete! %s", result)) end)
local func2work = luv.new_work(func2, function(result) print(string.format("func2 complete! %s", result)) end)
print("Starting background operations")
func1work:queue("hello")
func2work:queue("world")

print("Running long operation in foreground while we wait")
local max_loop = 1000
local _ = 0
luv.run()
while _ < max_loop do
    _ = _ + 1
end
print("Completed foreground operation")

My expected output

Starting background operations
Running long operation in foreground while we wait
hello
world
func2 complete! dun
Completed foreground operation
func1 complete! dun

Actual output

Starting background operations
Running long operation in foreground while we wait
hello
world
func2 complete! dun
func1 complete! dun
Completed foreground operation

It seems that the work_t handles are also not allowing run to return which means that my "main" work load (the loop outside the handles in this case) is still being blocked by the processes that should be running in the background. Am I missing something here?

Sorry if this wasn't clear: luv.run() runs the libuv event loop until there is nothing left in the queue. That means that everything that might call a callback must be finished before run can return.

This is mostly just a limitation of event loops. To call a callback, the event loop must be running.

You should basically never be writing code after luv.run() that isn't cleanup code that should happen before the process exits.

Ahh that makes sense! In this case, I am testing something that will (eventually) be running in a program that manages the libuv event loop for me, so with that being said, it sounds like I may not have to worry about that. To avoid prematurely closing this out (again), I will report back once I have tested that in said program to ensure that it does indeed work without me having to worry about run :)

Edit: Tested and indeed you are correct! My above issue is due to me managing the libuv event loop, and since the program I am writing for manages it for me, this works perfectly. Thank you!