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.