400 likes | 593 Views
Polyphonic C#. Nick Benton Luca Cardelli C é dric Fournet Microsoft Research. Asynchrony is where its at. Distribution => concurrency + latency => asynchrony => more concurrency Message-passing, event-based programming, dataflow models
E N D
Polyphonic C# Nick Benton Luca Cardelli Cédric Fournet Microsoft Research
Asynchrony is where its at • Distribution => concurrency + latency => asynchrony => more concurrency • Message-passing, event-based programming, dataflow models • For programming languages, coordination (orchestration) languages & frameworks, workflow
Language support for concurrency • Make invariants and intentions more apparent (part of the interface) • Good software engineering • Allows the compiler much more freedom to choose different implementations • Also helps other tools
.NET today • Java-style “monitors” • OS shared memory primitives • Clunky delegate-based asynchronous calling model • Hard to understand, use and get right • Different models at different scales • Support for asynchrony all on the caller side – little help building code to handle messages (must be thread-safe, reactive, and deadlock-free)
Polyphonic C# • An extension of the C# language with new concurrency constructs • Based on the join calculus • A foundational process calculus like the p-calculus but better suited to asynchronous, distributed systems • A single model which works both for • local concurrency (multiple threads on a single machine) • distributed concurrency (asynchronous messaging over LAN or WAN) • It is different • But it’s also simple – if Mort can do any kind of concurrency, he can do this
In one slide: • Objects have both synchronous and asynchronous methods. • Values are passed by ordinary method calls: • If the method is synchronous, the caller blocksuntil the method returns some result (as usual). • If the method is async, the call completes at once and returns void. • A class defines a collection of chords (synchronization patterns), which define what happens once a particular set of methods have been invoked. One method may appear in several chords. • When pending method calls match a pattern, its body runs. • If there is no match, the invocations are queued up. • If there are several matches, an unspecified pattern is selected. • If a pattern containing only async methods fires, the body runs in a new thread.
A simple buffer class Buffer { String get() & async put(String s) { return s; } }
A simple buffer class Buffer { String get() & async put(String s) { return s; } } • An ordinary (synchronous) method with no arguments, returning a string
A simple buffer class Buffer { String get() & async put(String s) { return s; } } • An ordinary (synchronous) method with no arguments, returning a string • An asynchronous method (hence returning no result), with a string argument
A simple buffer class Buffer { String get() & async put(String s) { return s; } } • An ordinary (synchronous) method with no arguments, returning a string • An asynchronous method (hence returning no result), with a string argument • Joined together in a chord
A simple buffer class Buffer { String get() & async put(String s) { return s; } } • Calls to put() return immediately (but are internally queued if there’s no waiting get()). • Calls to get() block until/unless there’s a matching put() • When there’s a match the body runs, returning the argument of the put() to the caller of get(). • Exactly which pairs of calls are matched up is unspecified.
A simple buffer class Buffer { String get() & async put(String s) { return s; } } • Does example this involve spawning any threads? • No. Though the calls will usually come from different pre-existing threads. • So is it thread-safe? You don’t seem to have locked anything… • Yes. The chord compiles into code which uses locks. (And that doesn’t mean everything is synchronized on the object.) • Which method gets the returned result? • The synchronous one. And there can be at most one of those in a chord.
Reader/Writer …using threads and mutexes in Modula 3 An introduction to programming with threads. Andrew D. Birrell, January 1989.
Reader/Writer in five chords public class ReaderWriter { public void Exclusive() & async Idle() {} public void ReleaseExclusive() { Idle(); } public void Shared() & async Idle() { S(1); } public void Shared() & async S(int n) { S(n+1); } public void ReleaseShared() & async S(int n) { if (n == 1) Idle(); else S(n-1); } public ReaderWriter() { Idle(); } } A single private message represents the state: none Idle()S(1) S(2) S(3) …
Asynchronous requests and responses • Service exposes an async method which takes parameters and somewhere to put the result: • a buffer, or a channel, or • a delegate public delegate async IntCB(int v); public class Service { public async request(String arg, IntCB callback) { int result; // do something interesting… callback(result); } }
Asynchronous requests and responses - Join class Join2 { void wait(out int i, out int j) & async first(int r1) & async second(int r2) { i = r1; j = r2; return; } } // client code: int i,j; Join2 x = new Join2(); service1.request(arg1, new IntCB(x.first)); service2.request(arg2, new IntCB(x.second)); // do something useful // now wait until both results have come back x.wait(out i,out j); // do something with i and j
Asynchronous requests and responses - Select class Select { int wait() & async reply(int r) { return r; } } // client code: int i; Select x = new Select(); service1.request(arg1, new IntCB(x.reply)); service2.request(arg2, new IntCB(x.reply)); // do something useful // now wait until one result has come back i = x.wait(); // do something with i
Active Objects public abstract class ActiveObject : MarshalByRefObject { protected bool done; abstract protected void processmessage(); public ActiveObject () { done = false; mainloop(); } async mainloop() { while (!done) { processmessage(); } } }
…continued class Stock : ActiveObject { override protected void processmessage() & public async bid(BidOffer thebid) { // process bid messages } override protected void processmessage() & public async register(Client who) { // process registration requests } … }
Extending C# with chords • Classes can declare methods using generalized chord-declarations instead of method-declarations. • Interesting well-formedness conditions: • At most one header can have a return type (i.e. be synchronous). • The inheritance restriction. • “ref” and “out” parameters cannot appear in async headers. chord-declaration ::= method-header [ & method-header ]* body method-header ::= attributes modifiers [return-type | async] name (parms)
Why only one synchronous method in a chord? • JoCaml allows multiple synchronous methods to be joined, as in the following rendezvous • But in which thread does the body run? In C#, thread identity is “very” observable, since threads are the holders of particular re-entrant locks. So we rule this out in the interests of keeping & commutative. (Of course, it’s still easy to code up an asymmetric rendezvous in Polyphonic C#.) int f(int x) & int g(int y) { return y to f; return x to g; }
The problem with inheritance class C { virtual void f() & virtual async g() {…} virtual void f() & virtual async h() {…} } class D : C { override async g() { …} } • We’ve “half” overridden f • Too easy to create deadlock or async leakage void m(C x) { x.g(); x.f();} … m(new D());
The inheritance restriction • Two methods are co-declared if they appear together in a chord declaration. Whenever a method is overridden,every co-declared method must also be overridden. • Hence, the compiler rejects patterns such as public virtual void f() & private async g() {…} • In general, inheritance and concurrency do not mix well.Our restriction is simple; it could be made less restrictive.
Types etc. • async is a subtype of void • Allow covariant return types on those two: • An async method may override a void one • A void delegate may be created from an async method • An async method may implement a void method in an interface • async methods are given the [OneWay] attribute, so remote calls are non-blocking
Implementation • Translate Polyphonic C# -> C# • Built on Proebsting & Hanson’s lcsc • Introduce queues for pending calls (holding blocked threads for sync methods, arguments for asyncs) • Generated code (using brief lock to protect queue state) looks for matches and then either • Enqueues args (async no match) • Enqueues thread and blocks (sync no match) • Dequeues other args and continues (sync match) • Wakes up blocked thread (async match with sync) • Spawns new thread (async match all async) • Efficient – bitmasks to look for matches, no PulseAlls,…
Samples • animated dining philosophers • web service combinators (Cardelli & Davies) • adaptive scheduler (cf. Larus & Parkes), • accessing web services (Terraserver), • active objects and remoting (stock trader)
Current and future work • Direct syntactic support for timeouts • Limited pattern-matching on message contents • Adding joinable transactions with explicit compensations • Behavioural types?
Conclusions • A clean, simple, new model for asynchronous concurrency in C# • Declarative, local synchronization • Model good for both local and distributed settings • Efficiently compiled to queues and automata • Easier to express and enforce concurrency invariants • Compatible with existing constructs, though they constrain our design somewhat • Minimalist design – pieces to build whatever complex synchronization behaviours you need • Solid foundations • Works well in practice http://research.microsoft.com/~nick/polyphony/
Fairer reader/writer lock class ReaderWriterFair { ReaderWriter() { idle(); } private int n = 0; // protected by s() or t() public void Shared() & async idle() { n=1; s(); } public void Shared() & async s() { n++; s(); } public void ReleaseShared() & async s() { if (--n == 0) idle(); else s(); } public void Exclusive() & async idle() {} public void ReleaseExclusive() { idle(); } public void ReleaseShared() & async t() { if (--n == 0) idleExclusive(); else t(); } public void Exclusive() & async s() { t(); wait(); } void wait() & async idleExclusive() {} }
Predictable Demo: Dining Philosophers waiting to eat eating waiting to eat thinking eating
Code extract classRoom { publicRoom (int size) {hasspaces(size); } public voidenter() & private async hasspaces(int n) { if (n > 1)hasspaces(n-1); elseisfull(); } public voidleave() & private async hasspaces(int n) { hasspaces(n+1); } public void leave() & private async isfull() {hasspaces(1);} }
A Better Syntax? class ReaderWriter { private async Idle(); // declare asyncs private async S(int n); public void Exclusive() when Idle() {} public void ReleaseExclusive() { Idle(); } public void Shared() // syncs can have sequence of when Idle() {S(1);} // “when” patterns involving | when S(int n) {S(n+1);} // asyncs public void ReleaseShared() when S(int n) { if (n==1) Idle(); else S(n-1); } } Could even allow when patterns as general statements, though this seems in dubious taste…
Santa Claus problem (Trono, Ben-Ari) • Santa sleeps until awakened by either all 9 reindeer or by 3 of the 10 elves. • If woken by reindeer he harnesses them all up, they deliver presents together, he unharnesses them, they go off on holiday and he goes back to sleep. • If woken by a group of elves, he shows them into his office, consults with them on toy R&D then shows them all out and goes back to sleep. • Surprisingly tricky to avoid bugs such as Santa going off without the reindeer, queue-jumping elves • Trono posed problem and gave incorrect solution using semaphores • Ben-Ari gave a non-trivial solution using Ada primitives and ugly, inefficient and unsatisfactory solution in Java
public class nway { public async produce(int n) & public void consume() { if (n==1) { alldone(); } else { produce(n-1); } } public void waitforalldone() & async alldone() { return; } }
class santa { static nway harness = new nway(); static nway unharness = new nway(); static nway roomin = new nway(); static nway roomout = new nway(); static void santalife() { while (true) { waittobewoken(); // get back here when dealt with elves or reindeer } } static void waittobewoken() & static async elvesready() { roomin.produce(3); roomin.waitforalldone(); elveswaiting(0); // all elves in the room, consult roomout.produce(3); roomout.waitforalldone(); // all elves shown out, go back to bed } static void waittobewoken() & static async reindeerready() { // similar to elvesready chord }
static async elflife(int elfid) { while (true) { // work elfqueue(); // wait to join group of 3 roomin.consume(); // wait to be shown in // consult with santa roomout.consume(); // wait to be shown out again } } static void elfqueue() & static async elveswaiting(int e) { if (e==2) { elvesready(); // last elf in a group so wake santa } else { elveswaiting(e+1); } }
Pattern Matching async Sell(string item, Client seller) & async Buy (string item, Client buyer) { ... // match them up } Very useful, but hard to compile efficiently
Ordered processing, currently class SequenceProcessor : ActiveObject { private SortedList pending = new SortedList(); private int next = 0; public async Message(int stamp, string contents) & override protected void ProcessMessage() { if (stamp == next) { DealWith(contents); while (pending.ContainsKey(++next)) { DealWith((string)pending[next]); pending.Remove(next); } } else { pending.Add(stamp,contents); } } ... }
with matching class SequenceProcessor : ActiveObject { public async Message(int stamp, string contents) & override protected void ProcessMessage() & async waitingfor(int stamp) { DealWith(contents); waitingfor(stamp++); } SequenceProcessor() { waitingfor(0); } ... }