shared adventures in the realm of thread-safety.

Jeremie Pelletier jeremiep at gmail.com
Fri Sep 11 14:02:06 PDT 2009


I decided to play once again with shared and see what 2.032 is capable of. Turns out a lot of the previous issues I was having last time are gone, however, there are still a few things left which prevent me from rewriting my code.

The first issue that jumped to my face straight away was how 'shared const' methods are not callable from 'shared' objects.

shared class Foo {
	void bar() const;
}
auto foo = new Foo; // foo is of type shared(Foo)
foo.bar; //  Error: function Foo.bar () shared const is not callable using argument types () shared

Considering how 'const' methods can be called from mutable objects, this looks like either a bug or a really awkward feature to me. Sending a shared(Foo) to a method expecting a shared(const(Foo)) also triggers a similar error from the compiler.

The other issue may be an intended feature, but it doesn't sound practical to me. Marking a method as shared assumes all used properties in the method's scope are also shared. Here is an example to illustrate my point:

class SimpleReader {
   this(LocalFile file) { _stream = new FileInputStream(file); }
   ...
private:
    synchronized void read(ubyte[] buf, long offset) {
        _stream.seek(offset);
        _stream.read(buf);
    }
    FileInputStream _stream;
}

The FileInputStream here is a generic blocking binary stream which is not thread-safe by design. The reader is a composite class where every instance has its own unique stream instance and use it to implement asynchronous reads over the file format it abstracts, which in my case is a specialized read-only archive using a lot of random accesses from different threads.

This is where the issue shows its ugly head. The 'synchronized' keyword tags the read method as shared, which in itself is quite neat, what is annoying however is that it also changes the type of _stream in the method's scope to shared(FileInputStream) and therefore triggers compiler errors because _stream.seek and _stream.read are not shared:

Error: function FileInputStream.read (ubyte[]) is not callable using argument types (ubyte[]) shared

While it may be an attempt to keep shared usage safe, it isn't very practical. The stream object here is not shared because it is not thread-safe. While it may be used by different threads, it is unique to the reader's context and its accesses are synchronized by the reader, the stream should therefore be completely oblivious to the fact it is being used by different threads.

Maybe this could be the time to implement an unique qualifier; this is a context where having _stream be of type unique(FileInputStream) would solve the problem and allow further compiler optimizations. I don't know if it can be done with templates, and without any overhead whatsoever. I know I would much rather see unique(Foo) than Unique!Foo, and it would allow the use of 'is(foo : unique)'.

Furthermore, tagging a method with shared does not make it thread-safe, it may however use synchronized within its scope to protect its shared or unique data. This may be confusing when calling shared methods vs calling synchronized methods; one may think the shared one is not thread-safe and optionally synchronize the call, resulting in another monitor being used for nothing, or no monitor being used at all:

class Foo {
    shared void bar() {
        // Do stuff with local or immutable data
        synchronized(this) { /* do stuff with shared data */ }
    }
    shared void bar2() {
        // Do stuff on shared data
    }
}

Someone seeing only the prototype of Foo.bar may assume the method is not thread-safe and call it as 'synchronized(foo) foo.bar()'. Just like they could see the prototype of bar2 and assume it is thread-safe, calling it as 'foo.bar2()'.

What could be a good design against this sort of misleading behavior?

Phew, that's about enough issues and questions for now :)



More information about the Digitalmars-d mailing list