Julia Language Parallel Processing @spawn and @spawnat


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")

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:


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

julia> fetch(result)

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

Did it work? Well, let's see by having worker 2 try to print a.

julia> @spawnat 2 println(a)


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)

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))

julia> @spawnat 2 println(a)

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")))

julia> @spawnat 2 println(c)

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.