How D can make concurrent programming a piece of cake
Bartosz MilewskiD Programming Language
• Multi-Core is here to stay• Programmers must use concurrency• (Dead-) Lock Oriented Programming
is BAD• New paradigm badly needed
void Toggle (){ if (x == 0) x = 1; else x = 0;}
• Fetch value of x• Store it in a temporary• Compare with zero• Based on the result, write new value
What if, in the meanwhile, the original x was modified by another thread? Write is based on incorrect assumption!
• Theorem:–Hypothesis: x == 0 (read x, compare to 0)– Conclusion: x = 1 (write 1 into x)
• Problem: Hypothesis invalidated before conclusion reached
• Must re-check the hypothesis before writing!
• Delay checking: Log read values for later
• Must re-check before writing: Log the “intent to write” (speculative writes) for later execution
• Verify hypothesis : Are read values unchanged?
• Reach conclusion: Execute writes from log to memory
• Concurrency issues postponed to the commit phase (read-check and write)
• Commit uses generic code, which can be optimized and tested once for all
• User code simple, less error-prone—code as if there were a single global lock
• Increased concurrency—executes as if every word were separately locked
• No deadlocks!
• Start a transaction: create log• Speculative execution—reads and
writes logged• Commit phase (atomic)– Read-check–Write to memory
• If failure, restart transaction
• Combining atomic operations using locks—almost impossible!
• Atomic (transacted) withdrawalatomic { acc.Withdraw (sum); }
• Atomic deposit—similar• Atomic transfer
atomic { accOne.Withdraw (sum); accTwo.Deposit (sum);}
• Example: Producer/Consumeratomic{ Item * item = pcQueue.Get ();}
Item * Get () atomic // PCQueue method{ if (_queue.Count () == 0) retry; else return _queue.pop_front ();}
• Restart transaction without destroying the log
• Make the read-log globally available• Block until any of the logged read
locations changes• Every commit checks the read-sets of
blocked transactions and unblocks the ones that overlap with its write-set
• Consumer doesn’t have to specify what it’s waiting for
• Producer doesn’t have to signal anybody• Composability: Wait for two items
atomic{ item1 = pcQueue.Get (); item2 = pcQueue.Get ();}
• Transactable (atomic) objects– Visible as opaque handles– Can be opened only inside transaction– Open (for read) returns a const pointer
to the actual object– Open for write clones the object and
returns pointer to the cloneatomic struct Foo { int x; }atomic Foo f (new Foo); // an opaque handleatomic { // start transaction Foo * foo = f.open_write (); ++foo.x;}
• Deep copy of the object• Embedded atomic handles are copied
but not the objects they refer to• Transactable data structures build
from small atomic objects (tree from nodes)
• Value-semantic objects (e.g. structs) cloned by copy construction
• Struct or class marked as “atomic”—all methods (except constructor) “atomic”
• Open and open_write can be called only inside a transaction—i.e. from inside: – Atomic block– Atomic function/method
• Atomic function/method may only be called from inside a transaction
struct Slist // not atomic{
this () {// Insert sentinelsSNode * last = new SNode (Infin, null);_head = new SNode (MinusInfin, last);
}// atomic methodsconst (SNode) * Head () const atomic{
retrun _head.open ();}void Insert (int i) atomic;void Remove (int i) atomic;
private:atomic SNode _head; // atomic handle
}
struct SNode atomic{public:
this (int i, const (SNode) * next) {_val = i; _next = next;
}// atomic methods (by default)int Value () const { return _val; }const (SNode) * Next () const {
return _next.open ();}Snode * SetNext (const (SNode) * next) {
SNode * self = this.open_write ()self._next = next;return self;
}private:
int _val;atomic Snode _next;
}
atomic { myList.Insert (x); } // transactioned
void Insert (int i) atomic{
const (SNode) * prev = Head (); // sentinelconst (SNode) * cur = prev.Next ();while (cur._val < i){
prev = cur;cur = prev.Next ();
}assert (cur != 0); // at worst high sentinelSNode * newNode = new SNode (i, cur);(void) prev.SetNext (newNode);
}
• Versioning and Locking– Global Version Clock (always even)– Version numbers always increase– (Hidden) version/lock word per atomic
object (lock is the lowest bit)• Consistent Snapshot maintenance– Version checks when opening an object– Read-check during commit
• Transaction starts by reading Version Clock into the transaction’s read-version variable
• Open object– Check the object lock (bit). If taken,
abort– Check object version number. If it’s
greater than read-version abort
• Every open is recorded in read-log– Pointer to original object (from which its
version lock can be retrieved)• Every open_write is recorded in read-
log and write_log– Pointer to original object– Pointer to clone
• Okay to call open_write after open (read)
• Lock all objects recorded in the write-log– Bounded spinlock on each version lock
• Increment global Version Clock—store as transaction’s write-version
• Sequence Point (if transaction commits, that’s when it “happened”)
• Read-check– Re-check object version numbers against
read-version
• For each location in the write-log– Swap the clone in place of original– Stamp it with write-version–Unlock
• We have C implementation (Brad Roberts’ port of GPL’d Dice, Shalev, and Shalit)
• Write D implementation• Modify type system
• Dave Dice, Ori Shalev, and Nir Shavit. Transactional Locking II
• Tim Harris, Simon Marlow, Simon Peyton Jones, and Maurice Herlihy. Composable Memory Transactions. ACM Conference on Principles and Practice of Parallel Programming 2005 (PPoPP'05). 2005.