Skip to content
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

weird interaction between sleep function and Threads #43952

Open
serhii-havrylov opened this issue Jan 27, 2022 · 4 comments
Open

weird interaction between sleep function and Threads #43952

serhii-havrylov opened this issue Jan 27, 2022 · 4 comments
Labels
multithreading Base.Threads and related functionality

Comments

@serhii-havrylov
Copy link

serhii-havrylov commented Jan 27, 2022

This issue was originally mentioned here.

function ex1()
    task = @Threads.spawn begin
        st = time()
        sleep(1.0)
        return time() - st
    end

    st2 = time()
    while time() - st2 < 5 end

    println("task slept for $(fetch(task)) seconds")
end

ex1()
ex1()
ex1()
ex1()
ex1()
ex1()

gives the following result

task slept for 4.993403911590576 seconds
task slept for 5.000104904174805 seconds
task slept for 5.000086784362793 seconds
task slept for 5.000097990036011 seconds
task slept for 5.000089883804321 seconds
task slept for 5.000094175338745 seconds

it works as expected if the sleep function is replaced with a while loop

function ex2()
    task = Threads.@spawn begin
        st = time()
        while time() - st < 1
        end
        return time() - st
    end

    st2 = time()
    while time() - st2 < 5
    end

    return println("task slept for $(fetch(task)) seconds")
end

ex2()
ex2()
task slept for 1.0 seconds
task slept for 1.0 seconds

I don't really know the under-the-hood details of Julia multithreading, but I would assume that there is some weird interaction with thread no.1 (the main julia thread) inside the sleep function, since it was designed for the async tasks; hence it “blocks”(yields back to the event loop, or whatever it is called in julia) the spawned task and doesn’t continue working until the main thread yields back, which doesn’t happen until it calls fetch (exactly after 5 seconds).

julia> VERSION
v"1.7.1"
@tkf
Copy link
Member

tkf commented Jan 27, 2022

I brought it up for discussion with @jpsamaroo and @vchuravy. My understanding is that, in ex1, the task is not re-scheduled when sleep(1.0) is finished since thread 1 is in the non-yielding busy loop. It'd be reasonable to expect that task will be migrated to thread 2. But Julia runs I/O loop (including sleep callback) only in thread 1 normally unless you are in Threads.@threads for loop. Indeed ex1 finishes much faster inside of Threads.@threads for.

julia> Threads.nthreads()
2

julia> ex1()
task slept for 4.99216890335083 seconds

julia> ex1()
task slept for 5.000109910964966 seconds

julia> Threads.@threads for _ in 1:1
           ex1()
       end
task slept for 1.002121925354004 seconds

julia> Threads.@threads for _ in 1:1
           ex1()
       end
task slept for 1.002121925354004 seconds

Of course, this is a rather ugly hack. The best option usually is to put yield in the busy loop like this (or just don't spin). But I think it's reasonable to provide a hint to prefer I/O in a given block of code. See: #43919 (comment)

@tkf tkf added the multithreading Base.Threads and related functionality label Jan 27, 2022
@vtjnash
Copy link
Member

vtjnash commented Jan 27, 2022

@tkf This is what ccall(:jl_enter_threaded_region, Cvoid, ()) solves, which you have been asking me about.

@carstenbauer
Copy link
Member

which you have been asking me about.

FWIW, if you're referring to the slack question from earlier today, that was me who was asking :)

@tkf
Copy link
Member

tkf commented Jan 27, 2022

Well, I was also asking this before :)

But I figured out how threaded region works and that's why I tried @threads for. (Or rather it became clear with discussion with @jpsamaroo and @vchuravy that the issue here is the lack of threaded region).

I wonder if we want to have something like

function prefer_io(f)
    ccall(:jl_enter_threaded_region, Cvoid, ())
    try
        f()
    finally
        ccall(:jl_exit_threaded_region, Cvoid, ())
    end
end

to work around the case like the OP? It's ugly since it changes how the scheduler works globally but it at least works on arbitrary threads now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
multithreading Base.Threads and related functionality
Projects
None yet
Development

No branches or pull requests

4 participants