The macros @spawn
and @spawnat
are two of the tools that Julia makes available to assign tasks to workers. Here is an example:
julia> @spawnat 2 println("hello world")
RemoteRef{Channel{Any}}(2,1,3)
julia> From worker 2: hello world
Both of these macros will evaluate an expression on a worker process. The only difference between the two is that @spawnat
allows you to choose which worker will evaluate the expression (in the example above worker 2 is specified) whereas with @spawn
a worker will be automatically chosen, based on availability.
In the above example, we simply had worker 2 execute the println function. There was nothing of interest to return or retrieve from this. Often, however, the expression we sent to the worker will yield something we wish to retrieve. Notice in the example above, when we called @spawnat
, before we got the printout from worker 2, we saw the following:
RemoteRef{Channel{Any}}(2,1,3)
This indicates that the @spawnat
macro will return a RemoteRef
type object. This object in turn will contain the return values from our expression that is sent to the worker. If we want to retrieve those values, we can first assign the RemoteRef
that @spawnat
returns to an object and then, and then use the fetch()
function which operates on a RemoteRef
type object, to retrieve the results stored from an evaluation performed on a worker.
julia> result = @spawnat 2 2 + 5
RemoteRef{Channel{Any}}(2,1,26)
julia> fetch(result)
7
The key to being able to use @spawn
effectively is understanding the nature behind the expressions that it operates on. Using @spawn
to send commands to workers is slightly more complicated than just typing directly what you would type if you were running an "interpreter" on one of the workers or executing code natively on them. For instance, suppose we wished to use @spawnat
to assign a value to a variable on a worker. We might try:
@spawnat 2 a = 5
RemoteRef{Channel{Any}}(2,1,2)
Did it work? Well, let's see by having worker 2 try to print a
.
julia> @spawnat 2 println(a)
RemoteRef{Channel{Any}}(2,1,4)
julia>
Nothing happened. Why? We can investigate this more by using fetch()
as above. fetch()
can be very handy because it will retrieve not just successful results but also error messages as well. Without it, we might not even know that something has gone wrong.
julia> result = @spawnat 2 println(a)
RemoteRef{Channel{Any}}(2,1,5)
julia> fetch(result)
ERROR: On worker 2:
UndefVarError: a not defined
The error message says that a
is not defined on worker 2. But why is this? The reason is that we need to wrap our assignment operation into an expression that we then use @spawn
to tell the worker to evaluate. Below is an example, with explanation following:
julia> @spawnat 2 eval(:(a = 2))
RemoteRef{Channel{Any}}(2,1,7)
julia> @spawnat 2 println(a)
RemoteRef{Channel{Any}}(2,1,8)
julia> From worker 2: 2
The :()
syntax is what Julia uses to designate expressions. We then use the eval()
function in Julia, which evaluates an expression, and we use the @spawnat
macro to instruct that the expression be evaluated on worker 2.
We could also achieve the same result as:
julia> @spawnat(2, eval(parse("c = 5")))
RemoteRef{Channel{Any}}(2,1,9)
julia> @spawnat 2 println(c)
RemoteRef{Channel{Any}}(2,1,10)
julia> From worker 2: 5
This example demonstrates two additional notions. First, we see that we can also create an expression using the parse()
function called on a string. Secondly, we see that we can use parentheses when calling @spawnat
, in situations where this might make our syntax more clear and manageable.