Skip to content

Commit ee862f9

Browse files
authored
scheduler: wake the sleeping event loop thread when IO work arrives (#63884)
1 parent 7c0b79b commit ee862f9

5 files changed

Lines changed: 83 additions & 12 deletions

File tree

‎doc/src/devdocs/scheduler-wakeup.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,12 @@ that the loop is idle before parking: it takes the iolock with a trylock and
118118
releases it without running finalizers. If the iolock is busy, it retries instead
119119
of parking.
120120

121+
The thread that armed a handle may not go to sleep for a long time, though, for
122+
example when the tasks of its pool keep yielding to each other. So when the check
123+
finds the loop idle, it also sets `jl_uv_loop_unwatched` under the iolock, and the
124+
scheduler clears it before it runs `uv_run`. A thread that leaves work in the loop
125+
while the flag is set clears it and wakes `io_loop_tid` from `jl_iolock_end`.
126+
121127
[`IoLoopWake.tla`](https://github.com/JuliaLang/julia/blob/master/doc/src/devdocs/scheduler-wakeup/IoLoopWake.tla)
122128
models this handoff outside threaded regions, including iolock contention and
123129
finalizers that take the iolock before the loop runs and after its unlock. It

‎src/jl_uv.c‎

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ void jl_wake_signal_watchers(void)
134134
}
135135

136136
jl_mutex_t jl_uv_mutex;
137+
int jl_uv_loop_unwatched = 0;
137138

138139
void jl_init_uv(void)
139140
{
@@ -170,6 +171,12 @@ JL_DLLEXPORT void jl_iolock_begin(void) JL_CANSAFEPOINT
170171
*/
171172
JL_DLLEXPORT void jl_iolock_end(void) JL_CANSAFEPOINT
172173
{
174+
// if we leave work in the loop while nobody runs it, wake the event loop thread
175+
// (before unlocking, which can throw)
176+
if (jl_uv_mutex.count == 1 && jl_uv_loop_unwatched && uv_loop_alive(jl_io_loop)) {
177+
jl_uv_loop_unwatched = 0;
178+
jl_wake_io_loop_thread();
179+
}
173180
JL_UV_UNLOCK();
174181
}
175182

@@ -432,7 +439,7 @@ static void jl_proc_exit_cleanup_cb(uv_process_t *process, int64_t exit_status,
432439
*/
433440
JL_DLLEXPORT void jl_close_uv(uv_handle_t *handle)
434441
{
435-
JL_UV_LOCK();
442+
jl_iolock_begin();
436443
if (handle->type == UV_PROCESS && ((uv_process_t*)handle)->pid != 0) {
437444
// take ownership of this handle,
438445
// so we can waitpid for the resource to exit and avoid leaving zombies
@@ -460,7 +467,7 @@ JL_DLLEXPORT void jl_close_uv(uv_handle_t *handle)
460467
uv_close(handle, &jl_uv_closeHandle);
461468
}
462469
}
463-
JL_UV_UNLOCK();
470+
jl_iolock_end();
464471
}
465472

466473
/**
@@ -471,11 +478,11 @@ JL_DLLEXPORT void jl_close_uv(uv_handle_t *handle)
471478
JL_DLLEXPORT void jl_forceclose_uv(uv_handle_t *handle) JL_CANSAFEPOINT
472479
{
473480
if (!uv_is_closing(handle)) { // avoid double-closing the stream
474-
JL_UV_LOCK();
481+
jl_iolock_begin();
475482
if (!uv_is_closing(handle)) { // double-check
476483
uv_close(handle, &jl_uv_closeHandle);
477484
}
478-
JL_UV_UNLOCK();
485+
jl_iolock_end();
479486
}
480487
}
481488

@@ -556,9 +563,9 @@ JL_DLLEXPORT int jl_spawn(char *name, char **argv,
556563
}
557564
}
558565
opts.exit_cb = cb;
559-
JL_UV_LOCK();
566+
jl_iolock_begin();
560567
int r = uv_spawn(loop, proc, &opts);
561-
JL_UV_UNLOCK();
568+
jl_iolock_end();
562569
return r;
563570
}
564571

@@ -707,10 +714,10 @@ JL_DLLEXPORT int jl_uv_write(uv_stream_t *stream, const char *data, size_t n,
707714
uv_buf_t buf[1];
708715
buf[0].base = (char*)data;
709716
buf[0].len = n;
710-
JL_UV_LOCK();
717+
jl_iolock_begin();
711718
JL_SIGATOMIC_BEGIN();
712719
int err = uv_write(uvw, stream, buf, 1, writecb);
713-
JL_UV_UNLOCK();
720+
jl_iolock_end();
714721
JL_SIGATOMIC_END();
715722
return err;
716723
}
@@ -775,10 +782,10 @@ JL_DLLEXPORT void jl_uv_puts(uv_stream_t *stream, const char *str, size_t n)
775782
buf[0].base = data;
776783
buf[0].len = n;
777784
req->data = NULL;
778-
JL_UV_LOCK();
785+
jl_iolock_begin();
779786
JL_SIGATOMIC_BEGIN();
780787
int status = uv_write(req, stream, buf, 1, (uv_write_cb)jl_uv_writecb);
781-
JL_UV_UNLOCK();
788+
jl_iolock_end();
782789
JL_SIGATOMIC_END();
783790
if (status < 0) {
784791
jl_uv_writecb(req, status);
@@ -1261,9 +1268,9 @@ JL_DLLEXPORT int jl_queue_work(work_cb_t work_func, void *work_ctx,
12611268
baton->notify_data1 = notify_data1;
12621269
baton->notify_data2 = notify_data2;
12631270

1264-
JL_UV_LOCK();
1271+
jl_iolock_begin();
12651272
uv_queue_work(jl_io_loop, &baton->req, jl_work_wrapper, jl_work_notifier);
1266-
JL_UV_UNLOCK();
1273+
jl_iolock_end();
12671274

12681275
return 0;
12691276
}

‎src/julia_internal.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,10 @@ static uv_loop_t *const unused_uv_loop_arg = (uv_loop_t *)0xBAD10;
183183

184184
extern jl_mutex_t jl_uv_mutex;
185185
extern _Atomic(int) jl_uv_n_waiters;
186+
// Set when a thread finds the event loop idle before it sleeps. Cleared before the
187+
// scheduler runs the loop, and by jl_iolock_end when it wakes the event loop thread.
188+
// Protected by jl_uv_mutex.
189+
extern int jl_uv_loop_unwatched;
186190

187191
// Global data structures for accessing symbols and other globals
188192
#include "jl_internal_data.inc"
@@ -1390,6 +1394,7 @@ void jl_safepoint_exclude_gc_end(void) JL_NOTSAFEPOINT JL_NOTSAFEPOINT_LEAVE;
13901394
void jl_wake_libuv(void) JL_NOTSAFEPOINT;
13911395
void jl_init_signal_watchers(uv_async_cb cb);
13921396
void jl_wake_signal_watchers(void) JL_NOTSAFEPOINT;
1397+
void jl_wake_io_loop_thread(void) JL_NOTSAFEPOINT;
13931398
void jl_send_abandon_signal(int16_t tid) JL_NOTSAFEPOINT;
13941399
int jl_abandon_try_commit(jl_ptls_t ptls) JL_NOTSAFEPOINT;
13951400
void JL_NORETURN jl_abandon_task_cb(void) JL_CANSAFEPOINT;

‎src/scheduler.c‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,19 @@ JL_DLLEXPORT void jl_wakeup_thread_from_foreign(int16_t tid) JL_NOTSAFEPOINT
326326
}
327327
}
328328

329+
// Called by a thread that is leaving work in the event loop while nobody runs it.
330+
// It may stay busy for a long time and never reach the check it makes before it
331+
// sleeps, so wake the event loop thread now.
332+
void jl_wake_io_loop_thread(void) JL_NOTSAFEPOINT
333+
{
334+
jl_task_t *ct = jl_current_task;
335+
int16_t tid = jl_atomic_load_relaxed(&io_loop_tid);
336+
if (tid == jl_atomic_load_relaxed(&ct->tid))
337+
return; // it runs uv_run itself before it sleeps
338+
jl_fence(); // [^store_buffering_1]
339+
wake_thread(tid);
340+
}
341+
329342
// Round-robin start hint for jl_wakeup_threadpool, sharded across cache-line-padded
330343
// stripes so concurrent producers don't contend on a single counter.
331344
#define POOL_WAKE_HINT_STRIPES 64
@@ -569,6 +582,7 @@ JL_DLLEXPORT jl_task_t *jl_task_get_next(jl_value_t *trypoptask, jl_value_t *q,
569582
if (enter_eventloop) {
570583
uv_loop_t *loop = jl_global_event_loop();
571584
loop->stop_flag = 0;
585+
jl_uv_loop_unwatched = 0;
572586
JULIA_DEBUG_SLEEPWAKE( ptls->uv_run_enter = cycleclock() );
573587
active = uv_run(loop, UV_RUN_ONCE);
574588
JULIA_DEBUG_SLEEPWAKE( ptls->uv_run_leave = cycleclock() );
@@ -584,6 +598,8 @@ JL_DLLEXPORT jl_task_t *jl_task_get_next(jl_value_t *trypoptask, jl_value_t *q,
584598
active = 1;
585599
if (jl_mutex_trylock_nogc(&jl_uv_mutex)) {
586600
active = uv_loop_alive(jl_global_event_loop());
601+
// whoever leaves work in the loop from now on wakes the event loop thread
602+
jl_uv_loop_unwatched = !active;
587603
jl_mutex_unlock_nogc(&jl_uv_mutex);
588604
}
589605
}

‎test/threads.jl‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,43 @@ end
801801
close(t)
802802
end
803803

804+
@testset "event loop thread wakeup with busy workers" begin
805+
# A timer or `@threadcall` started on a pool whose threads never sleep must still
806+
# get serviced by the event loop thread (#63882).
807+
script = """
808+
# The ^C listener keeps the event loop alive, so the event loop thread never
809+
# sleeps. Close it so the loop can go idle.
810+
hooks = filter(f -> hasproperty(f, :listeners) && hasproperty(f, :cond) &&
811+
getproperty(f, :cond) isa Base.AsyncCondition, Base.atexit_hooks)
812+
isdefined(Base, :start_sigint_listener) && length(hooks) != 1 && error("^C listener not found")
813+
for f in hooks
814+
ccall(:jl_set_sigint_cond, Cvoid, (Ptr{Cvoid},), C_NULL)
815+
close(f.cond)
816+
end
817+
const done = Threads.Atomic{Bool}(false)
818+
# keep the only default thread busy: it yields, but never sleeps
819+
spinner = Threads.@spawn :default begin
820+
t_end = time_ns() + 30_000_000_000
821+
while !done[]
822+
time_ns() > t_end && return false
823+
yield()
824+
end
825+
return true
826+
end
827+
waiter = Threads.@spawn :default begin
828+
for _ in 1:3
829+
sleep(0.001)
830+
@threadcall(:jl_getpagesize, Clong, ())
831+
end
832+
done[] = true
833+
end
834+
wait(waiter)
835+
fetch(spinner) || exit(1)
836+
"""
837+
cmd = `$(Base.julia_cmd()) --startup-file=no --threads=1,1 -e $script`
838+
@test success(pipeline(cmd; stdout, stderr))
839+
end
840+
804841
@testset "--timeout-for-safepoint-straggler command-line flag" begin
805842
program = "
806843
function main()

0 commit comments

Comments
 (0)