From 87224c33cfcd2dc9c3bf4548c007604ef5ca87dc Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Fri, 17 Jan 2025 15:24:59 -0500 Subject: [PATCH 01/11] default to 1 interactive thread, or 0 if generating output --- src/jloptions.c | 25 +++++++++++++++---------- src/threading.c | 13 ++++++++----- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/jloptions.c b/src/jloptions.c index 2c5a9074eb465..ac515bea19845 100644 --- a/src/jloptions.c +++ b/src/jloptions.c @@ -623,8 +623,13 @@ JL_DLLEXPORT void jl_parse_opts(int *argcp, char ***argvp) break; case 't': // threads errno = 0; - jl_options.nthreadpools = 1; - long nthreads = -1, nthreadsi = 0; + jl_options.nthreadpools = 2; + // By default: + // default threads = -1 (== "auto") + long nthreads = -1; + // interactive threads = 1, or 0 if generating output + long nthreadsi = jl_generating_output() ? 0 : 1; + if (!strncmp(optarg, "auto", 4)) { jl_options.nthreads = -1; if (optarg[4] == ',') { @@ -633,10 +638,9 @@ JL_DLLEXPORT void jl_parse_opts(int *argcp, char ***argvp) else { errno = 0; nthreadsi = strtol(&optarg[5], &endptr, 10); - if (errno != 0 || endptr == &optarg[5] || *endptr != 0 || nthreadsi < 1 || nthreadsi >= INT16_MAX) - jl_errorf("julia: -t,--threads=auto,; m must be an integer >= 1"); + if (errno != 0 || endptr == &optarg[5] || *endptr != 0 || nthreadsi < 0 || nthreadsi >= INT16_MAX) + jl_errorf("julia: -t,--threads=auto,; m must be an integer >= 0"); } - jl_options.nthreadpools++; } } else { @@ -650,17 +654,18 @@ JL_DLLEXPORT void jl_parse_opts(int *argcp, char ***argvp) errno = 0; char *endptri; nthreadsi = strtol(&endptr[1], &endptri, 10); - if (errno != 0 || endptri == &endptr[1] || *endptri != 0 || nthreadsi < 1 || nthreadsi >= INT16_MAX) - jl_errorf("julia: -t,--threads=,; n and m must be integers >= 1"); + // Allow 0 for interactive + if (errno != 0 || endptri == &endptr[1] || *endptri != 0 || nthreadsi < 0 || nthreadsi >= INT16_MAX) + jl_errorf("julia: -t,--threads=,; m must be an integer ≥ 0"); + if (nthreadsi == 0) + jl_options.nthreadpools = 1; } - jl_options.nthreadpools++; } jl_options.nthreads = nthreads + nthreadsi; } int16_t *ntpp = (int16_t *)malloc_s(jl_options.nthreadpools * sizeof(int16_t)); ntpp[0] = (int16_t)nthreads; - if (jl_options.nthreadpools == 2) - ntpp[1] = (int16_t)nthreadsi; + ntpp[1] = (int16_t)nthreadsi; jl_options.nthreads_per_pool = ntpp; break; case 'p': // procs diff --git a/src/threading.c b/src/threading.c index ab63114fc9c8f..a51916cdcd8d8 100644 --- a/src/threading.c +++ b/src/threading.c @@ -698,15 +698,15 @@ void jl_init_threading(void) // and `jl_n_threads_per_pool`. jl_n_threadpools = 2; int16_t nthreads = JULIA_NUM_THREADS; - int16_t nthreadsi = 0; + // if generating output default to 0 interactive threads, otherwise default to 1 + int16_t nthreadsi = jl_generating_output() ? 0 : 1; char *endptr, *endptri; if (jl_options.nthreads != 0) { // --threads specified nthreads = jl_options.nthreads_per_pool[0]; if (nthreads < 0) nthreads = jl_effective_threads(); - if (jl_options.nthreadpools == 2) - nthreadsi = jl_options.nthreads_per_pool[1]; + nthreadsi = (jl_options.nthreadpools == 1) ? 0 : jl_options.nthreads_per_pool[1]; } else if ((cp = getenv(NUM_THREADS_NAME))) { // ENV[NUM_THREADS_NAME] specified if (!strncmp(cp, "auto", 4)) { @@ -722,13 +722,16 @@ void jl_init_threading(void) } if (*cp == ',') { cp++; - if (!strncmp(cp, "auto", 4)) + if (!strncmp(cp, "auto", 4)) { nthreadsi = 1; + cp += 4; + } else { errno = 0; nthreadsi = strtol(cp, &endptri, 10); if (errno != 0 || endptri == cp || nthreadsi < 0) - nthreadsi = 0; + nthreadsi = 1; + cp = endptri; } } } From e922afd0a6b4d6b02f591dd1b5f9785386df3db6 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sat, 18 Jan 2025 10:14:37 -0500 Subject: [PATCH 02/11] un-doctest examples that aren't guaranteed threaded output order --- doc/src/manual/faq.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/src/manual/faq.md b/doc/src/manual/faq.md index 2673ca7532acf..178a674e46643 100644 --- a/doc/src/manual/faq.md +++ b/doc/src/manual/faq.md @@ -941,7 +941,7 @@ While the streaming I/O API is synchronous, the underlying implementation is ful Consider the printed output from the following: -```jldoctest +``` julia> @sync for i in 1:3 Threads.@spawn write(stdout, string(i), " Foo ", " Bar ") end @@ -954,7 +954,7 @@ yields to other tasks while waiting for that part of the I/O to complete. `print` and `println` "lock" the stream during a call. Consequently changing `write` to `println` in the above example results in: -```jldoctest +``` julia> @sync for i in 1:3 Threads.@spawn println(stdout, string(i), " Foo ", " Bar ") end @@ -965,7 +965,7 @@ julia> @sync for i in 1:3 You can lock your writes with a `ReentrantLock` like this: -```jldoctest +``` julia> l = ReentrantLock(); julia> @sync for i in 1:3 From def3e0689ce36e5050ff4c57e87a71c32c3ff1d2 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sat, 18 Jan 2025 10:25:53 -0500 Subject: [PATCH 03/11] hide unstable channel show in doctests --- base/channels.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/base/channels.jl b/base/channels.jl index 527c22b3d45fd..ef508bd40e3ed 100644 --- a/base/channels.jl +++ b/base/channels.jl @@ -130,8 +130,7 @@ julia> chnl = Channel{Char}(1, spawn=true) do ch for c in "hello world" put!(ch, c) end - end -Channel{Char}(1) (2 items available) + end; julia> String(collect(chnl)) "hello world" From 5a677d4acfbf0539b48a76012cd47af244459d81 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sat, 18 Jan 2025 10:31:55 -0500 Subject: [PATCH 04/11] expand threads_exec tests to cover interactive threads fix test thread indexing make threads_exec work being called from :interactive and :default pools add tests for threaded construct default pools etc. --- test/threads.jl | 21 +++++++-- test/threads_exec.jl | 108 ++++++++++++++++++++++++------------------- 2 files changed, 77 insertions(+), 52 deletions(-) diff --git a/test/threads.jl b/test/threads.jl index 43fbea1e351a2..a1da408252fc4 100644 --- a/test/threads.jl +++ b/test/threads.jl @@ -72,11 +72,24 @@ let e = Event(true), started1 = Event(true), started2 = Event(true), done = Even end end -let cmd = `$(Base.julia_cmd()) --depwarn=error --rr-detach --startup-file=no threads_exec.jl` - for test_nthreads in (1, 2, 4, 4) # run once to try single-threaded mode, then try a couple times to trigger bad races + +let cmd1 = `$(Base.julia_cmd()) --depwarn=error --rr-detach --startup-file=no threads_exec.jl`, + cmd2 = `$(Base.julia_cmd()) --depwarn=error --rr-detach --startup-file=no -e 'print(Threads.threadpoolsize(:default), ",", Threads.threadpoolsize(:interactive))'` + for (test_nthreads, test_nthreadsi) in ( + (1, 0), + (1, 1), + (2, 0), + (2, 1), + (4, 0), + (4, 0)) # try a couple times to trigger bad races new_env = copy(ENV) - new_env["JULIA_NUM_THREADS"] = string(test_nthreads) - run(pipeline(setenv(cmd, new_env), stdout = stdout, stderr = stderr)) + new_env["JULIA_NUM_THREADS"] = string(test_nthreads, ",", test_nthreadsi) + run(pipeline(setenv(cmd1, new_env), stdout = stdout, stderr = stderr)) + threads_config = "$test_nthreads,$test_nthreadsi" + # threads set via env var + @test chomp(read(setenv(cmd2, new_env), String)) == threads_config + # threads set via -t + @test chomp(read(`$cmd2 -t$test_nthreads,$test_nthreadsi`, String)) == threads_config end end diff --git a/test/threads_exec.jl b/test/threads_exec.jl index 65e5ef57c911b..8c67d01ce7bea 100644 --- a/test/threads_exec.jl +++ b/test/threads_exec.jl @@ -31,10 +31,11 @@ Timer(t -> killjob("KILLING BY THREAD TEST WATCHDOG\n"), 1200) @testset """threads_exec.jl with JULIA_NUM_THREADS == $(ENV["JULIA_NUM_THREADS"])""" begin @test Threads.threadid() == 1 -@test 1 <= threadpoolsize() <= Threads.maxthreadid() +@test threadpool() in (:interactive, :default) # thread 1 could be in the interactive pool +@test 1 <= threadpoolsize(:default) <= Threads.maxthreadid() # basic lock check -if threadpoolsize() > 1 +if threadpoolsize(:default) > 1 let lk = SpinLock() c1 = Base.Event() c2 = Base.Event() @@ -56,7 +57,18 @@ end # threading constructs -let a = zeros(Int, 2 * threadpoolsize()) +@testset "@threads and @spawn threadpools" begin + @threads for i in 1:1 + @test threadpool() == :default + end + @test fetch(Threads.@spawn threadpool()) == :default + @test fetch(Threads.@spawn :default threadpool()) == :default + if threadpoolsize(:interactive) > 0 + @test fetch(Threads.@spawn :interactive threadpool()) == :interactive + end +end + +let a = zeros(Int, 2 * threadpoolsize(:default)) @threads for i = 1:length(a) @sync begin @async begin @@ -76,7 +88,7 @@ end # parallel loop with parallel atomic addition function threaded_loop(a, r, x) - counter = Threads.Atomic{Int}(min(threadpoolsize(), length(r))) + counter = Threads.Atomic{Int}(min(threadpoolsize(:default), length(r))) @threads for i in r # synchronize the start given that each partition is started sequentially, # meaning that without the wait, if the loop is too fast the iteration can happen in order @@ -429,7 +441,7 @@ end for T in intersect((Int32, Int64, Float32, Float64), Base.Threads.atomictypes) var = Atomic{T}() nloops = 1000 - di = threadpoolsize() + di = threadpoolsize(:default) @threads for i in 1:di test_atomic_cas!(var, i:di:nloops) end @@ -519,13 +531,13 @@ function test_thread_cfunction() @test cfs[1] == cf1 @test cfs[2] == cf(fs[2]) @test length(unique(cfs)) == 1000 - ok = zeros(Int, threadpoolsize()) + ok = zeros(Int, threadpoolsize(:default)) @threads :static for i in 1:10000 i = mod1(i, 1000) fi = fs[i] cfi = cf(fi) GC.@preserve cfi begin - ok[threadid()] += (cfi === cfs[i]) + ok[threadid() - threadpoolsize(:interactive)] += (cfi === cfs[i]) end end @test sum(ok) == 10000 @@ -582,17 +594,17 @@ test_nested_loops() function test_thread_too_few_iters() x = Atomic() - a = zeros(Int, threadpoolsize()+2) - threaded_loop(a, 1:threadpoolsize()-1, x) - found = zeros(Bool, threadpoolsize()+2) - for i=1:threadpoolsize()-1 + a = zeros(Int, threadpoolsize(:default)+2) + threaded_loop(a, 1:threadpoolsize(:default)-1, x) + found = zeros(Bool, threadpoolsize(:default)+2) + for i=1:threadpoolsize(:default)-1 found[a[i]] = true end - @test x[] == threadpoolsize()-1 + @test x[] == threadpoolsize(:default)-1 # Next test checks that all loop iterations ran, # and were unique (via pigeon-hole principle). - @test !(false in found[1:threadpoolsize()-1]) - @test !(true in found[threadpoolsize():end]) + @test !(false in found[1:threadpoolsize(:default)-1]) + @test !(true in found[threadpoolsize(:default):end]) end test_thread_too_few_iters() @@ -734,10 +746,10 @@ function _atthreads_with_error(a, err) end a end -@test_throws CompositeException _atthreads_with_error(zeros(threadpoolsize()), true) -let a = zeros(threadpoolsize()) +@test_throws CompositeException _atthreads_with_error(zeros(threadpoolsize(:default)), true) +let a = zeros(threadpoolsize(:default)) _atthreads_with_error(a, false) - @test a == [1:threadpoolsize();] + @test a == [threadpoolsize(:interactive) .+ (1:threadpoolsize(:default));] end # static schedule @@ -748,11 +760,11 @@ function _atthreads_static_schedule(n) end return ids end -@test _atthreads_static_schedule(threadpoolsize()) == 1:threadpoolsize() -@test _atthreads_static_schedule(1) == [1;] +@test _atthreads_static_schedule(threadpoolsize(:default)) == threadpoolsize(:interactive) .+ (1:threadpoolsize(:default)) +@test _atthreads_static_schedule(1) == [threadpoolsize(:interactive) + 1;] @test_throws( "`@threads :static` cannot be used concurrently or nested", - @threads(for i = 1:1; _atthreads_static_schedule(threadpoolsize()); end), + @threads(for i = 1:1; _atthreads_static_schedule(threadpoolsize(:default)); end), ) # dynamic schedule @@ -765,35 +777,35 @@ function _atthreads_dynamic_schedule(n) end return inc[], flags end -@test _atthreads_dynamic_schedule(threadpoolsize()) == (threadpoolsize(), ones(threadpoolsize())) +@test _atthreads_dynamic_schedule(threadpoolsize(:default)) == (threadpoolsize(:default), ones(threadpoolsize(:default))) @test _atthreads_dynamic_schedule(1) == (1, ones(1)) @test _atthreads_dynamic_schedule(10) == (10, ones(10)) -@test _atthreads_dynamic_schedule(threadpoolsize() * 2) == (threadpoolsize() * 2, ones(threadpoolsize() * 2)) +@test _atthreads_dynamic_schedule(threadpoolsize(:default) * 2) == (threadpoolsize(:default) * 2, ones(threadpoolsize(:default) * 2)) # nested dynamic schedule function _atthreads_dynamic_dynamic_schedule() inc = Threads.Atomic{Int}(0) - Threads.@threads :dynamic for _ = 1:threadpoolsize() - Threads.@threads :dynamic for _ = 1:threadpoolsize() + Threads.@threads :dynamic for _ = 1:threadpoolsize(:default) + Threads.@threads :dynamic for _ = 1:threadpoolsize(:default) Threads.atomic_add!(inc, 1) end end return inc[] end -@test _atthreads_dynamic_dynamic_schedule() == threadpoolsize() * threadpoolsize() +@test _atthreads_dynamic_dynamic_schedule() == threadpoolsize(:default) * threadpoolsize(:default) function _atthreads_static_dynamic_schedule() - ids = zeros(Int, threadpoolsize()) + ids = zeros(Int, threadpoolsize(:default)) inc = Threads.Atomic{Int}(0) - Threads.@threads :static for i = 1:threadpoolsize() + Threads.@threads :static for i = 1:threadpoolsize(:default) ids[i] = Threads.threadid() - Threads.@threads :dynamic for _ = 1:threadpoolsize() + Threads.@threads :dynamic for _ = 1:threadpoolsize(:default) Threads.atomic_add!(inc, 1) end end return ids, inc[] end -@test _atthreads_static_dynamic_schedule() == (1:threadpoolsize(), threadpoolsize() * threadpoolsize()) +@test _atthreads_static_dynamic_schedule() == (threadpoolsize(:interactive) .+ (1:threadpoolsize(:default)), threadpoolsize(:default) * threadpoolsize(:default)) # errors inside @threads :dynamic function _atthreads_dynamic_with_error(a) @@ -802,7 +814,7 @@ function _atthreads_dynamic_with_error(a) end a end -@test_throws "user error in the loop body" _atthreads_dynamic_with_error(zeros(threadpoolsize())) +@test_throws "user error in the loop body" _atthreads_dynamic_with_error(zeros(threadpoolsize(:default))) #### # :greedy @@ -817,57 +829,57 @@ function _atthreads_greedy_schedule(n) end return inc[], flags end -@test _atthreads_greedy_schedule(threadpoolsize()) == (threadpoolsize(), ones(threadpoolsize())) +@test _atthreads_greedy_schedule(threadpoolsize(:default)) == (threadpoolsize(:default), ones(threadpoolsize(:default))) @test _atthreads_greedy_schedule(1) == (1, ones(1)) @test _atthreads_greedy_schedule(10) == (10, ones(10)) -@test _atthreads_greedy_schedule(threadpoolsize() * 2) == (threadpoolsize() * 2, ones(threadpoolsize() * 2)) +@test _atthreads_greedy_schedule(threadpoolsize(:default) * 2) == (threadpoolsize(:default) * 2, ones(threadpoolsize(:default) * 2)) # nested greedy schedule function _atthreads_greedy_greedy_schedule() inc = Threads.Atomic{Int}(0) - Threads.@threads :greedy for _ = 1:threadpoolsize() - Threads.@threads :greedy for _ = 1:threadpoolsize() + Threads.@threads :greedy for _ = 1:threadpoolsize(:default) + Threads.@threads :greedy for _ = 1:threadpoolsize(:default) Threads.atomic_add!(inc, 1) end end return inc[] end -@test _atthreads_greedy_greedy_schedule() == threadpoolsize() * threadpoolsize() +@test _atthreads_greedy_greedy_schedule() == threadpoolsize(:default) * threadpoolsize(:default) function _atthreads_greedy_dynamic_schedule() inc = Threads.Atomic{Int}(0) - Threads.@threads :greedy for _ = 1:threadpoolsize() - Threads.@threads :dynamic for _ = 1:threadpoolsize() + Threads.@threads :greedy for _ = 1:threadpoolsize(:default) + Threads.@threads :dynamic for _ = 1:threadpoolsize(:default) Threads.atomic_add!(inc, 1) end end return inc[] end -@test _atthreads_greedy_dynamic_schedule() == threadpoolsize() * threadpoolsize() +@test _atthreads_greedy_dynamic_schedule() == threadpoolsize(:default) * threadpoolsize(:default) function _atthreads_dymamic_greedy_schedule() inc = Threads.Atomic{Int}(0) - Threads.@threads :dynamic for _ = 1:threadpoolsize() - Threads.@threads :greedy for _ = 1:threadpoolsize() + Threads.@threads :dynamic for _ = 1:threadpoolsize(:default) + Threads.@threads :greedy for _ = 1:threadpoolsize(:default) Threads.atomic_add!(inc, 1) end end return inc[] end -@test _atthreads_dymamic_greedy_schedule() == threadpoolsize() * threadpoolsize() +@test _atthreads_dymamic_greedy_schedule() == threadpoolsize(:default) * threadpoolsize(:default) function _atthreads_static_greedy_schedule() - ids = zeros(Int, threadpoolsize()) + ids = zeros(Int, threadpoolsize(:default)) inc = Threads.Atomic{Int}(0) - Threads.@threads :static for i = 1:threadpoolsize() + Threads.@threads :static for i = 1:threadpoolsize(:default) ids[i] = Threads.threadid() - Threads.@threads :greedy for _ = 1:threadpoolsize() + Threads.@threads :greedy for _ = 1:threadpoolsize(:default) Threads.atomic_add!(inc, 1) end end return ids, inc[] end -@test _atthreads_static_greedy_schedule() == (1:threadpoolsize(), threadpoolsize() * threadpoolsize()) +@test _atthreads_static_greedy_schedule() == (threadpoolsize(:interactive) .+ (1:threadpoolsize(:default)), threadpoolsize(:default) * threadpoolsize(:default)) # errors inside @threads :greedy function _atthreads_greedy_with_error(a) @@ -876,7 +888,7 @@ function _atthreads_greedy_with_error(a) end a end -@test_throws "user error in the loop body" _atthreads_greedy_with_error(zeros(threadpoolsize())) +@test_throws "user error in the loop body" _atthreads_greedy_with_error(zeros(threadpoolsize(:default))) #### # multi-argument loop @@ -1109,7 +1121,7 @@ function check_sync_end_race() nnotscheduled += y === :notscheduled end # Useful for tuning the test: - @debug "`check_sync_end_race` done" threadpoolsize() ncompleted nnotscheduled nerror + @debug "`check_sync_end_race` done" threadpoolsize(:default) ncompleted nnotscheduled nerror finally done[] = true end @@ -1123,7 +1135,7 @@ end # issue #41546, thread-safe package loading @testset "package loading" begin - ntasks = max(threadpoolsize(), 4) + ntasks = max(threadpoolsize(:default), 4) ch = Channel{Bool}(ntasks) barrier = Base.Event() old_act_proj = Base.ACTIVE_PROJECT[] From 731c4e807a7cfb44760459476ecfd7c19f7b7d47 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sat, 18 Jan 2025 15:36:17 -0500 Subject: [PATCH 05/11] use isempty so failing test shows content --- test/gcext/gcext-test.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/gcext/gcext-test.jl b/test/gcext/gcext-test.jl index 81637392e3c5d..86b6ad1a2ce59 100644 --- a/test/gcext/gcext-test.jl +++ b/test/gcext/gcext-test.jl @@ -31,7 +31,7 @@ end # @test success(p) errlines = fetch(err_task) lines = fetch(out_task) - @test length(errlines) == 0 + @test isempty(errlines) # @test length(lines) == 6 @test length(lines) == 5 @test checknum(lines[2], r"([0-9]+) full collections", n -> n >= 10) From 040532f7d3fbd68d9925107862f0c7e6e3a69e47 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sat, 18 Jan 2025 15:43:12 -0500 Subject: [PATCH 06/11] run gcext test single threaded --- test/gcext/gcext.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/gcext/gcext.c b/test/gcext/gcext.c index d5bf91ec8c9ab..3da44a388a5b8 100644 --- a/test/gcext/gcext.c +++ b/test/gcext/gcext.c @@ -600,6 +600,13 @@ int main() jl_gc_set_cb_notify_external_alloc(alloc_bigval, 1); jl_gc_set_cb_notify_external_free(free_bigval, 1); + // single threaded mode + // Note: with -t1,1 a signal 10 occurs in task_scanner + jl_options.nthreadpools = 1; + jl_options.nthreads = 1; + int16_t ntpp[] = {jl_options.nthreads}; + jl_options.nthreads_per_pool = ntpp; + jl_init(); if (jl_gc_enable_conservative_gc_support() < 0) abort(); From 96bbd3cbfcec3f5900a8c91c745e96da10eeca25 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sat, 18 Jan 2025 16:53:14 -0500 Subject: [PATCH 07/11] delete unclear test --- test/threads_exec.jl | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/test/threads_exec.jl b/test/threads_exec.jl index 8c67d01ce7bea..7091893908cb3 100644 --- a/test/threads_exec.jl +++ b/test/threads_exec.jl @@ -546,20 +546,6 @@ if cfunction_closure test_thread_cfunction() end -function test_thread_range() - a = zeros(Int, threadpoolsize()) - @threads for i in 1:threadid() - a[i] = 1 - end - for i in 1:threadid() - @test a[i] == 1 - end - for i in (threadid() + 1):threadpoolsize() - @test a[i] == 0 - end -end -test_thread_range() - # Thread safety of `jl_load_and_lookup`. function test_load_and_lookup_18020(n) @threads for i in 1:n From a895fa86d545440db8c26463432ff15cd8c99461 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sat, 18 Jan 2025 22:41:46 -0500 Subject: [PATCH 08/11] REPL: fix brittle replcompletions test --- stdlib/REPL/test/replcompletions.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stdlib/REPL/test/replcompletions.jl b/stdlib/REPL/test/replcompletions.jl index 2c8d48cc232cf..77d056b63655d 100644 --- a/stdlib/REPL/test/replcompletions.jl +++ b/stdlib/REPL/test/replcompletions.jl @@ -1187,7 +1187,7 @@ let s, c, r REPL.REPLCompletions.next_cache_update = 0 end c,r = test_scomplete(s) - wait(REPL.REPLCompletions.PATH_cache_task::Task) # wait for caching to complete + timedwait(()->REPL.REPLCompletions.next_cache_update != 0, 5) # wait for caching to complete c,r = test_scomplete(s) @test "tmp-executable" in c @test r == 1:9 @@ -1221,7 +1221,7 @@ let s, c, r REPL.REPLCompletions.next_cache_update = 0 end c,r = test_scomplete(s) - wait(REPL.REPLCompletions.PATH_cache_task::Task) # wait for caching to complete + timedwait(()->REPL.REPLCompletions.next_cache_update != 0, 5) # wait for caching to complete c,r = test_scomplete(s) @test ["repl-completion"] == c @test s[r] == "repl-completio" From 47cd776a915f2b7f05bfa12618b01f64d8c74ca7 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 19 Jan 2025 23:14:44 -0500 Subject: [PATCH 09/11] revert doc examples to `@async` I believe these make more sense as async than spawn, otherwise more orchestration is needed in the example. Were changed to spawn in https://github.com/JuliaLang/julia/pull/55315 --- doc/src/manual/methods.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/src/manual/methods.md b/doc/src/manual/methods.md index e448c62465b0d..45d22e08aaffe 100644 --- a/doc/src/manual/methods.md +++ b/doc/src/manual/methods.md @@ -615,7 +615,7 @@ Start some other operations that use `f(x)`: julia> g(x) = f(x) g (generic function with 1 method) -julia> t = Threads.@spawn f(wait()); yield(); +julia> t = @async f(wait()); yield(); ``` Now we add some new methods to `f(x)`: @@ -640,7 +640,7 @@ julia> g(1) julia> fetch(schedule(t, 1)) "original definition" -julia> t = Threads.@spawn f(wait()); yield(); +julia> t = @async f(wait()); yield(); julia> fetch(schedule(t, 1)) "definition for Int" From 4b1c78b0df4d912ddfabcbe440fe6df3edd8d7f8 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Mon, 20 Jan 2025 22:02:50 -0500 Subject: [PATCH 10/11] add News --- NEWS.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/NEWS.md b/NEWS.md index 3aa74034ead54..796fe9647fff0 100644 --- a/NEWS.md +++ b/NEWS.md @@ -29,6 +29,13 @@ New language features Language changes ---------------- + - Julia now defaults to 1 "interactive" thread, in addition to the 1 "default" worker thread. i.e. `-t1,1` + This means in default configuration the main task and repl (when in interactive mode), which both run on + thread 1, now run within the `interactive` threadpool. Also the libuv IO loop runs on thread 1, + helping efficient utilization of the "default" worker threadpool, which is what `Threads.@threads` and a bare + `Threads.@spawn` uses. Use `0` to disable the interactive thread i.e. `-t1,0` or `JULIA_NUM_THREADS=1,0`, or + `-tauto,0` etc. The zero is explicitly required to disable it, `-t2` will set the equivalent of `-t2,1` ([#57087]) + - When methods are replaced with exactly equivalent ones, the old method is no longer deleted implicitly simultaneously, although the new method does take priority and become more specific than the old method. Thus if the new From eb92c57befc58704011cfbf53e08a8dab35e2f1f Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Mon, 20 Jan 2025 22:02:56 -0500 Subject: [PATCH 11/11] update docs --- doc/src/manual/multi-threading.md | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/doc/src/manual/multi-threading.md b/doc/src/manual/multi-threading.md index 209e2ffe1da56..ec470f867cc47 100644 --- a/doc/src/manual/multi-threading.md +++ b/doc/src/manual/multi-threading.md @@ -5,11 +5,13 @@ of Julia multi-threading features. ## Starting Julia with multiple threads -By default, Julia starts up with a single thread of execution. This can be verified by using the -command [`Threads.nthreads()`](@ref): +By default, Julia starts up with 2 threads of execution; 1 worker thread and 1 interactive thread. +This can be verified by using the command [`Threads.nthreads()`](@ref): ```jldoctest -julia> Threads.nthreads() +julia> Threads.nthreads(:default) +1 +julia> Threads.nthreads(:interactive) 1 ``` @@ -22,6 +24,9 @@ The number of threads can either be specified as an integer (`--threads=4`) or a (`--threads=auto`), where `auto` tries to infer a useful default number of threads to use (see [Command-line Options](@ref command-line-interface) for more details). +See [threadpools](@ref man-threadpools) for how to control how many `:default` and `:interactive` threads are in +each threadpool. + !!! compat "Julia 1.5" The `-t`/`--threads` command line argument requires at least Julia 1.5. In older versions you must use the environment variable instead. @@ -29,6 +34,10 @@ The number of threads can either be specified as an integer (`--threads=4`) or a !!! compat "Julia 1.7" Using `auto` as value of the environment variable [`JULIA_NUM_THREADS`](@ref JULIA_NUM_THREADS) requires at least Julia 1.7. In older versions, this value is ignored. + +!!! compat "Julia 1.12" + Starting by default with 1 interactive thread, as well as the 1 worker thread, was made as such in Julia 1.12 + Lets start Julia with 4 threads: ```bash @@ -96,10 +105,17 @@ using Base.Threads Interactive tasks should avoid performing high latency operations, and if they are long duration tasks, should yield frequently. -Julia may be started with one or more threads reserved to run interactive tasks: +By default Julia starts with one interactive thread reserved to run interactive tasks, but that number can +be controlled with: ```bash $ julia --threads 3,1 +julia> Threads.nthreads(:interactive) +1 + +$ julia --threads 3,0 +julia> Threads.nthreads(:interactive) +0 ``` The environment variable [`JULIA_NUM_THREADS`](@ref JULIA_NUM_THREADS) can also be used similarly: