Asynchronous Calls

Overview

As we've seen in Separate Calls, feature calls to a non-separate target are always synchronous. Furthermore, queries are always synchronous as well, because the caller has to wait for the result.

Target Query Command
non-separate synchronous synchronous
separate synchronous potentially asynchronous

Asynchronous calls can therefore only happen on commands with a separate target. Indeed, such calls are by default executed asynchronously, but there are some important exceptions to this rule. A command to a separate target is executed synchronously if any of the following applies:

  • The client (caller) and supplier (target) region are the same.
  • The target region is passive.
  • The callee needs a lock currently held by the caller (lock passing).
  • The caller holds the locks of the callee (separate callbacks).

Triggers for Synchronization

Same Regions

The first case happens when a reference is declared separate, but happens to be non-separate. This case follows directly from the type system: A non-separate type A always conforms to its variation separate A. At run-time such cases can be detected with an object test:

sep_object: separate A --... if attached {A} sep_object as non_sep_object then -- ... end

Passive Regions

In the SCOOP model, a passive region does not have a processor attached to it.This means that clients of the passive region need to apply features logged against a passive region themselves.The logical consequence of this is that all call to a passive region, including commands, are executed synchronously.

Lock Passing

Lock passing is another source of synchronization. It is one of the trickiest issues to detect, and to fully understand it we must first introduce a few more definitions.

In Exclusive Access we have learned that an object is controlled if it appears as a formal argument of the enclosing routine. SCOOP however always grants exclusive access over a whole region. We therefore introduce the new term Lock:

Definition -- Lock: Exclusive access to a SCOOP region and all objects therein.

Note the difference between controlled and locked:

  • Controlled applies to a single object, whereas locked applies to a region.
  • The controlled property can be determined statically at compile-time, whereas locked is determined at runtime.
  • The set of controlled objects of a processor is always a subset of the set of objects in locked regions.

Note: In terms of implementation, a lock corresponds to an open call queue to a region.

Now consider small classes HASH_STORAGE and EXAMPLE:class HASH_STORAGE feature hash_code: INTEGER set_hash_code (a_string: separate STRING) do hash_code := a_string.hash_code end end class EXAMPLE feature run (a_hash_storage: separate HASH_STORAGE; a_string: separate STRING) do a_hash_storage.set_hash_code (a_string) io.put_integer (a_hash_storage.hash_code) end end

You might notice a problem here:In the feature {EXAMPLE}.run, exclusive access to 'a_hash_storage' and 'a_string' is guaranteed by the SCOOP semantics. Or in other words, the corresponding regions are locked. The feature {HASH_STORAGE}.set_hash_code however needs access to a_string as well. In the SCOOP model, as seen so far, this would result in a deadlock. The handler of the HASH_STORAGE object waits for exclusive access on the string object, and the EXAMPLE object waits for the query {HASH_STORAGE}.hash_code to return.

To resolve this problem, SCOOP implements a technique called Lock Passing. Locks on regions can be passed to the handler of the target of a separate call. Lock passing happens whenever the client processor (the handler of the EXAMPLE object) has locked a region that holds an object which is passed as an actual argument to a separate call. Note that this also includes non-separate reference objects, because a processor always holds a lock over its own region.

When a client has passed its locks to the supplier processor, it cannot continue execution until the called feature has been applied by the supplier processor, and the supplier processor has given back the locks to the client. Therefore, this type of call must be synchronous.

Note: During lock passing, a processor gives away all the locks that it currently holds, including the lock on itself.

Note: Lock passing happens for every synchronous call, in particular also for queries and passive processors.

The advantage of lock passing is that it enables some very common programming patterns without triggering a deadlock. The disadvantage, however, is that it's hard to tell when it happens. However, there are a few cases when lock passing is guaranteed to happen, namely when the actual argument passed to a separate call is

  • a formal argument of the enclosing routine,
  • of a non-separate reference type or
  • Current.

There are, however, some cases where it's not immediately obvious that lock passing happens.For example, a region might be locked because of a controlled argument somewhere further up in the call stack (i.e. not the enclosing routine, but the caller of that routine), or because an object is passed as an argument which happens to be on the same region as one of the controlled objects.

There is a workaround to disable lock passing for a specific call:async_call (a_procedure: separate PROCEDURE [TUPLE]) do a_procedure.call (Void) end example (a_object: separate ANY) do async_call (agent a_object.some_feature (Current)) end

The feature async_call can be defined somewhere in the project and can be reused. The downside is that an agent needs to be created, but there's no lock passing happening, because all arguments to the agent are closed and only Void is passed to the separate call which cannot trigger lock passing.However, this mechanism should be used with some care, because it's easy to run into one of the above mentioned deadlocks.

Separate Callbacks

The last occurrence of synchronous calls is closely related to lock passing. If a processor A has passed a non-separate reference argument to another processor B, and thus has passed its locks away, it cannot proceed its execution. Sometimes however processor B has to log some calls back to A, which is called a separate callback.

Definition -- Separate Callback : A separate call where the caller holds the locks of the callee.

During a separate callback processor B has to give back the locks it has previously received from A.This in turn means B has to wait until A has finished its execution of the separate callback and returned the locks, which effectively makes the call synchronous.