Skip to content

Commit

Permalink
Default to 1 interactive thread (#57087)
Browse files Browse the repository at this point in the history
  • Loading branch information
IanButterworth authored Jan 28, 2025
1 parent d5523b6 commit fbe8656
Show file tree
Hide file tree
Showing 12 changed files with 143 additions and 95 deletions.
7 changes: 7 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions base/channels.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 3 additions & 3 deletions doc/src/manual/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions doc/src/manual/methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`:
Expand All @@ -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"
Expand Down
24 changes: 20 additions & 4 deletions doc/src/manual/multi-threading.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand All @@ -22,13 +24,20 @@ 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.

!!! 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
Expand Down Expand Up @@ -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:
Expand Down
25 changes: 15 additions & 10 deletions src/jloptions.c
Original file line number Diff line number Diff line change
Expand Up @@ -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] == ',') {
Expand All @@ -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>; 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>; m must be an integer >= 0");
}
jl_options.nthreadpools++;
}
}
else {
Expand All @@ -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>,<m>; 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=<n>,<m>; 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
Expand Down
13 changes: 8 additions & 5 deletions src/threading.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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;
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions stdlib/REPL/test/replcompletions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion test/gcext/gcext-test.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions test/gcext/gcext.c
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
21 changes: 17 additions & 4 deletions test/threads.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading

0 comments on commit fbe8656

Please sign in to comment.