Thread overview
A working way to improve the "shared" situation
Nov 15, 2012
Sönke Ludwig
Nov 15, 2012
Sönke Ludwig
Nov 21, 2012
deadalnix
Nov 21, 2012
Sönke Ludwig
November 15, 2012
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
November 15, 2012
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.
November 21, 2012
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.
November 21, 2012
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 :-/