The research language Cω promised C# users a more pleasant world of concurrent programming. Cω presents a simple,
declarative and powerful model of concurrency - join patterns -
applicable both to multithreaded applications and to the orchestration
of asynchronous, event-based distributed applications.
Using Generics, we can now provide join patterns as a .NET library rather than a language extension. We call this library Joins for short.
The library works with C#, but also VB and other .NET languages.
At the heart of Joins is the Join class.
This class provides a mostly declarative, type-safe mechanism for defining
thread-safe asynchronous and synchronous communication channels and
patterns, for use by concurrently executing local threads or remote
processes. The communication channels are values of special delegate types
initialized from a common join object. Communication and/or
synchronization takes place by invoking these delegates, passing
arguments and optionally waiting for replied return values. The
allowable communication patterns as well as their effects are
declared using join patterns: bodies of code whose execution is
guarded by linear combinations of channels. The body, or
continuation, of a join pattern is provided by the user as a delegate
that may manipulate external resources protected by the join object.
Here's a simple example of using C# 2.0 and the Joins library to declare a string buffer for shared communication between threads:
class Buffer {
public readonly Asynchronous.Channel<string> Put;
public readonly Synchronous<string>.Channel Get;
public Buffer() {
Join join = Join.Create();
join.Initialize(out Put);
join.Initialize(out Get);
join.When(Get).And(Put).Do(delegate(string s) {
return s;
});
}
}
This code declares a buffer class with two communication channels. The Put field contains an asynchronous channel whose
delegate Invoke method takes a string argument and returns void (immediately).
The Get field contains a synchronous channel whose
delegate Invoke method returns a string but takes no argument.
A producer thread asynchronously sends a string by calling Put(s), never waiting.
A consumer thread synchronously requests a string by calling Get(), possibly waiting.
The join pattern constructed by the call to join.When declares that a caller of Get may wait until/or unless there has been some matching call to Put, in which case the argument s of that Put is consumed (atomically) and returned to the caller. Since this is the only pattern involving
Get, a caller of Get must wait for a matching Put(s).
The Joins library has much wider application than just
encoding buffers. In general,
a channel may be involved in multiple join patterns and a pattern may join any number of channels. Additionally,
patterns that join only asynchronous channels may be used to conditionally spawn new threads.
Join patterns support natural encodings of traditional concurrency
constructs like locks and semaphores as well as higher-level abstractions such as Futures and Active Objects. The library can also be used in distributed applications, e.g. using .NET Remoting.
The Joins binaries are available as an MSR download. Installation requires the .NET Framework 2.0.
The download includes a tutorial, reference documentation, samples and demos.
Some flavour of Visual Studio 2005 is recommended for building samples.
A separate paper and slide deck describe the implementation of the Joins library.
The Joins library was written by Claudio Russo, adapting techniques
proposed by Nick Benton, Cédric Fournet
and Luca Cardelli for the implementation of Polyphonic C# and Cω. It would not have been possible without
Andrew Kennedy and Don Syme's pioneering work on
Generics for .NET.