Asynchronous calls

Asynchronous function calls and requests is a mechanism in AMUSE to let multiple models or code instances do work concurrently.

Normally when a function call to a code is made (be it explicitly or implicitly by e.g. referencing a particle attribute) the framework script waites on the call to finish. Any method call on an AMUSE code can be made asynchronous, by appending .asynchronous to the method’s name or (preferred) adding a keyword argument return=request. When such asynchronous call is made to a code, the Python script continues to run while the call is being made. The Python script can then perform computations or calls to other codes in AMUSE. Subsequent calls to the same code instance can be made, but they will be performed in sequence.

An asynchronous AMUSE function call returns immediately, i.e. it does not wait for the worker code to finish the function. Instead of the normal return value, the asynchronous function returns a request object, which can be used to access the result of the function call later. The request can also be added to a request pool, which manages several concurrent requests.

request1 = gravity.evolve_model(target_time, return_request=True)
# ... do something else ...
print(request1.result())

Example

Running two models simultaneously:

from amuse.community.huayno.interface import Huayno
from amuse.units import nbody_system
from amuse.ic.plummer import new_plummer_model

h1=Huayno()
h2=Huayno()

p1=new_plummer_model(100)
p2=new_plummer_model(100)

h1.particles.add_particles(p1)
h2.particles.add_particles(p2)

request1 = h1.evolve_model(1| nbody_system.time, return_request=True)
request2 = h2.evolve_model(1| nbody_system.time, return_request=True)

# join the request to make request pool that can be waited on
pool=request1.join(request2)

# wait for the requests to finish
pool.waitall()

Requests can be joined to form a pool. An empty pool can explicitly be constructed by importing from amuse.rfi.async_request import AsyncRequestsPool.

Asynchronous variable access:

# setting attribute
# normal synchronous call
h1.particles[0:10].mass = 1 | nbody_system.mass

# asynchronous call
h2.particles[0:10].request.mass = 1 | nbody_system.mass

# getting attribute
# synchronous call, returns an array
xpos = h1.particles[0:10].x

# asynchronous call, returns a request.
request = h2.particles.request.x
xpos = request.result() # retrieve result. Implicit wait for the request to finish

Requests can be used in arithmetic operations, and such operation returns a new request that can be waited on (with the corresponding result):

x1=h1.particles.request.x
x2=h2.particles.request.x

relx=x2-x1

relx=relx.result()

Caveats

  • Mixing asynchronous and synchronous calls produces correct results, but has consequences for the sequencing of the work: Performing a normal, synchronous call to a code after one or more asynchronous calls, causes an implicit wait for the asynchronous calls to complete.

  • Accessing result() on a request causes a wait for the request to complete.

  • When making several calls to the same code instance, the first call begins immediately. The second call begins only when the code is waited on, not automatically when the first call completes.

  • asynchronous access does not work (yet) with compound attributes like velocity.