Skip to content

Commit

Permalink
Adds Threads.@spawncall, which evaluates arguments immediately
Browse files Browse the repository at this point in the history
This acts more like a "parallel function call", where the arguments are
evaluated immediately, in the current thread, and the function is then
invoked in any available thread with the stored argument values.

This prevents variables being "boxed" in order to capture them in the
closure.

Fix at-spawncall to work w/ kwargs; add unit tests

Rough implementation of `$`-interpolation in at-spawn

Update spawncall tests to exercise the interpolation

Fix my basic "lift $" function for $ inside :()

Add string-interp test

Escape the `$` reference in at-spawn docstring

Oops, remove spurious "end" at end of file

Factor out `$`-lifting; share b/w `@async` & `@spawn`
  • Loading branch information
NHDaly committed Dec 18, 2019
1 parent 34219a1 commit b9ad1de
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 34 deletions.
42 changes: 37 additions & 5 deletions base/task.jl
Original file line number Diff line number Diff line change
Expand Up @@ -346,18 +346,50 @@ end
Wrap an expression in a [`Task`](@ref) and add it to the local machine's scheduler queue.
"""
macro async(expr)
letargs = Base._lift_one_interp!(expr)

thunk = esc(:(()->($expr)))
var = esc(sync_varname)
quote
local task = Task($thunk)
if $(Expr(:islocal, var))
push!($var, task)
let $(letargs...)
local task = Task($thunk)
if $(Expr(:islocal, var))
push!($var, task)
end
schedule(task)
task
end
end
end

# Capture interpolated variables in $() and move them to let-block
function _lift_one_interp!(e)
letargs = Any[] # store the new gensymed arguments
_lift_one_interp_helper(e, false, letargs) # Start out _not_ in a quote context (false)
letargs
end
_lift_one_interp_helper(v, _, _) = v
function _lift_one_interp_helper(expr::Expr, in_quote_context, letargs)
if expr.head == :$
if in_quote_context # This $ is simply interpolating out of the quote
# Now, we're out of the quote, so any _further_ $ is ours.
in_quote_context = false
else
newarg = gensym()
push!(letargs, :($(esc(newarg)) = $(esc(expr.args[1]))))
return newarg # Don't recurse into the lifted $() exprs
>>>>>>> fb29992... Factor out `$`-lifting; share b/w `@async` & `@spawn`
end
schedule(task)
task
elseif expr.head == :quote
in_quote_context = true # Don't try to lift $ directly out of quotes
end
for (i,e) in enumerate(expr.args)
expr.args[i] = _lift_one_interp_helper(e, in_quote_context, letargs)
end
expr
end


# add a wait-able object to the sync pool
macro sync_add(expr)
var = esc(sync_varname)
Expand Down
25 changes: 3 additions & 22 deletions base/threadingconstructs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ Create and run a [`Task`](@ref) on any available thread. To wait for the task to
finish, call [`wait`](@ref) on the result of this macro, or call [`fetch`](@ref)
to wait and then obtain its return value.
(Values can be interpolated into `@spawn` via `$` to evaluate them in the current task.)
(Values can be interpolated into `@spawn` via `\$` to evaluate them in the current task.)
!!! note
This feature is currently considered experimental.
Expand All @@ -116,26 +116,9 @@ to wait and then obtain its return value.
This macro is available as of Julia 1.3.
"""
macro spawn(expr)
# Capture interpolated variables in $() and move them to let-block
letargs = Any[] # store the new gensymed arguments
lift_one_interp!(v) = v
function lift_one_interp!(expr::Expr)
if expr.head == :quote # Don't try to lift $ out of quotes
return expr
end
if expr.head == :$
newarg = gensym()
push!(letargs, :($(esc(newarg)) = $(esc(expr.args[1]))))
return newarg # Don't recurse into the $() exprs
end
for (i,e) in enumerate(expr.args)
expr.args[i] = lift_one_interp(e)
end
expr
end
lifted_expr = lift_one_interp!(expr)
letargs = Base._lift_one_interp!(expr)

thunk = esc(:(()->($lifted_expr)))
thunk = esc(:(()->($expr)))
var = esc(Base.sync_varname)
quote
let $(letargs...)
Expand All @@ -149,5 +132,3 @@ macro spawn(expr)
end
end
end

end
37 changes: 30 additions & 7 deletions test/threads_exec.jl
Original file line number Diff line number Diff line change
Expand Up @@ -704,26 +704,49 @@ catch ex
@test ex.error isa ArgumentError
end

@testset "@spawncall" begin
@testset "@spawn interpolation" begin
# Issue #30896: evaluating argumentss immediately
begin
outs = zeros(5)
@sync begin
local i = 1
while i <= 5
Threads.@spawncall setindex!(outs, i, i)
Threads.@spawn setindex!(outs, $i, $i)
i += 1
end
end
@test outs == 1:5
end

# Args
@test fetch(Threads.@spawncall 2+2) == 4
@test fetch(Threads.@spawncall Int(2.0)) == 2
@test fetch(Threads.@spawn 2+$2) == 4
@test fetch(Threads.@spawn Int($(2.0))) == 2
a = 2
@test fetch(Threads.@spawncall *(a,a)) == a^2
@test fetch(Threads.@spawn *($a,$a)) == a^2
# kwargs
@test fetch(Threads.@spawncall sort([3 2; 1 0], dims=2)) == [2 3; 0 1]
@test fetch(Threads.@spawncall sort([3 2; 1 0]; dims=2)) == [2 3; 0 1]
@test fetch(Threads.@spawn sort($([3 2; 1 0]), dims=2)) == [2 3; 0 1]
@test fetch(Threads.@spawn sort([3 $2; 1 $0]; dims=$2)) == [2 3; 0 1]

# Supports multiple levels of interpolation
@test fetch(Threads.@spawn :($a)) == a
@test fetch(Threads.@spawn :($($a))) == a
@test fetch(Threads.@spawn "$($a)") == "$a"

# Test the difference between different levels of interpolation
let
oneinterp = Vector{Any}(undef, 5)
twointerps = Vector{Any}(undef, 5)
@sync begin
local i = 1
while i <= 5
Threads.@spawn setindex!(oneinterp, :($i), $i)
Threads.@spawn setindex!(twointerps, :($($i)), $i)
i += 1
end
end
# The first definition _didn't_ escape i
@test oneinterp == fill(6, 5)
# The second definition _did_ escape i
@test twointerps == 1:5
end
end

0 comments on commit b9ad1de

Please sign in to comment.