Skip to content

Consider a Threads.Future struct implementation? #60377

@quinnj

Description

@quinnj

I have this little Future utility that I've found really useful in a number of projects/packages:

mutable struct Future{T}
    const notify::Threads.Condition
    @atomic set::Int8 # if 0, result is undefined, 1 means result is T, 2 means result is an exception
    result::Union{Exception, T} # undefined initially
    Future{T}() where {T} = new{T}(Threads.Condition(), 0)
end

function Future{T}(f)
    fut = Future{T}()
    Threads.@spawn try
        notify(fut, f())
    catch e
        notify(fut, capture(e))
    end
    return fut
end

function Base.wait(f::Future{T}) where {T}
    set = @atomic f.set
    set == 1 && return f.result::T
    set == 2 && throw(f.result::Exception)
    lock(f.notify) # acquire barrier
    try
        set = f.set
        set == 1 && return f.result::T
        set == 2 && throw(f.result::Exception)
        wait(f.notify)
    finally
        unlock(f.notify) # release barrier
    end
    if f.set == 1
        return f.result::T
    else
        @assert isdefined(f, :result)
        throw(f.result::Exception)
    end
end

capture(e::Exception) = CapturedException(e, Base.backtrace())

Base.notify(f::Future{Nothing}) = notify(f, nothing)
function Base.notify(f::Future{T}, x) where {T}
    lock(f.notify) # acquire barrier
    try
        if f.set == Int8(0)
            if x isa Exception
                set = Int8(2)
                f.result = x
            else
                set = Int8(1)
                f.result = convert(T, x)
            end
            @atomic :release f.set = set
            notify(f.notify)
        end
    finally
        unlock(f.notify)
    end
    nothing
end

It's essentially a mix between a Threads.Event and a single-value Channel{T}. Personally, I like the simplicity of just wait and notify, while also keeping the exception-handling of channels.

I know we're starting to accumulate a variety of "flavors" of these kinds of definitions in Base, so if we feel like it'd be "one extra thing" I'll probably put it in ConcurrentUtilities.jl, but I figured it's general enough and simple enough, it might be worth including in Base directly.

For example usage, you end up with really convenient syntax like:

function do_foo_async()
    return Future{Result}() do
        compute_result_async()
    end
end

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions