Freigeben über


CCR tips and tricks - part 7

There are many ways you can receive messages on a port. In all examples below I'll be using two implementations of Fibonacci that uses CCR. The methods I call have the following signatures:

  1: public Port<int> Fibonacci(int n)
 2: public IEnumerator<ITask> Fibonacci(int n, Port<int> resultPort)

The assumption is that the first method creates a port, spawns a task using the second method and returns the result port hence calculating the Fibonacci number asynchronously. The second method just calculates the number synchronously and posts the result on the given port.

First out is a very common way that I call "call and receive". Basically you create a little method train using the fact that the method we call spawns a task and returns a result port.

  1: public IEnumerator<ITask> CallAndReceive(
 2:     int n, 
 3:     Port<KeyValuePair<int, int>> resultPort)
 4: {
 5:     yield return Fibonacci(n)
 6:         .Receive(s => resultPort.Post(new KeyValuePair<int, int>(n, s)));
 7: }

The second way I call "spawn and receive". The interesting thing about this one is that if the spawned operation completes before the receive is activated the message lives in the port until the receive is activated. This means that theoretically you have slightly worse performance than if you have the receive already activated when the result is posted.

  1: public IEnumerator<ITask> SpawnAndReceive(
 2:     int n, 
 3:     Port<KeyValuePair<int, int>> resultPort)
 4: {
 5:     var port = new Port<int>();
 6:     Arbiter.Activate(
 7:         dispatcherQueue,
 8:         Arbiter.FromIteratorHandler(() => Fibonacci(n, port)));
 9:     yield return port.Receive(
 10:         s => resultPort.Post(new KeyValuePair<int, int>(n, s)));
 11: }

In the third example called "receive and spawn" I guarantee that the receiver is activated before the task is executed. Hence the theoretical performance penalty given above is eliminated since in this example there will always be a receiver activated when the result is posted. Also note that I here use the Arbiter.Receive method rather than the Port.Receive extension method used above.

  1: public IEnumerator<ITask> ReceiveAndSpawn(
 2:     int n, 
 3:     Port<KeyValuePair<int, int>> resultPort)
 4: {
 5:     var port = new Port<int>();
 6:     Arbiter.Activate(
 7:         dispatcherQueue, 
 8:         Arbiter.Receive(
 9:         false, 
 10:         port, s => resultPort.Post(new KeyValuePair<int, int>(n, s))));
 11:     yield return new IterativeTask(() => Fibonacci(n, port));
 12: }

The forth way I call "spawn and test" uses two features of CCR that are not that well known. especially if you're new to CCR. This example (and the one above) uses "yield return new IterativeTask" which actually executes that task to completion before returning. This is equivalent to using Arbiter.ExecuteToCompletion. The second feature used is the fact that casting a Port (or PortSet) to a given type will remove a message from the port and return its value or the default value if no message of given type is available. This is truly an interesting feature of CCR since it means that the cast operator now has a side effect on the object you're casting from.

  1: public IEnumerator<ITask> SpawnAndTest(
 2:     int n, 
 3:     Port<KeyValuePair<int, int>> resultPort)
 4: {
 5:     var port = new Port<int>();
 6:     yield return new IterativeTask(() => Fibonacci(n, port));
 7:     int result = port;
 8:     resultPort.Post(new KeyValuePair<int, int>(n, result));
 9: }

Line 7 could also be written like this:

  7:     int result = (int)port.Test();

or this:

  7:     int result;
7.5:     port.Test(out result);

Of these four variants the receive and spawn is the most dangerous one to use since it's easy to introduce a race condition if you're not careful. Take a look at the following code where some local variables are introduced to increase (?) readability. Maybe you'd do something like this to see values in the debugger.

  1: public IEnumerator<ITask> ReceiveAndSpawnWithRaceCondition(
 2:     int n, 
 3:     Port<KeyValuePair<int, int>> resultPort)
 4: {
 5:     int result = 0;
 6:     var port = new Port<int>();
 7:     Arbiter.Activate(
 8:         dispatcherQueue, 
 9:         Arbiter.Receive(false, port, s => result = s));
 10:     yield return new IterativeTask(() => Fibonacci(n, port));
 11:     resultPort.Post(new KeyValuePair<int, int>(n, result));
 12: }

This code probably works fine when you step through it in the debugger, but there is a race condition. The problem is that when the Fibonacci method posts the result, the result handler (line 9) is scheduled for execution. You do not know when it's executed. If you're unlucky the method above gets back after completing the iterative task and executes line 11 before the handler on line 9 and hence the wrong result is posted back to the caller.