Can the threads in the :interactive pool also undertake heavy computing tasks?

(From a related issue Unsticky Tasks stick to threadpool since 1.10, causing issues since 1.12 · Issue #59936 · JuliaLang/julia · GitHub
But here I’m not referring to the functional API Task, instead, I’m talking about @spawn itself and the interactive threadpool.)

Edit: you may skip this post and see post #3 directly, which is more meaningful.

Currently there are only 2 available thread pools—:default and :interactive.

I want to run a few (e.g. one or two) computing (blocking) tasks in the non-default pool (so the only way out is to spawn them in the interactive pool.) The reason is that the default pool is reserved for other heavier tasks.

I find it not possible to achieve what I want according to the current behavior, as follows.

I launch the julia program can_it_echo.jl from the zsh in Linux.

can_it_echo.jl

println("$PROGRAM_FILE> threads_setting = $((Threads.nthreads(), Threads.nthreads(:interactive)))")
function bzwt(t) # it's merely a busy waiting function that blocks a thread for t seconds
    tdue = time() + t
    while true
        try
            @assert time() < tdue
        catch e
            return
        end
    end
end;
function main()
    Threads.@spawn :interactive println("main()> begins...")
    Threads.@spawn :interactive bzwt(2.5)
    Threads.@spawn :interactive bzwt(2.5)
    Threads.@spawn :interactive println("main()> echo")
end;
main()

Here is what I get in zsh

The results suggest that @spawn :interactive bzwt(2.5) is not flexibly scheduled so it can be run on any available idle threads within the :interactive pool—I’m having sufficiently 4 threads in this test.

(The behavior seems to be a bit random, so you are certain to reproduce the reported behavior above—the main() > echo is unseen immediately after main()> begins...—as long as you try multiple times. And this behavior is not due to insufficient threads—it occurs even if I use --threads=64,64)


The direct consequence is that: the :interactive pool is unusable other than logging messages. So given this, I’m wondering

  • is there any other methods that I can create some other user pools other than the existing two? (e.g. :default2, :default3…). So I can schedule tasks within those new pools?
  • What is a solution to the current situation?

(post deleted by author)

Sorry, it seems that my test was improper.


The proper test file should be

can_it_echo.jl

println("$PROGRAM_FILE> " *
"threads_setting = $((Threads.nthreads(), Threads.nthreads(:interactive))), " *
"id = $(Threads.threadid()), " *
"pool = $(Threads.threadpool())"
)
function bzwt(t) # it's merely a busy waiting function that blocks a thread for t seconds
    tdue = time() + t
    while true
        try
            time() < tdue || error()
        catch e
            return
        end
    end
end
f(v, i, t0) = (v[i] = time() - t0; nothing)
function main()
    v = [NaN, NaN]
    t0 = time()
    tp = (
        Threads.@spawn(:interactive, f(v, 1, t0)),
        Threads.@spawn(:interactive, bzwt(2.0)),
        Threads.@spawn(:interactive, bzwt(2.0)),
        Threads.@spawn(:interactive, bzwt(2.0)),
        Threads.@spawn(:interactive, f(v, 2, t0))
    )
    println("main> Tasks spawned. Now begins to wait...")
    foreach(wait, tp)
    println("main> v = $v")
    println("main> quit.")
end
main()

Now it is expected (I think so).

❯ julia --threads=2,4 can_it_echo.jl
can_it_echo.jl> threads_setting = (2, 4), id = 1, pool = interactive
main> v = [0.012079954147338867, 0.017039060592651367]
main> quit.

The above result seems to be deterministic and stable behavior.

By comparison, if you duplicate one more line of Threads.@spawn(:interactive, bzwt(2.0)), the results becomes non-deterministic, sometimes with an >2.0 entry.


So, my conclusion is:

  • The threads in :interactive pool can indeed serve as a separate computing resource from the threads in the :default pool, as long as you leave one non-busy thread to print desired texts.

Printing in threads can be done with printf to avoid serialization to thread 1. I.e. with something like

@ccall printf("Hi %d\n"::Cstring; 23::Cint)::Cint

Note the semicolon after the initial format string. It’s important because the C printf is a function with variable number of arguments. The ; ensures this is coded properly.

1 Like

It appears that it won’t automatically flush (update the printing status) without adding a \n.

Why? How to resolve this, e.g. manual flush? is this normal operation?

That’s right. You can call Libc.flush_cstdio() to flush.

To be honest, until here I notice that some operations I do everyday is slow.

e.g. print in julia is very slow.
e.g. t = time() in julia is slow (relatively), (it appears that time_ns() could be a bit faster).

Because I need to use these functions to monitor my program so that I can ensure they are running properly. I guess I will again adjust my code accordingly…

It’s not very accurate to measure such short intervals, anyway. The fastest timer is to do it directly in llvm, but I’m not sure how it does with memory fencing and similar. And it’s not calibrated in nanoseconds, it’s just clock ticks. And the clock speed may vary. Unless very specific measurements, I would stick to time_ns().

function clockticks()
    Core.Intrinsics.llvmcall(
        ("""
         declare i64 @llvm.readycyclecounter()
         define i64 @entry() {
           %cycles = call i64 @llvm.readcyclecounter()
           ret i64 %cycles
         }
         """, "entry"), Int64, Tuple{})
end
start = clockticks()
sleep(1)
elapsed = clockticks() - start