Trying to get the most out of the current 'shared' system

Sönke Ludwig sludwig at outerproduct.org
Mon Nov 12 03:41:00 PST 2012


Am 11.11.2012 19:46, schrieb Alex Rønne Petersen:
> Something needs to be done about shared. I don't know what, but the
> current situation is -- and I'm really not exaggerating here --
> laughable. I think we either need to just make it perfectly clear that
> shared is for documentation purposes and nothing else, or, figure out an
> alternative system to shared, because I don't see shared actually being
> useful for real world work no matter what we do with it.
> 

After reading Walter's comment, it suddenly seemed obvious that we are
currently using 'shared' the wrong way. Shared is just not meant to be
used on objects at all (or only in some special cases like
synchronization primitives). I just experimented a bit with a statically
checked library based solution and a nice way to use shared is to only
use it for disabling access to non-shared members while its monitor is
not locked. A ScopedLock proxy and a lock() function can be used for this:

---
class MyClass {
	void method();
}

void main()
{
	auto inst = new shared(MyClass);
	//inst.method(); // forbidden
	
	{
		ScopedLock!MyClass l = lock(inst);
		l.method(); // now allowed as long as 'l' is in scope
	}

	// can also be called like this:
	inst.lock().method();
}
---

ScopedLock is non-copyable and handles the dirty details of locking and
casting away 'shared' when its safe to do so. No tagging of the class
with 'synchronized' or 'shared' needs to be done and everything works
nicely without casts.

This comes with a restriction, though. Doing all this is only safe as
long as the instance is known to not contain any unisolated aliasing*.
So use would be restricted to types that contain only immutable or
unique/isolated references.

So I also implemented an Isolated!(T) type that is recognized by
ScopedLock, as well as functions such as spawn(). The resulting usage
can be seen in the example at the bottom.

It doesn't provide all the flexibility that a built-in 'isolated' type
would do, but the possible use cases at least look interesting. There
are still some details to be worked out, such as writing a spawn()
function that correctly moves Isolated!() parameters instead of copying
or the forward reference error mentioned in the example.

I'll now try and see if some of my earlier multi-threading designs fit
into this system.

---
import std.stdio;
import std.typecons;
import std.traits;
import stdx.typecons;

class SomeClass {

}

class Test {
	private {
		string m_test1 = "test 1";
		Isolated!SomeClass m_isolatedReference;
		// currently causes a size forward reference error:
		//Isolated!Test m_next;
	}

	this()
	{
		//m_next = ...;
	}

	void test1() const { writefln(m_test1); }
	void test2() const { writefln("test 2"); }
}

void main()
{
	writefln("Shared locking");
	// create a shared instance of Test - no members will
	// be accessible
	auto t = new shared(Test);
	{
		// temporarily lock t to make all non-shared members
		// safely available
		// lock() words only for objects with no unisolated
		// aliasing.
		ScopedLock!Test l = lock(t);
		l.test1();
		l.test2();
	}

	// passing a shared object to a different thread works as usual
	writefln("Shared spawn");
	spawn(&myThreadFunc1, t);

	// create an isolated instance of Test
	// currently, Test may not contain unisolated aliasing, but
	// this requirement may get lifted,
	// as long as only pure methods are called
	Isolated!Test u = makeIsolated!Test();

	// move ownership to a different function and recover
	writefln("Moving unique");
	Isolated!Test v = myThreadFunc2(u.move());

	// moving to a different thread also works
	writefln("Moving unique spawn");
	spawn(&myThreadFunc2, v.move());

	// another possibility is to convert to immutable
	auto w = makeIsolated!Test();
	writefln("Convert to immutable spawn");
	spawn(&myThreadFunc3, w.freeze());

	// or just loose the isolation and act on the base type
	writefln("Convert to mutable");
	auto x = makeIsolated!Test();
	Test xm = x.extract();
	xm.test1();
	xm.test2();
}

void myThreadFunc1(shared(Test) t)
{
	// call non-shared method on shared object
	t.lock().test1();
	t.lock().test2();
}

Isolated!Test myThreadFunc2(Isolated!Test t)
{
	// call methods as usual on an isolated object
	t.test1();
	t.test2();
	return t.move();
}

void myThreadFunc3(immutable(Test) t)
{
	t.test1();
	t.test2();
}


// fake spawn function just to test the type constraints
void spawn(R, ARGS...)(R function(ARGS) func, ARGS args)
{
	foreach( i, T; ARGS )
		static assert(!hasUnisolatedAliasing!T ||
			!hasUnsharedAliasing!T,
			"Parameter "~to!string(i)~" of type"
			~T.stringof~" has unshared or unisolated
			 aliasing. Cannot safely be passed to a
			different thread.");
	
	// TODO: do this in a different thread...
	// TODO: don't cheat with the 1-parameter move detection
	static if(__traits(compiles, func(args[0])) ) func(args);
	else func(args[0].move());
}
---


* shared aliasing would also be OK, but this is not yet handled by the
implementation.


More information about the Digitalmars-d mailing list