@@ -22,14 +22,14 @@ See also: `BLAS.get_num_threads` and `BLAS.set_num_threads` in the
22
22
"""
23
23
nthreads () = Int (unsafe_load (cglobal (:jl_n_threads , Cint)))
24
24
25
- function threading_run (func )
25
+ function threading_run (fun, static )
26
26
ccall (:jl_enter_threaded_region , Cvoid, ())
27
27
n = nthreads ()
28
28
tasks = Vector {Task} (undef, n)
29
29
for i = 1 : n
30
- t = Task (func)
31
- t. sticky = true
32
- ccall (:jl_set_task_tid , Cint, (Any, Cint), t, i- 1 )
30
+ t = Task (() -> fun (i)) # pass in tid
31
+ t. sticky = static
32
+ static && ccall (:jl_set_task_tid , Cint, (Any, Cint), t, i- 1 )
33
33
tasks[i] = t
34
34
schedule (t)
35
35
end
@@ -48,15 +48,14 @@ function _threadsfor(iter, lbody, schedule)
48
48
quote
49
49
local threadsfor_fun
50
50
let range = $ (esc (range))
51
- function threadsfor_fun (onethread= false )
51
+ function threadsfor_fun (tid = 1 ; onethread= false )
52
52
r = range # Load into local variable
53
53
lenr = length (r)
54
54
# divide loop iterations among threads
55
55
if onethread
56
56
tid = 1
57
57
len, rem = lenr, 0
58
58
else
59
- tid = threadid ()
60
59
len, rem = divrem (lenr, nthreads ())
61
60
end
62
61
# not enough iterations for all the threads?
@@ -86,15 +85,17 @@ function _threadsfor(iter, lbody, schedule)
86
85
end
87
86
end
88
87
end
89
- if ccall (:jl_in_threaded_region , Cint, ()) != 0
88
+ if $ (schedule === :dynamic )
89
+ threading_run (threadsfor_fun, false )
90
+ elseif ccall (:jl_in_threaded_region , Cint, ()) != 0
90
91
$ (if schedule === :static
91
92
:(error (" `@threads :static` cannot be used concurrently or nested" ))
92
93
else
93
94
# only use threads when called from outside @threads
94
- :(threadsfor_fun (true ))
95
+ :(threadsfor_fun (onethread = true ))
95
96
end )
96
97
else
97
- threading_run (threadsfor_fun)
98
+ threading_run (threadsfor_fun, true )
98
99
end
99
100
nothing
100
101
end
@@ -110,15 +111,73 @@ A barrier is placed at the end of the loop which waits for all tasks to finish
110
111
execution.
111
112
112
113
The `schedule` argument can be used to request a particular scheduling policy.
113
- The only currently supported value is `:static`, which creates one task per thread
114
- and divides the iterations equally among them. Specifying `:static` is an error
115
- if used from inside another `@threads` loop or from a thread other than 1.
114
+
115
+ Except for `:static` scheduling, how the iterations are assigned to tasks, and how the tasks
116
+ are assigned to the worker threads is undefined. The exact assignments can be different
117
+ for each execution. The scheduling option is a hint. The loop body code (including any code
118
+ transitively called from it) must not make assumptions about the distribution of iterations
119
+ to tasks or the worker thread in which they are executed. The loop body for each iteration
120
+ must be able to make forward progress independent of other iterations and be free from data
121
+ races. As such, synchronizations across iterations may deadlock.
122
+
123
+ For example, the above conditions imply that:
124
+
125
+ - The lock taken in an iteration *must* be released within the same iteration.
126
+ - Communicating between iterations using blocking primitives like `Channel`s is incorrect.
127
+ - Write only to locations not shared across iterations (unless a lock or atomic operation is used).
128
+
129
+
130
+ Schedule options are:
131
+ - `:static` creates one task per thread and divides the iterations equally among
132
+ them, assigning each task specifically to each thread.
133
+ Specifying `:static` is an error if used from inside another `@threads` loop
134
+ or from a thread other than 1.
135
+ - `:dynamic` will schedule iterations dynamically to available worker threads,
136
+ assuming that the workload for each iteration is uniform.
137
+
138
+ Without the scheduler argument, the exact scheduling is unspecified; i.e. it may be
139
+ different across Julia releases. Currently, the behavior is dependent on the calling thread.
140
+ The default is `:static` when called from thread 1. The loop will be executed without threading
141
+ when called from other threads.
116
142
117
143
The default schedule (used when no `schedule` argument is present) is subject to change.
118
144
145
+ For example, an illustration of the different scheduling strategies where `busywait`
146
+ is a non-yielding timed loop that runs for a number of seconds.
147
+
148
+ ```julia-repl
149
+ julia> function busywait(seconds)
150
+ tstart = time_ns()
151
+ while (time_ns() - tstart) / 1e9 < seconds
152
+ end
153
+ end
154
+
155
+ julia> @time begin
156
+ Threads.@spawn busywait(5)
157
+ Threads.@threads :static for i in 1:Threads.nthreads()
158
+ busywait(1)
159
+ end
160
+ end
161
+ 6.003001 seconds (16.33 k allocations: 899.255 KiB, 0.25% compilation time)
162
+
163
+ julia> @time begin
164
+ Threads.@spawn busywait(5)
165
+ Threads.@threads :dynamic for i in 1:Threads.nthreads()
166
+ busywait(1)
167
+ end
168
+ end
169
+ 2.012056 seconds (16.05 k allocations: 883.919 KiB, 0.66% compilation time)
170
+ ```
171
+
172
+ The `:dynamic` example takes 2 seconds since one of the non-occupied threads is able
173
+ to run two of the 1-second iterations to complete the for loop.
174
+
119
175
!!! compat "Julia 1.5"
120
176
The `schedule` argument is available as of Julia 1.5.
121
177
178
+ !!! compat "Julia 1.8"
179
+ The `:dynamic` option for the `schedule` argument is available as of Julia 1.8.
180
+
122
181
See also: [`@spawn`](@ref Threads.@spawn), [`nthreads()`](@ref Threads.nthreads),
123
182
[`threadid()`](@ref Threads.threadid), `pmap` in [`Distributed`](@ref man-distributed),
124
183
`BLAS.set_num_threads` in [`LinearAlgebra`](@ref man-linalg).
@@ -133,7 +192,7 @@ macro threads(args...)
133
192
# for now only allow quoted symbols
134
193
sched = nothing
135
194
end
136
- if sched != = :static
195
+ if sched != = :static && sched != = :dynamic
137
196
throw (ArgumentError (" unsupported schedule argument in @threads" ))
138
197
end
139
198
elseif na == 1
0 commit comments