Friday, February 02, 2007

Concurrency and C-Omega

The old name was Polyphonic C#. Both sound cool, but C-Omega is harder to type, but looks way cooler when you use the real symbol: Cw (that made blogger mad putting that in, and Firefox remains unimpressed with my font tag, so follow the link or read this in IE to see the symbol). Apparently it is not CW which would have looked cooler.
Anyway, I totally stumbled across it while trolling the Microsoft Research site. Concurrency stuff is becoming a lot more interesting to me -- from the geeky theory level just because of the increasing abundance of multi-core processors, but also because I'm running into these same issues at work. Cw dovetails a bunch of LINQ stuff into it too, but the concurrency stuff is what has me excited.
We implemented a pretty naive attempt at SOA here, and ran into the classic performance bottleneck. The extra network hop involved in talking to a web service caused all the same old kinds of performance issues that we used to run into with DCOM, just in new ways. Consuming too many discreet services means you're making too many network roundtrips, so the server spends most of its time waiting around for responses from other servers. And for us, it does all that waiting in a serial fashion. Ouch.
This is of course why all of .Net's auto-generated web service proxies have asynchronous equivalents to every call, but synchronizing all the callbacks in a way that isn't both confusing and brittle is pretty hard. And then there's the whole issue of defending your code from slash-and-burn programmers that just don't get it.

But check out their simple example (I made it simpler):

1 public class Buffer

2 {

3 public async Put(string s);

4

5 public string Get() & Put(string s)

6 {

7 return s;

8 }

9 }




The idea is that any call to Put just hands off the parameter and returns immediately (async implies a void return type). A call to Get is synchronous like a regular method call, but has to be paired with a call to Put. So if Put had already been called, the value s is waiting to be returned. If Put has not been called, the call to Get blocks until some other thread calls Put. Multiple calls to Put will stack up waiting to be consumed.
This example hurt my brain to look at the first time:



1 public class ReaderWriter

2 {

3 private async idle();

4 private async s(int n);

5

6 /// <summary>

7 /// We start out by dropping message to idle()

8 /// </summary>

9 public ReaderWriter()

10 {

11 //

12 idle();

13 }

14

15 /// <summary>

16 /// A call to Exclusive() will block unless or until

17 /// there is an idle() message dropped. It consumes

18 /// the idle() message, so another call to Exclusive()

19 /// will have to wait for someone else to call idle().

20 /// </summary>

21 public void Exclusive() & idle()

22 {

23 //We don't need to actually do anything.

24 //Consuming the idle() message is enough.

25 }

26

27 public void ReleaseExclusive()

28 {

29 //Makes us idle again.

30 idle();

31 }

32

33 /// <summary>

34 /// A call to Shared will block until there is a matching

35 /// message of either idle() or s(int). You only have

36 /// to match on one of the asynchronous methods. So a

37 /// call to Shared() will either match idle() or s(int),

38 /// but not both. (The & syntax makes that a little

39 /// confusing. You might think Shared would wait for

40 /// both idle and s(int).)

41 /// </summary>

42 public void Shared() & idle()

43 {

44 /*

45 * If we were idle, when somebody called Shared,

46 * then we go here, and drop a message that there

47 * is one reader.

48 */

49 s(1);

50 }

51 & s(int n)

52 {

53 /*

54 * If we were already in Shared mode and somebody

55 * called Shared again, then just bump up the counter.

56 */

57 s(n+1);

58 }

59

60 /// <summary>

61 /// This just needs to make the decision to either

62 /// decrement the shared counter or call idle. Either

63 /// way, it will drop another asynchronous message.

64 /// And if you follow the logic, ReleaseShared() will

65 /// always find that there is a waiting s(int) message.

66 /// </summary>

67 public void ReleaseShared() & s(int n)

68 {

69 if (n == 1)

70 idle();

71 else

72 s(n - 1);

73 }

74 }



It's a lot less painful once you grasp on to the fact that an asynchronous call is just dropping off a message. (The contents of the message being the parameters.) The & token groups together method signatures (you can group two or more, but only one can be synchronous). Together the grouped signatures form a chord, and the body will execute when a synchronous and an asynchronous call find a match. I found the concept got simpler as I started thinking about it in a similar way to signature matching for overloaded methods.
It is a little unsettling to see that state is being stored with no variables for you to look at. But there's at least a baseline here for debugging tools to latch onto that would making debugging a multi-threaded app a lot less painful.
And yah, this stuff is hard to wrap your brain around, but concurrency is hard to wrap your brain around. This is a big step into abstracting away the non-essentials.

There are some other equally-impressive things being baked into Cw, go check it out.

0 comments: