-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature request: prevent spawning to thread 1 #34267
Comments
Alternately, of course, the equivalent of |
Mostly a duplicate of #16350. We could add a thread reserved for latency-bound tasks. |
Agreed that this issue and #34240 are the same. #16350 is highly-related but a little different, in that a single thread was being used by both a mostly-blocking task and the REPL. (If the I can close this one out if you want to just track the other - your guys' call. |
Also discussed here (for crossref): https://discourse.julialang.org/t/threading-1-3-success-story/27111/10 |
I'll just comment that I've had a desire for something like this as well; particularly in the context of running a microservice/kernel, you want to keep a "heartbeat" thread always ready to respond/beat, while being able to spawn threaded-tasks elsewhere w/o worrying about completely saturating resources to the point that the heartbeat can't respond. What's the path forward here? I saw that @JeffBezanson suggested potentially having a |
Is this as simple as... task.jl:542
tid = 0
if ccall(:jl_enqueue_task, Cint, (Any,), t) != 0
# if multiq is full, give to a random thread (TODO fix)
+ if t.background # Perhaps nontrivial like this due to Core.Task mod, but you get the idea
+ tid = mod(time_ns() % Int, Threads.nthreads()-1) + 2
+ else
tid = mod(time_ns() % Int, Threads.nthreads()) + 1
+ end
ccall(:jl_set_task_tid, Cvoid, (Any, Cint), t, tid-1)
push!(Workqueues[tid], t)
end ? I'd be happy to do it and test, but I don't have the prereqs to build Julia. Is there a simple way to toggle the Base reference? (Seems like I read that at some point) |
Of course, you’d need an |
What would that implementation do, if Julia is started with a single thread? IMHO, it needs some dynamic thread creation to be useful. I.e. start Julia with a single thread and increase the pool once |
Oh, totally agreed. The above code block is in the multi-threaded branch of the scheduler anyways - no change to single-thread. If it is a truly single thread situation with no ability to grow the thread count, then a non- |
I think initially, something like |
Yes, a step in-between is absolutely a good thing. Would prefer throwing an error instead of getting stuck. Would be great getting something in shape for 1.4. |
No dice on the fix above - that "multiq full" branch never gets hit on my machine. (PC, 8 threads). So the thread assignment is being handled at the C level. If I try to use the |
Digging in a little more, I thought I found a 0/1 scheduler issue, but was wrong. 0 is just an uninitialized thread id. Also the "multiq full" conditional branch will never get hit - the limit from |
Well, there is a hacked solution to prevent Thread 1 activity from |
Okay, folks, I have found a temporary solution for this and packaged it up in the ThreadPools.jl package I just submitted for registration. It uses the C-level I don't know where things will end up with the higher-level concurrency conversations going on, but hopefully the few who have run into this issue will find the package helpful. Once you all decide on an overall strategy, I'd be happy to help implement. @quinnj - if this needs something more to help your use case, please let me know. |
FWIW, function Threads.pin(t::Task, tid::UInt16)
ccall(:jl_set_task_tid, Cvoid, (Any, Cint), t, tid-1)
schedule(t)
return t
end with schedule(Threads.pin(@task(work), tid))) (which does not currently exist but should) |
For whatever reason, any time I tried to use Efficiency on that call isn't super important - it is a one-time call to set up the Task Channels. After that, they do all the work. Feedback on the structure is welcome - this is my first Julia package, and I've only been using the language for a couple months. Thx. |
Which I realize does give almost the same behaviors as: @threads for i = 1:nthreads()
i == 1 || @async do_some_work
end (aka what you know as Calling |
I see your point, I could have implemeted the setup as (pseudocode) @threads for i = 1:nthreads()
i == 1 || chans[i-1] = Channel{Task}...
end instead of using |
@vtjnash, thanks. I moved to your suggestion above - code is simpler, and tests all pass. |
IIUC julia> begin
chan = Channel(Inf)
@sync Threads.@threads for i in 1:Threads.nthreads()
i == 1 || @async @sync Threads.@threads for j in 1:Threads.nthreads()
j == 1 || @async push!(chan, (i, j, Threads.threadid()))
end
end
close(chan)
sort!(collect(chan))
end
9-element Array{Any,1}:
(2, 2, 2)
(2, 3, 2)
(2, 4, 2)
(3, 2, 3)
(3, 3, 3)
(3, 4, 3)
(4, 2, 4)
(4, 3, 4)
(4, 4, 4) So I guess it's not a replacement of |
Regarding growable thread pool, it may also be useful for avoiding deadlocks when you are It'd be nice to resolve this issue quickly since it is a common practice to allocate per-thread workspace assuming julia/stdlib/Random/src/RNGs.jl Lines 309 to 311 in 45a0381
julia/stdlib/LinearAlgebra/src/LinearAlgebra.jl Lines 440 to 442 in 45a0381
Lines 42 to 43 in 45a0381
Code like this is not forward-compatible to future Julia versions with growable thread pool. |
@tkf - you are correct. The scheduler for Julia (at least for me in 1.3.1-land) only allows the primary thread to spawn tasks to alternate threads. If a non-primary thread schedules a Task, it goes to the same thread mandatorily. To see this, let's remove your thread id constraint above: julia> begin
chan = Channel(Inf)
@sync Threads.@threads for i in 1:Threads.nthreads()
@async @sync Threads.@threads for j in 1:Threads.nthreads()
@async push!(chan, (i, j, Threads.threadid()))
end
end
close(chan)
sort!(collect(chan))
end
16-element Array{Any,1}:
(1, 1, 1)
(1, 2, 2)
(1, 3, 3)
(1, 4, 4)
(2, 1, 2)
(2, 2, 2)
(2, 3, 2)
(2, 4, 2)
(3, 1, 3)
(3, 2, 3)
(3, 3, 3)
(3, 4, 3)
(4, 1, 4)
(4, 2, 4)
(4, 3, 4)
(4, 4, 4) Once an inner Task is off thread 1, its children stay where it is. I ran the same experiment with the @bgthreads macro from my package. Same result (it still uses the Julia scheduler), but everything stays off of thread 1: julia> begin
chan = Channel(Inf)
@sync @bgthreads for i in 1:Threads.nthreads()
@async @sync @bgthreads for j in 1:Threads.nthreads()
@async push!(chan, (i, j, Threads.threadid()))
end
end
close(chan)
sort!(collect(chan))
end
16-element Array{Any,1}:
(1, 1, 2)
(1, 2, 2)
(1, 3, 2)
(1, 4, 2)
(2, 1, 3)
(2, 2, 3)
(2, 3, 3)
(2, 4, 3)
(3, 1, 4)
(3, 2, 4)
(3, 3, 4)
(3, 4, 4)
(4, 1, 2)
(4, 2, 2)
(4, 3, 2)
(4, 4, 2) |
@vtjnash, that Threads.pin implementation does seem to work like the @spawnat I was requesting. When I was getting lockups earlier, I was reworking the internals of |
@tanmaykm and I discussed this problem recently when trying to submit logs into CloudWatch. We're using a task to push logs over the network into CloudWatch based both on the size of a queue and regularly in time. If there's some highly parallel code using a lot of I'm not sure what's best done about this, but a relatively simple thing might be to partition the threads into multiple thread pools at startup and to have the scheduler ensure that |
I have a similar application, where one thread is required to have low latency (it is doing data acquisition with a certain buffer size) and the other thread is responsible for updating the user interface. Tim Holy had reported that Gtk (which runs a regular task on thread 1) does massively slow down |
+1 for wanting the ability to have |
We are experiencing similar issues at RelationalAI and I have just filed a bug report #41586 outlining what we've learned about the scheduling problems. The issue has several possible workarounds sketched out. Some are closely resembling what this feature request asks for, but there are alternatives that are somewhat different. In general, what we need to achieve is the ability to effectively isolate critical tasks (server infrastructure) from user-supplied workloads (query processing) and to ensure some form of low-latency guarantees on the critical tasks. |
FYI, I just added an ability to specify custom scheduler for This can be used for manually controlling worker pool across unknown code (i.e., tasks spawn from dependent/transitive dependencies), if we can propagate the scheduler to be used within a "dynamic scope" using the context variable #35833. We need to be able to do this in a compiler-aware manner to avoid dynamic dispatch on scheduler. Making the context variable compiler-aware seems to be a similar problem to how to manage the stack of compiler plugins (i.e., specializing method w.r.t calling context). Of course, another big assumption here is that all "compute-only" part of parallel Julia code are migrated to |
Thread pools now include the option to have an interactive thread which does not pick up default work |
^ Added in #42302. |
Currently the scheduler through the
Threads.@spawn
macro will assign to random thread #'s, including the primary thread. There may be times where there is a "traffic cop" activity on Thread 1 that can be impacted if a heavy-computational (in essence, blocking) task is put onto that thread. It would be nice if there was a way to cordon off Thread 1 from spawning in those cases.The text was updated successfully, but these errors were encountered: