Assignment not effective in `@spawn` in function

Define a simple function copying the contents of one array to another:

function copy_array!(array_t::AbstractArray, array_s::AbstractArray)
    array_t .= array_s

    return nothing
end

The following function performs the above function with and without @spawn and compares the results:

using Base.Threads: @spawn
using LinearAlgebra: norm

function test_spawned_copy()
    N = 1000
    A = rand(N);
    B = zeros(N);
    C = zeros(N);

    copy_array!(B, A)
    @spawn copy_array!(C, A)

    println("‖A‖ = $(norm(A))\n‖B‖ = $(norm(B))\n‖C‖ = $(norm(C))")
end

Executing the above function, we realize that copy_array! is not effective with @spawn:

julia> test_spawned_copy()
‖A‖ = 18.176417571506622
‖B‖ = 18.176417571506622
‖C‖ = 0.0  # assignment in array C didn't occur

However, if the contents of the above test function are executed in REPL, copy_array! produces the same results with and without @spawn:

julia> N = 1000;

julia> A = rand(N);

julia> B = zeros(N);

julia> C = zeros(N);

julia> copy_array!(B, A);

julia> @spawn copy_array!(C, A);

julia> println("‖A‖ = $(norm(A))\n‖B‖ = $(norm(B))\n‖C‖ = $(norm(C))");
‖A‖ = 18.054455128272014
‖B‖ = 18.054455128272014
‖C‖ = 18.054455128272014  # assignment in array C did occur

Is this an expected result? Here is the Julia version:

julia> VERSION
v"1.5.4-pre.0"

It seems that the @spawn macro from Distribution is used (sometimes).
If you do in a fresh REPL:

function copy_array!(array_t::AbstractArray, array_s::AbstractArray)
    array_t .= array_s
    return nothing
end

function test_spawned_copy()
    n = 10
    c = zeros(n);
	d = zeros(n);
    @spawn copy_array!(c, rand(n))
	Threads.@spawn copy_array!(d, rand(n))
	(c,d)
end

(c,d)=test_spawned_copy()

you get:

([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.5282095840733001, 0.5556105622927612, 0.20150965328960568, 0.31812680241750946, 0.46483288319514715, 0.013753794057519642, 0.6303004740215832, 0.22042140183053638, 0.9973105034241585, 0.346232614933345])

If you start again a new REPL and start with:

using Base.Threads: @spawn

you get:

([0.35270524970982176, 0.7986731832036376, 0.4356428356231954, 0.4497905410479941, 0.3878368060348809, 0.11946402924101096, 0.6408044427105426, 0.6883479688491787, 0.7404863070919507, 0.31914232478228866], [0.004167961824976141, 0.5848904661256766, 0.5722504383047835, 0.9284901252145417, 0.677612496185245, 0.715904376389237, 0.9944319571686495, 0.005437923152379565, 0.1644076654195401, 0.17196103627138593])

Again, a fresh REPL:

function copy_array!(array_t::AbstractArray, array_s::AbstractArray)
    array_t .= array_s
    return nothing
end
n = 10
c = zeros(n);
@which( @spawn copy_array!(c, rand(n)) )

gives

julia> @which( @spawn copy_array!(c, rand(n)) )
@spawn(__source__::LineNumberNode, __module__::Module, expr) in Distributed at C:\Users\oheil\AppData\Local\Programs\Julia 1.5.3\share\julia\stdlib\v1.5\Distributed\src\macros.jl:46

That Distributed.@spawn doesn’t work is understandable, as different processes are used where data is copied into the new process.
So, the solution is, always make sure, that Threads.@spawn is used.

Of course the REPL must be started with Threads and/or Processes:
julia.exe -t auto -p auto

Isn’t this a race condition? You are not waiting for the task to finish.

2 Likes

Thanks, @kristoffer.carlsson! For other people, the solution is to modify test_spawned_copy using wait as follows:

function test_spawned_copy()
    N = 1000
    A = rand(N);
    B = zeros(N);
    C = zeros(N);

    copy_array!(B, A)
    t = @spawn copy_array!(C, A)
    wait(t)

    println("‖A‖ = $(norm(A))\n‖B‖ = $(norm(B))\n‖C‖ = $(norm(C))")
end