How D can make concurrent programming a piece of cake Bartosz Milewski D Programming Language.

Download Report

Transcript How D can make concurrent programming a piece of cake Bartosz Milewski D Programming Language.

How D can make concurrent
programming a piece of cake
Bartosz Milewski
D 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) withdrawal
atomic { acc.Withdraw (sum); }
• Atomic deposit—similar
• Atomic transfer
atomic {
accOne.Withdraw (sum);
accTwo.Deposit (sum);
}
• Example: Producer/Consumer
atomic
{
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 clone
atomic struct Foo { int x; }
atomic Foo f (new Foo); // an opaque handle
atomic { // 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 sentinels
SNode * last = new SNode (Infin, null);
_head = new SNode (MinusInfin, last);
}
// atomic methods
const (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
{
const
const
while
{
(int i) atomic
(SNode) * prev = Head (); // sentinel
(SNode) * cur = prev.Next ();
(cur._val < i)
prev = cur;
cur = prev.Next ();
}
assert (cur != 0); // at worst high sentinel
SNode * 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.