The join-calculus C# library
April 20, 2011 Leave a comment
In this post I will take you through the main features of my join-calculus library.
Terminology
Chord
A chord represents a pattern of methods, it is always associated to a block of code to be executed on a match.
Method
Synchronous and asynchronous methods are exposed as public instance methods of the join class. Internally they are defined as instances of the SyncMethod or AsyncMethod classes which are then used to express the chord.
Message
A message is sent to a join class by calling one of its methods. Messages are queued up and pattern matched on the chords defined in the join class.
Primary method
The primary method is the first method defined in a chord.
Body execution
The library offers several options when it comes to the execution of the body of a chord.
Sync
Synchronous chords only. In this case the body will be executed on the thread of the primary method, i.e. the first synchronous method of the chord.
(get & put).Do((T t) => { return t; });
Pool
Asynchronous chords only. The chord body will be executed on a thread of the specified ThreadPool.
(signalOne & signalTwo).Pool(() => { ... });
Spawn
Asynchronous chords only. The chord body will be executed on a new thread.
(signalOne & signalTwo).Spawn(() => { OnSignals(); });
Continue
Asynchronous chords only. The chord body will be executed on the thread of the message which completes the chord. Must be used with caution as this risks blocking the asynchronous message. It is however very convenient to use when one wants to avoid the overhead cost of creating a new thread.
(signalOne & signalTwo).Continue(() => { OnSignals(); });
SyncContext
Synchronous and asynchronous chords. The chord body will be dispatched to the specified SynchronizationContext. The body is dispatched on the Send/Post method for synchronous/asynchronous chords. By default the WindowsFormsSynchronizationContext.Current is picked up by the Join constructor, otherwise it can be explicitly specified:
mJoin.SynchronizationContext = WindowsFormsSynchronizationContext.Current; // this is redundant (signalOne & signalTwo).SyncContext(() => { OnSignals(); });
Asynchronous chords
A chord consisting of only asynchronous messages is called an asynchronous chord. Since all the messages are asynchronous there is no synchronous message on which to execute the body of the chord. Usually the body of the chord will be executed on a new thread. Depending on the amount of work performed in the body, the creation of a new thread might present too much of an overhead and it is convenient to execute it on the thread of the last message.
(async1 & async2). Continue(() => { ... });
Synchronous chords
A chord which contains at least one synchronous method is called a synchronous chord. The synchronous method must be first in the chord definition and will block until the chord is matched. The body of the chord is executed on the thread of the synchronous message. The synchronous message returns the value returned by the block:
(sync & async).Do(() => { return 1; });
Multiple synchronous chords
A chord can have multiple synchronous methods. All synchronous messages will block until either the end of the chord’s body or until they are “returned” from the body of the chord:
(sync1 & sync2).Do(() => { sync2.Return(2); return 1; });
Where sync2 returns 2 when the Return method is invoked. All methods which return a value must call their Return method before the body ends or an exception will be thrown.
Exceptions
Exceptions thrown in synchronous chords are passed to the thread of the primary method. For asynchronous chords the body must implement its own exception handling; exceptions which are allowed to escape the body are ignored. For example:
public async One (int i, Action onException) { ... } public async Two (int j) { ... } (one & two).Do((int i, int j, Action onException) => { try { ... } catch (Exception exception) { onException(exception); } });
Multipliers
When defining a chord, any method can include a multiplier, e.g.
(wait & 2*signal).Do(() => { });
which is logically equivalent to
(wait & signal & signal).Do(() => { });
Arguments to multiplied methods are passed in arrays:
public async SignalInteger(int i) { ... } (wait & n*signalInteger).Do((int[] ints) => { ... });
or
(wait & 2*signalInteger & 3*signalInteger).Do((int[] twoInts, int[] threeInts) => { ... });
Timeout
Every synchronous method can have an associated timeout and OnTimeout delegate configured. This delegate will be executed whenever an associated synchronous message has been received but not been used in a match within the specified time span. Note that this is not fully implemented yet.
Performance
The goal of this library implementation of join-calculus is to give all C# users a chance to try out join-calculus and investigate its potential in the real world. The implementation is not at all optimized and it should be fairly easy to do some simple optimization. A better implementation could take advantage of runtime code generation to pre-compile a join class before use. This would allow a number of optimizations which are not possible in the current implementation due to its generic nature.
Warning
Please remember that the current implementation is incomplete and has only been tested very superficially.
The future
There are a few features I would like to add to this library. The main one is probably to add a way of including the message arguments in the pattern:
public async One (int i) { ... } public async Two (int j) { ... } (one & two).Where((int i int,j) => i > j).Do((int i, int j) => { Console.WriteLine(“i > j”); }); (one & two).Where((int i int,j) => i <= j).Do((int i, int j) => { Console.WriteLine(“i <= j”); });
Such a feature will make the library much more powerful and expressive, but it is likely to come at a rather high run-time cost.
Otherwise I would like to integrate the library better with the new concurrency features in .Net 4.0 and the upcoming async feature.
In my next post I will look at more examples.