Skip to content

Instantly share code, notes, and snippets.

@apaprocki
Last active December 17, 2015 20:59
Show Gist options
  • Save apaprocki/5671990 to your computer and use it in GitHub Desktop.
Save apaprocki/5671990 to your computer and use it in GitHub Desktop.
Using libuv to create a loop which uses `UV_RUN_ONCE` to handle either a single timer firing or async send. If the timeout for the timer is 0ms, only one `UV_RUN_ONCE` is needed, but if the timeout is > 0ms, a second `UV_RUN_ONCE` call is needed or the timer callback does not fire.
// Compiling from toplevel of built node.js repo on OSX:
// CC -g -o issue5564 issue5564.cc -I deps/uv/include/ \
// -L out/Debug/ -luv -framework CoreFoundation -framework Carbon
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include "uv.h"
uint64_t usec;
#define L(x) do { struct timeval __tv; gettimeofday(&__tv, 0); \
uint64_t __usec = (1000000 * __tv.tv_sec) + __tv.tv_usec - usec; \
printf("%03llu.%03llu %s\n", __usec / 1000000, (__usec % 1000000) / 1000, x); \
fflush(stdout); } while(0)
static uv_loop_t* loop_;
static uv_async_t async_;
static uv_timer_t timer_;
static uv_thread_t thread_;
static uv_cond_t cond_;
static uv_mutex_t mutex_;
void async_cb(uv_async_t* async, int status) {
L("async_cb fired");
}
void timer_cb(uv_timer_t* timer, int status) {
L("timer_cb fired, cond pre-signal");
uv_cond_signal(&cond_);
}
void run_cb(void* arg) {
L("run_cb pre-UV_RUN_ONCE 1");
uv_run(loop_, UV_RUN_ONCE);
L("run_cb post-UV_RUN_ONCE 1");
L("run_cb pre-UV_RUN_ONCE 2");
uv_run(loop_, UV_RUN_ONCE);
L("run_cb post-UV_RUN_ONCE 2");
uv_close(reinterpret_cast<uv_handle_t*>(&async_), NULL);
uv_close(reinterpret_cast<uv_handle_t*>(&timer_), NULL);
L("run_cb pre-UV_RUN_DEFAULT");
uv_run(loop_, UV_RUN_DEFAULT);
L("run_cb post-UV_RUN_DEFAULT");
}
int main(int argc, char** argv) {
struct timeval start;
gettimeofday(&start, 0);
usec = (1000000 * start.tv_sec) + start.tv_usec;
if (3 != argc) {
fprintf(stderr, "issue5564 <timer_timeout> <code_delay>\n");
exit(1);
}
uint64_t timeout = strtoull(argv[1], NULL, 10);
uint64_t delay = 1000000 * strtoull(argv[2], NULL, 10);
uv_cond_init(&cond_);
uv_mutex_init(&mutex_);
uv_mutex_lock(&mutex_);
loop_ = uv_loop_new();
uv_async_init(loop_, &async_, &async_cb);
uv_timer_init(loop_, &timer_);
uv_timer_start(&timer_, &timer_cb, timeout, 0);
L("main thread pre-create");
uv_thread_create(&thread_, &run_cb, NULL);
L("main thread post-create");
L("main cond pre-timedwait");
uv_cond_timedwait(&cond_, &mutex_, delay);
L("main cond post-timedwait");
L("main async pre-send");
uv_async_send(&async_);
L("main async post-send");
L("main thread pre-join");
uv_thread_join(&thread_);
L("main thread post-join");
uv_loop_delete(loop_);
return 0;
}

Running with timer timeout of 50ms and simulated code execution of 1000ms:

$ ./issue5564 50 1000
000.000 main thread pre-create
000.000 main thread post-create
000.000 main cond pre-timedwait
000.000 run_cb pre-UV_RUN_ONCE 1
000.051 run_cb post-UV_RUN_ONCE 1
000.051 run_cb pre-UV_RUN_ONCE 2
000.051 timer_cb fired, cond pre-signal
000.051 main cond post-timedwait
000.051 main async pre-send
000.051 main async post-send
000.051 main thread pre-join
000.051 async_cb fired
000.051 run_cb post-UV_RUN_ONCE 2
000.051 run_cb pre-UV_RUN_DEFAULT
000.051 run_cb post-UV_RUN_DEFAULT
000.051 main thread post-join

Note that timer_cb was not fired until the second UV_RUN_ONCE pass.

Running with timer timeout of 1000ms and simulated code execution of 50ms:

$ ./issue5564 1000 50
000.000 main thread pre-create
000.000 main thread post-create
000.000 main cond pre-timedwait
000.000 run_cb pre-UV_RUN_ONCE 1
000.051 main cond post-timedwait
000.051 main async pre-send
000.051 main async post-send
000.051 main thread pre-join
000.051 async_cb fired
000.051 run_cb post-UV_RUN_ONCE 1
000.051 run_cb pre-UV_RUN_ONCE 2
001.001 run_cb post-UV_RUN_ONCE 2
001.001 run_cb pre-UV_RUN_DEFAULT
001.001 run_cb post-UV_RUN_DEFAULT
001.001 main thread post-join

Note that async_cb was fired in the first UV_RUN_ONCE pass like it should, so the second pass is unnecessary and made the program wait until the 1 second timer.

Running with a timer timeout of 0ms (code execution time does not matter):

$ ./issue5564 0 1000
000.000 main thread pre-create
000.000 main thread post-create
000.000 main cond pre-timedwait
000.000 run_cb pre-UV_RUN_ONCE 1
000.000 timer_cb fired, cond pre-signal
000.000 main cond post-timedwait
000.000 main async pre-send
000.000 main async post-send
000.000 main thread pre-join
000.000 async_cb fired
000.000 run_cb post-UV_RUN_ONCE 1
000.000 run_cb pre-UV_RUN_ONCE 2
*BLOCKED*

With a timer timeout of 0ms, timer_cb is fired in the first UV_RUN_ONCE, also triggering the async_cb, causing the second UV_RUN_ONCE pass to block forever.

@apaprocki
Copy link
Author

A second UV_RUN_ONCE should not be needed and leads to broken situations in the second and third example output I gave. If I only have one UV_RUN_ONCE, the timer triggers it to exit, but the timer callback is never called if the timeout is > 0ms.

If the I add the second UV_RUN_ONCE, the second pass actually calls the callback, but breaks the other situations where the async fires before the timeout.

@apaprocki
Copy link
Author

In uv_run(), timers are only run before polling, which is where the timeout is implemented. So in a single UV_RUN_ONCE run, it is not possible to have your callback fired because it will only be run prior to the poll in the next pass. If uv_run() is modified like so, it works as expected in all 3 cases above with only one UV_RUN_ONCE pass:

@@ -310,6 +310,12 @@ int uv_run(uv_loop_t* loop, uv_run_mode mode) {
       timeout = uv_backend_timeout(loop);

     uv__io_poll(loop, timeout);
+
+    if (mode & (UV_RUN_ONCE | UV_RUN_NOWAIT)) {
+      uv__update_time(loop);
+      uv__run_timers(loop);
+    }
+
     uv__run_check(loop);
     uv__run_closing_handles(loop);
     r = uv__loop_alive(loop);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment