Introduction to Polyphonic C#

Page: Prev 1, 2, 3 Next

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

  1. 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.
  2. 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".
  3. 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.

Page: Prev 1, 2, 3 Next