Mechanisms for concurrency so far: * Data parallel * Threads (+ mutexes and condition variables) - Partial orders - Composability * Transactional memory solves partial orders and composability - Only takes care of serialization (nothing about condition variables) - No expression of concurrency By default threads with locks or atomic blocks (TM) start with the notion that all interleavings are possible and we add code to restrict interleavings. Want to bias to correctness first, not concurrency first. Today: Automatic mutual exclusion (AME) It's written in an event-based style. It's a set of concurrent "serializable" blocks + mechanisms to create new blocks, control their order of execution, and partition large blocks into smaller ones + transactions. It also has a way to incorporate nonabortable actions. There will be papers on this stuff posted. Basic event system: There's an event loop that waits for external events to happen. When an event happens, an event handler is called to handle that event. GUI systems and servers are often written this way. This isn't very exciting so far. Events provide I/O level concurrency. Typically, events are short-lived and non-blocking. This is important, because events are scheduled non-premptively. (they run until completion) Two techniques for "nice" events that don't bog down the system: 1) Decomposition: break event into pre-event and a post-event The pre-event registers the post event and then returns, and the post-event completes the work. This way, the event loop can schedule something else in the middle. 2) Stack ripping Almost always use asynchronous I/O, which is: * Decompose open(filename) into start_open(filename) and open_finished(fd). * In the synchronous model, information is held on the stack, and in the async model information is passed as event arguments. * This means that all events have to be broken into "before open" and "after open". This is stack ripping, and it's the crux of the problem. The moral of the story: if you don't need a lot of performance, events can be simpler, but threads are more general. The essential notion we're going to try to capture is to allow events to overlap if they are "serializable". Two events, A and B, can be run concurrently if the outcome of the particular concurrent execution under consideration is indistinguishable from running either A before B or B before A. In AME: Start with 1 event: main(...) { } create new asynchronous events: async foo(...); This guarantees that foo must be serialized *after* the caller. If a single caller issues multiple async method calls, the called methods can be run in any serializable order (but all after main). We want to be able to constrain this for 3 read/compute/write events. In an async method: BlockUntil(predicate) - if predicate true --> NOP - if predicate false --> abort method and retry later. This is "obviously" safe because of serializability. "Safe" means that once BlockUntil is called, the value will not "flip" until this event ends. Consider some other event C that happens concurrently with the blocking event. Because of serializability, this appears as though C happened either strictly before or strictly after this event. In practice, events are transactions and they get rolled back and serialized if there's a problem in ze middle. Potential deviation from understandability follows. bool done = false; parallel_for(R,B,P) { if(p->is_divisible() && r->should_divide()) { Range * rhs = new Range(r, split()); async parallel_for(r,b,p); async parallel_for(rhs, b, p); BlockUntil(done); } else { b(r); done = true; } } Damn, this never makes progress because the async calls happen AFTER the caller. We're screwed. There are two mechanisms to handle this. 1) In the event style, parallel_for would take a function called post_for: parallel_for(r,b,p, post_for) { refcount++; async rhs //( .... ref --) refcount++ async lhs //(... ref --) async post_for // (BlockUntil refcnt == 0) } continuation-passing style is really common in event-based programming. 2) Mechanism: split transactions. It's called yield. Yield ends the current transaction and begins a new one. (it works like you'd think and sort of like it does in coroutines.) Invariants on shared state must be true for a yield call. Functions that yield must be tagged as such: bar(...) yields; if you call bar synchronously, you must inform the compiler that you know it could cause the calling transaction to end: int baz = bar(...) yielding; WARNING to reader: I'm leaving out a lot of marked up chalkboard here because I'm feeling lazy.