Introduction to Polyphonic C#
In Polyphonic C#, methods can be defined as either
synchronous or asynchronous. When a synchronous
method is called, the caller is blocked until the method returns, as
is normal in C#. However, when an asynchronous method is called, there
is no result and the caller proceeds immediately without being
blocked. Thus from the caller's point of view, an asynchronous method
is like a void
one, but with the useful extra guarantee
of returning immediately. We often refer to asynchronous methods as
messages, as they are a one-way communication from caller to
receiver (think of posting a letter rather as opposed to asking a
question during a face-to-face conversation).
By themselves, asynchronous method declarations are not particularly novel. Indeed, .NET already has a widely-used set of library classes which allow any method to be invoked asynchronously (though note that in this standard pattern it is the caller who decides to invoke a method asynchronously, whereas in Polyphonic C# it is the callee (defining) side which declares a particular method to be asynchronous). The significant innovation in Polyphonic C# is the way in which method bodies are defined.
In most languages, including C#, methods in the signature of a class are in bijective correspondence with the code of their implementations - for each method which is declared, there is a single, distinct definition of what happens when that method is called. In Polyphonic C#, however, a body may be associated with a set of (synchronous and/or asynchronous) methods. We call such a definition a chord, and a particular method may appear in the header of several chords. The body of a chord can only execute once all the methods in its header have been called. Thus, when a polyphonic method is called there may be zero, one, or more chords which are enabled:
- If no chord is enabled then the method invocation is queued up. If the method is asynchronous, then this simply involves adding the arguments (the contents of the message) to a queue. If the method is synchronous, then the calling thread is blocked.
- If there is a single enabled chord, then the arguments of the calls involved in the match are de-queued, any blocked thread involved in the match is awakened, and the body runs.
- When a chord which involves only asynchronous methods runs, then it does so in a new thread.
- If there are several chords which are enabled then an unspecified one of them is chosen to run.
- Similarly, if there are multiple calls to a particular method queued up, we do not specify which call will be de-queued when there is a match.
Example: A Simple Buffer
Here is the simplest interesting example of a Polyphonic C# class:
public class Buffer {
public String get() & public async put(String s) {
return s;
}
}
This class declares two methods: a synchronous one, get()
, which takes
no arguments and returns a string, and an asynchronous one, put()
,
which takes a string argument and (like all asynchronous methods)
returns no result. These two methods appear (separated by an
ampersand) in the header of a single chord, the body of which consists
of the return statement.
Now assume that b
is an instance of Buffer and that
producer and consumer threads wish to communicate via
b
. Producers make calls to put()
, which,
since the method is asynchronous, do not block. Consumers make calls
to get()
, which, since the method is synchronous, will
block until or unless there is a matching call to
put()
. Once b
has received both a
put()
and a get()
, the body runs and the
argument to the put()
is returned as the result of the
call to get()
. Multiple calls to get()
may
be pending before a put()
is received to reawaken one of
them and multiple calls to put()
may be made before their
arguments are consumed by subsequent get()
s. Note
that
- The body of the chord runs in the (reawakened) thread corresponding to the matched call to get(). Hence no new threads are spawned in this example.
- The code which is generated by the class definition above is completely thread safe. The compiler automatically generates the locking necessary to ensure that, for example, the argument to a particular call of put() cannot be delivered to two distinct calls to get(). Furthermore (though it makes little difference in this small example), the locking is fine-grained and brief - polyphonic methods do not lock the whole object and are not executed with "monitor semantics".
- The reader may wonder how we know which of the methods involved in a chord gets the returned value. The answer is that it is always the synchronous one, and there can be at most one synchronous method involved in a chord.