Skip to content

Commit 5848445

Browse files
authored
Fix external IO loop thead interaction and add function to Base.Experimental to facilitate it's use. Also add a test. (#55529)
While looking at #55525 I found that the implementation wasn't working correctly. I added it to Base.Experimental so people don't need to handroll their own and am also testing a version of what the issue was hitting.
1 parent 9e14bf8 commit 5848445

File tree

5 files changed

+100
-1
lines changed

5 files changed

+100
-1
lines changed

base/experimental.jl

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,29 @@ without adding them to the global method table.
457457
"""
458458
:@MethodTable
459459

460+
"""
461+
Base.Experimental.make_io_thread()
462+
463+
Create a new thread that will run the Julia IO loop. This can potentially reduce the latency of some
464+
IO operations as they no longer depend on the main thread to run it. This does mean that code that uses
465+
this as implicit synchronization needs to be checked for correctness.
466+
"""
467+
function make_io_thread()
468+
tid = UInt[0]
469+
threadwork = @cfunction function(arg::Ptr{Cvoid})
470+
current_task().donenotify = Base.ThreadSynchronizer() #TODO: Should this happen by default in adopt thread?
471+
Base.errormonitor(current_task()) # this may not go particularly well if the IO loop is dead, but try anyways
472+
@ccall jl_set_io_loop_tid((Threads.threadid() - 1)::Int16)::Cvoid
473+
wait() # spin uv_run as long as needed
474+
nothing
475+
end Cvoid (Ptr{Cvoid},)
476+
err = @ccall uv_thread_create(tid::Ptr{UInt}, threadwork::Ptr{Cvoid}, C_NULL::Ptr{Cvoid})::Cint
477+
err == 0 || Base.uv_error("uv_thread_create", err)
478+
@ccall uv_thread_detach(tid::Ptr{UInt})::Cint
479+
err == 0 || Base.uv_error("uv_thread_detach", err)
480+
# n.b. this does not wait for the thread to start or to take ownership of the event loop
481+
end
482+
460483
"""
461484
Base.Experimental.entrypoint(f, argtypes::Tuple)
462485

base/task.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,11 @@ function task_done_hook(t::Task)
849849
end
850850
end
851851

852+
function init_task_lock(t::Task) # Function only called from jl_adopt_thread so foreign tasks have a lock.
853+
if t.donenotify === nothing
854+
t.donenotify = ThreadSynchronizer()
855+
end
856+
end
852857

853858
## scheduler and work queue
854859

src/scheduler.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,8 @@ JL_DLLEXPORT jl_task_t *jl_task_get_next(jl_value_t *trypoptask, jl_value_t *q,
437437
// responsibility, so need to make sure thread 0 will take care
438438
// of us.
439439
if (jl_atomic_load_relaxed(&jl_uv_mutex.owner) == NULL) // aka trylock
440-
wakeup_thread(ct, 0);
440+
jl_wakeup_thread(jl_atomic_load_relaxed(&io_loop_tid));
441+
441442
}
442443
if (uvlock) {
443444
int enter_eventloop = may_sleep(ptls);

src/threading.c

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,28 @@ jl_ptls_t jl_init_threadtls(int16_t tid)
401401
return ptls;
402402
}
403403

404+
static _Atomic(jl_function_t*) init_task_lock_func JL_GLOBALLY_ROOTED = NULL;
405+
406+
static void jl_init_task_lock(jl_task_t *ct)
407+
{
408+
jl_function_t *done = jl_atomic_load_relaxed(&init_task_lock_func);
409+
if (done == NULL) {
410+
done = (jl_function_t*)jl_get_global(jl_base_module, jl_symbol("init_task_lock"));
411+
if (done != NULL)
412+
jl_atomic_store_release(&init_task_lock_func, done);
413+
}
414+
if (done != NULL) {
415+
jl_value_t *args[2] = {done, (jl_value_t*)ct};
416+
JL_TRY {
417+
jl_apply(args, 2);
418+
}
419+
JL_CATCH {
420+
jl_no_exc_handler(jl_current_exception(ct), ct);
421+
}
422+
}
423+
}
424+
425+
404426
JL_DLLEXPORT jl_gcframe_t **jl_adopt_thread(void)
405427
{
406428
// `jl_init_threadtls` puts us in a GC unsafe region, so ensure GC isn't running.
@@ -423,6 +445,8 @@ JL_DLLEXPORT jl_gcframe_t **jl_adopt_thread(void)
423445
JL_GC_PROMISE_ROOTED(ct);
424446
uv_random(NULL, NULL, &ct->rngState, sizeof(ct->rngState), 0, NULL);
425447
jl_atomic_fetch_add(&jl_gc_disable_counter, -1);
448+
ct->world_age = jl_get_world_counter(); // root_task sets world_age to 1
449+
jl_init_task_lock(ct);
426450
return &ct->gcstack;
427451
}
428452

test/threads.jl

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,52 @@ end
360360
end
361361
end
362362

363+
@testset "io_thread" begin
364+
function io_thread_test()
365+
# This test creates a thread that does IO and then blocks the main julia thread
366+
# This test hangs if you don't spawn an IO thread.
367+
# It hanging or not is technically a race but I haven't seen julia win that race yet.
368+
cmd = """
369+
Base.Experimental.make_io_thread()
370+
function callback()::Cvoid
371+
println("Running a command")
372+
run(`echo 42`)
373+
return
374+
end
375+
function call_on_thread(callback::Ptr{Nothing})
376+
tid = UInt[0]
377+
threadwork = @cfunction function(arg::Ptr{Cvoid})
378+
current_task().donenotify = Base.ThreadSynchronizer()
379+
Base.errormonitor(current_task())
380+
println("Calling Julia from thread")
381+
ccall(arg, Cvoid, ())
382+
nothing
383+
end Cvoid (Ptr{Cvoid},)
384+
err = @ccall uv_thread_create(tid::Ptr{UInt}, threadwork::Ptr{Cvoid}, callback::Ptr{Cvoid})::Cint
385+
err == 0 || Base.uv_error("uv_thread_create", err)
386+
gc_state = @ccall jl_gc_safe_enter()::Int8
387+
err = @ccall uv_thread_join(tid::Ptr{UInt})::Cint
388+
@ccall jl_gc_safe_leave(gc_state::Int8)::Cvoid
389+
err == 0 || Base.uv_error("uv_thread_join", err)
390+
return
391+
end
392+
function main()
393+
callback_ptr = @cfunction(callback, Cvoid, ())
394+
call_on_thread(callback_ptr)
395+
println("Done")
396+
end
397+
main()
398+
399+
"""
400+
proc = run(pipeline(`$(Base.julia_cmd()) -e $cmd`), wait=false)
401+
t = Timer(60) do t; kill(proc); end;
402+
@test success(proc)
403+
close(t)
404+
return true
405+
end
406+
@test io_thread_test()
407+
end
408+
363409
# Make sure default number of BLAS threads respects CPU affinity: issue #55572.
364410
@testset "LinearAlgebra number of default threads" begin
365411
if AFFINITY_SUPPORTED

0 commit comments

Comments
 (0)