www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - A working way to improve the "shared" situation

reply =?ISO-8859-15?Q?S=F6nke_Ludwig?= <sludwig outerproduct.org> writes:
After working a bit more on it (accompanied by a bad flu with 40 °C fever, so
hopefully it's not all
wrong in reality), I got a library approach that allows to use shared objects
in a (statically
checked) safe and comfortable way. As a bonus, it also introduces an
isolated/unique type that can
be safely moved between threads and converts safely to immutable (and mutable).

It would be really nice to get a discussion going to see if this or something
similar should be
included in Phobos and which (if any) language extensions, that could help (or
replace) such an
approach, are realistic to get implemented in the short term (e.g. Walter
suggested
__unique(expression) to statically verify that an expression yields a value
with no mutable aliasing
to the outside).

But first a rough description of the proposed system - there are three basic
ingredients:

 - ScopedRef!T:

   wraps a type allowing only operations that are guaranteed to not leak any
references in or out.
   This type is non-copyable but allows reference-like access to a value. In
contrast to 'scope' it
   works recursively and also works on return values in addition to function
parameters.

 - Isolated!T:

   Statically ensures that any contained aliasing is either immutable or is
only reachable through
   the Isolated!T itself (*strong isolation*). This allows safe passing between
threads and safe
   conversion to immutable. A less strict mode also allows shared aliasing
(*weak isolation*).
   Implicit conversion to immutable is not possible for weakly isolated values,
but they can still
   safely be moved between threads and accessed without locking or similar
means. As such they
   provide a natural bridge between the shared and the thread local world.
Isolated!T is
   non-copyable, but can be move()d between variables.

 - ScopedLock!T:

   Provides scoped access to shared objects. It will lock the object's mutex
and provide access to
   its non-shared methods and fields. A convenience function lock() is used to
construct a
   ScopedLock!T, which is also non-copyable. The type T must be weakly
isolated, because otherwise
   it cannot be guaranteed that there are no shared references that are not
also marked with
   'shared'.

The operations done on either of these three wrappers are forced to be (weakly)
pure and may not
have parameters or return types that could leak references (neither /to/ nor
/from/ the outside).

It solves a number of common usage patterns, not only removing the need for
casts, but also
statically verifying the correctness of the code. The following example shows
it in action. Apart
from the pure annotations ('pure:' would help), nothing else is necessary.

---
import stdx.typecons;

class Item {
	private double m_value;
	this(double value) pure { m_value = value; }
	 property double value() const pure { return m_value; }
}

class Manager {
	private {
		string m_name;
		Isolated!(Item) m_ownedItem;
		Isolated!(shared(Item)[]) m_items;
	}

	this(string name) pure
	{
		m_name = name;
		auto itm = makeIsolated!Item(3.5);
		// _move_ itm to m_ownedItem
		m_ownedItem = itm;
		// itm is now empty
	}

	void addItem(shared(Item) item) pure { m_items ~= item; }

	double getTotalValue()
	const pure {
		double sum = 0;

		// lock() is required to access shared objects
		foreach( ref itm; m_items ) sum += itm.lock().value;

		// owned objects can be accessed without locking
		sum += m_ownedItem.value;

		return sum;
	}
}

void main()
{
	import std.stdio;

	auto man = new shared(Manager)("My manager");
	{ // doing multiple method calls during a single lock is no problem
		auto l = man.lock();
		l.addItem(new shared(Item)(1.5));
		l.addItem(new shared(Item)(0.5));
	}

	writefln("Total value: %s", man.lock().getTotalValue());
}
---

This all works quite well and is able to come close to what the C# system that
I linked some days
ago (*) is able to do. Notably, ScopedRef!T allows to directly modify isolated
objects without
having to implement the recovery rules that the paper mentions. It cannot
capture all those cases,
but is good enough in most cases. Note that there are a lot of small details
that I left out, but
just to hopefully better get the general idea across.

There are still some open points where I think small language changes are
needed to make this
bullet-proof:

 - It would be nice to be able to disallow 'auto var =
somethingThatReturnsScopedRef();'. Copying
   can nicely be disabled using ' disable this(this)', but initializing a
variable can't. This
   opens up a possible whole:

   ---
   Isolated!MyType myvalue = ...;
   ScopedRef!int fieldref = myvalue.someIntField;
   send(someThread, myvalue); // isolated values can be safely moved to
different threads
   fieldref++; // but wait, we can still screw it up!
   ---

 - opApply() seemingly cannot be used in a pure context in a meaningful way.
Making it pure means
   that also the delegate that it takes must be pure. But a pure foreach body
basically means that
   the whole loop has no effect (okay, it could still modify the iterated
elements). The workaround
   I did was to let the pure opApply take an impure delegate that is casted to
pure upon calling it.

 - Locking is technically an impure operation, but from a high level view it
has no visible effect.
   To make the whole system really usable, it is required that lock() can be
used from a pure
   context. As a workaround I declared _d_monitorenter/exit as pure (these are
used for locking the
   object's mutex).


github project containing the D implementation:

https://github.com/s-ludwig/d-isolated-test

A little documentation:

http://vibed.org/temp/d-isolated-test/stdx/typecons/lock.html
http://vibed.org/temp/d-isolated-test/stdx/typecons/makeIsolated.html
http://vibed.org/temp/d-isolated-test/stdx/typecons/makeIsolatedArray.html


(*) Microsoft paper about the C# type system extension, from which some of the
ideas originate:

http://research.microsoft.com/pubs/170528/msr-tr-2012-79.pdf
Nov 15 2012
parent reply =?ISO-8859-15?Q?S=F6nke_Ludwig?= <sludwig outerproduct.org> writes:
Since the "Something needs to happen with shared" thread is currently split up
into a low level
discussion (atomic operations, memory barriers etc.) and a high level one
(classes, mutexes), it
probably makes sense to explicitly state that this proposal here applies more
to the latter.
Nov 15 2012
parent reply deadalnix <deadalnix gmail.com> writes:
Le 15/11/2012 08:56, Sönke Ludwig a écrit :
 Since the "Something needs to happen with shared" thread is currently split up
into a low level
 discussion (atomic operations, memory barriers etc.) and a high level one
(classes, mutexes), it
 probably makes sense to explicitly state that this proposal here applies more
to the latter.

One problem remains : even with isolated like you propose, some memory barriers are required (acquire/release semantic is required). So it does seems pretty hard to get away with it only using lib.
Nov 20 2012
parent =?ISO-8859-15?Q?S=F6nke_Ludwig?= <sludwig outerproduct.org> writes:
Am 21.11.2012 07:11, schrieb deadalnix:
 Le 15/11/2012 08:56, Sönke Ludwig a écrit :
 Since the "Something needs to happen with shared" thread is currently split up
into a low level
 discussion (atomic operations, memory barriers etc.) and a high level one
(classes, mutexes), it
 probably makes sense to explicitly state that this proposal here applies more
to the latter.

One problem remains : even with isolated like you propose, some memory barriers are required (acquire/release semantic is required). So it does seems pretty hard to get away with it only using lib.

Right, it only solves the lock-based part of the shared world, where the mutex ensures proper acquire/release. As for the rest, after all that discussion I'm still not convinced that letting the compiler insert something automatically makes sense - it may impair performance, just avoids a small part of the pool of potential bugs and only really works for small types. Just disallowing all usual operations on shared values*, making sure that access follows volatile semantics and providing the appropriate atomic operations/barriers as functions/intrinsics looks like a sufficient solution to me, given how seldom this is needed and how critical the details are - and that seems about to be the status quo. But since I only have lock-free stuff in a few places and I'm sure that those can be kept working with whatever the final system will look like, I'm not so strongly opinionated there. It's different with lock based stuff, this is currently just an unusable mess and a strict/safe solution that is later relaxed is probably a better plan than the opposite (e.g. if synchronized classes would automatically provide access to their non-shared fields from inside of their methods). * Just noticed that this is another case for Rebindable!T as you usually do not want to disallow non-atomic access to a shared class reference, but sometimes you need to. A nice little syntax for the head-X and X distinction would be so nice :-/
Nov 20 2012