www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - shared adventures in the realm of thread-safety.

reply Jeremie Pelletier <jeremiep gmail.com> writes:
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 :)
Sep 11 2009
parent reply Jason House <jason.james.house gmail.com> writes:
I'm glad to see I'm not the only one trying to use shared. I tried to use it
with 2.031 and rapidly hit bug after bug... I submitted several bug reports for
basic functionality, and none of it appeared in the changelog.

http://d.puremagic.com/issues/show_bug.cgi?id=3089
http://d.puremagic.com/issues/show_bug.cgi?id=3090
http://d.puremagic.com/issues/show_bug.cgi?id=3091



Jeremie Pelletier Wrote:

 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 :)

Sep 12 2009
next sibling parent reply Graham St Jack <graham.stjack internode.on.net> writes:
I'm also having the same problems.

As Jeremie said, as soon as you start introducing shared methods (via 
synchronized for example), you rapidly get into trouble that can only be 
overcome by excessive casting.

It may be possible to contain the problem by refactoring multi-threaded 
code so that the shared objects are very small and simple, but even then 
the casting required is too much. This approach might be ok if you could 
define classes as being shared or immutable, and ALL instance of them 
were then implicitly shared or immutable. Also, immutable objects should 
be implicitly shareable.


On Sat, 12 Sep 2009 15:32:05 -0400, Jason House wrote:

 I'm glad to see I'm not the only one trying to use shared. I tried to
 use it with 2.031 and rapidly hit bug after bug... I submitted several
 bug reports for basic functionality, and none of it appeared in the
 changelog.
 
 http://d.puremagic.com/issues/show_bug.cgi?id=3089
 http://d.puremagic.com/issues/show_bug.cgi?id=3090
 http://d.puremagic.com/issues/show_bug.cgi?id=3091
 
 
 
 Jeremie Pelletier Wrote:
 
 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 :)


Sep 12 2009
parent reply Jeremie Pelletier <jeremiep gmail.com> writes:
Graham St Jack Wrote:

 I'm also having the same problems.
 
 As Jeremie said, as soon as you start introducing shared methods (via 
 synchronized for example), you rapidly get into trouble that can only be 
 overcome by excessive casting.
 
 It may be possible to contain the problem by refactoring multi-threaded 
 code so that the shared objects are very small and simple, but even then 
 the casting required is too much. This approach might be ok if you could 
 define classes as being shared or immutable, and ALL instance of them 
 were then implicitly shared or immutable. Also, immutable objects should 
 be implicitly shareable.

I agree, this is also one of my main concerns with shared in its current state. It's an amazing and powerful concept and has the potential to make multi-thread code much easier and safer to write. But all the required casting is killing the safety, and makes it harder to use than it would be not having shared at all. The lack of an unique qualifier certainly doesn't help either. Unique data could only be used for aggregate properties, const/immutable data would also be implicitly unique. This qualifier alone would simplify shared quite a lot, allowing the use of unshared objects in shared contexts safely. The compiler should make the distinction between shared code and shared data and allow both shared and unshared instances to use shared methods, just like both const and mutable instances may call const methods. An error should also be triggered when calling a shared method of a shared object without synchronization, and maybe have a __sync keyword to override this. If a synchronized method is called from a non-shared object, no synchronization takes place. Allow me to illustrate my point with some code: class Foo { int bar() shared { return a; } __sync bar2() { synchronized(this) return a; } synchronized void foo() { a = 1; } int a; } auto foo1 = new shared(Foo)(); auto foo2 = new Foo; foo1.foo(); // ok, synchronized call synchronized(foo1) foo1.foo(); // warning: recursive synchronization foo2.foo(); // ok, unsynchronized call synchronized(foo2) foo2.foo(); // ok synchronized call foo1.bar(); // error, unsynchronized call to bar() shared synchronized(foo1) foo1.bar(); // ok, synchronized call foo2.bar(); // ok, unsynchronized call synchronized(foo1) foo1.bar(); // ok, synchronized call foo1.bar2(); // ok, method handles synchronized synchronized(foo1) foo1.bar2(); // warning, recursive synchronization foo2.bar2(); // ok, method handles synchronized, even on unshared object synchronized(foo2) foo2.bar2(); // warning, recursive synchronization, even on unshared object That's about it, I believe this behavior would allow quite a number of multi-threaded techniques to be easily implemented and documented. It would only be the most natural thing since its quite similar to how const works.
Sep 13 2009
parent reply Jeremie Pelletier <jeremiep gmail.com> writes:
Robert Jacques Wrote:

 On Sun, 13 Sep 2009 15:04:57 -0400, Jeremie Pelletier <jeremiep gmail.com>  
 wrote:
 [snip]
 Unique data could only be used for aggregate properties, const/immutable  
 data would also be implicitly unique. This qualifier alone would  
 simplify shared quite a lot, allowing the use of unshared objects in  
 shared contexts safely.

Neither const nor immutable data can be considered unique. First, any const data may be being mutated by another routine, so it can't be safely accessed without synchronization. Second, unique data is mutable while const/immutable data is not. Third, most implementations of unique allow for deterministic memory reclamation, which isn't possible if the unique data might actually be const/immutable.

Good points, I can only agree with you here. However I still believe immutable data should be able to be used in shared contexts without being 'shared' or protected by a monitor.
 The compiler should make the distinction between shared code and shared  
 data and allow both shared and unshared instances to use shared methods,  
 just like both const and mutable instances may call const methods. An  
 error should also be triggered when calling a shared method of a shared  
 object without synchronization, and maybe have a __sync keyword to  
 override this. If a synchronized method is called from a non-shared  
 object, no synchronization takes place.

I think you have the wrong paradigm in mind. Shared and non-shared aren't mutable and const. They're mutable and immutable. From a technical perspective, synchronization of shared methods are handled by the callee, so there is no way not to call them and non-shared objects don't have a monitor that can be synchronized. Now you can have the compiler use the same code to generate two different object types (vtables, object layouts, etc) with have the same interface, but that doesn't sound like what you're suggesting.

I know that shared/unshared is not const/mutable. What I meant is that right now in D if a method is 'shared' it cannot be called from a non-shared object, which makes unshared instance of the class unusable without plenty of dirty casts. Take the following objects: class Foo { void foo() const; } class Bar { void bar() shared; } Foo foo; foo.foo(); // ok, mutable object can call const method Bar bar; bar.bar(); // error, unshared object may not call shared method I had only presented the concept, your idea of using two virtual tables for shared/unshared instances is also what I had in mind for the implementation, and it would give exactly the behavior I had in mind.
 Allow me to illustrate my point with some code:

 class Foo {
     int bar() shared { return a; }
     __sync bar2() { synchronized(this) return a; }
     synchronized void foo() { a = 1; }
     int a;
 }
 auto foo1 = new shared(Foo)();
 auto foo2 = new Foo;

 foo1.foo(); // ok, synchronized call
 synchronized(foo1) foo1.foo(); // warning: recursive synchronization

Why a warning? Monitors are designed to handle recursive synchronization.

Its a performance issue that can easily be avoided, but still generates valid code.
 foo2.foo(); // ok, unsynchronized call
 synchronized(foo2) foo2.foo(); // ok synchronized call

 foo1.bar(); // error, unsynchronized call to bar() shared
 synchronized(foo1) foo1.bar(); // ok, synchronized call
 foo2.bar(); // ok, unsynchronized call
 synchronized(foo1) foo1.bar(); // ok, synchronized call

 foo1.bar2(); // ok, method handles synchronized
 synchronized(foo1) foo1.bar2(); // warning, recursive synchronization
 foo2.bar2(); // ok, method handles synchronized, even on unshared object
 synchronized(foo2) foo2.bar2(); // warning, recursive synchronization,  
 even on unshared object

 That's about it, I believe this behavior would allow quite a number of  
 multi-threaded techniques to be easily implemented and documented. It  
 would only be the most natural thing since its quite similar to how  
 const works.

The major benefit of const isn't method declaration, but object use: i.e. only having to declare func(const T var) and not func(immutable T var) and func(T var). Currently, there's no planned type to fill this role though there have been some proposals.

I disagree, I think const methods are just as useful as const objects, since they are the only methods that can be called on such objects. They do not however prevent you from calling them on a mutable object. This is the behavior I want with shared too; unshared objects should be able to call shared methods, but shared objects should only be able to call shared methods.
 P.S. Shouldn't 'a' be either private or protected?

It should, but this was just an example ;)
 P.S.S. Bartosz Milewski has a good series of blogs on multi-threading  
 (with an eye on how to do it well in D).

I know, this is what sparked my interest for shared in the first place, I really look forward to implement most of his ideas in my runtime, but I am waiting until shared gets better semantics.
 Bike-shed: I've always preferred the CSP/pi-calculas term 'mobile' for the  
 concept of 'unique'. I think mobile better expresses the concept with  
 regard to multi-threading, where mobile is used to cheaply transfer data  
 between threads (i.e. it moves around/can move between threads, but isn't  
 shared between them). I find 'unique' to mainly convey the memory storage  
 aspect of the concept, which is less important outside of C/C++.

Maybe this is where 'volatile' could come back, from what I know it's still a reserved keyword in D and would fit nicely this purpose.
Sep 13 2009
next sibling parent Michel Fortin <michel.fortin michelf.com> writes:
On 2009-09-13 18:08:57 -0400, Jeremie Pelletier <jeremiep gmail.com> said:

 foo1.foo(); // ok, synchronized call
 synchronized(foo1) foo1.foo(); // warning: recursive synchronization

Why a warning? Monitors are designed to handle recursive synchronization.

Its a performance issue that can easily be avoided, but still generates valid code.

Also, some people consider recursive locks as potential design flaws. <http://landheer-cieslak.com/wordpress/?p=57> -- Michel Fortin michel.fortin michelf.com http://michelf.com/
Sep 13 2009
prev sibling next sibling parent Jeremie Pelletier <jeremiep gmail.com> writes:
Robert Jacques Wrote:

 On Sun, 13 Sep 2009 18:08:57 -0400, Jeremie Pelletier <jeremiep gmail.com>  
 wrote:
 
 Robert Jacques Wrote:

 On Sun, 13 Sep 2009 15:04:57 -0400, Jeremie Pelletier  
 <jeremiep gmail.com>
 wrote:
 [snip]
 Unique data could only be used for aggregate properties,  

 data would also be implicitly unique. This qualifier alone would
 simplify shared quite a lot, allowing the use of unshared objects in
 shared contexts safely.

Neither const nor immutable data can be considered unique. First, any const data may be being mutated by another routine, so it can't be safely accessed without synchronization. Second, unique data is mutable while const/immutable data is not. Third, most implementations of unique allow for deterministic memory reclamation, which isn't possible if the unique data might actually be const/immutable.

Good points, I can only agree with you here. However I still believe immutable data should be able to be used in shared contexts without being 'shared' or protected by a monitor.

One of the purposes behind immutable was lock-free access. As far as I know you can use immutable data in shared contexts today without any other modifiers. A quick test seems to indicate this works today, but if you've got a test case where it doesn't, I'd recommend filing it as a bug.

Oh yeah, I'm confusing it with 'shared' methods not able to call 'const shared' methods, which is a pain in the ass :(
 The compiler should make the distinction between shared code and  

 data and allow both shared and unshared instances to use shared  

 just like both const and mutable instances may call const methods. An
 error should also be triggered when calling a shared method of a  

 object without synchronization, and maybe have a __sync keyword to
 override this. If a synchronized method is called from a non-shared
 object, no synchronization takes place.

I think you have the wrong paradigm in mind. Shared and non-shared aren't mutable and const. They're mutable and immutable. From a technical perspective, synchronization of shared methods are handled by the callee, so there is no way not to call them and non-shared objects don't have a monitor that can be synchronized. Now you can have the compiler use the same code to generate two different object types (vtables, object layouts, etc) with have the same interface, but that doesn't sound like what you're suggesting.

I know that shared/unshared is not const/mutable. What I meant is that right now in D if a method is 'shared' it cannot be called from a non-shared object, which makes unshared instance of the class unusable without plenty of dirty casts. Take the following objects: class Foo { void foo() const; } class Bar { void bar() shared; } Foo foo; foo.foo(); // ok, mutable object can call const method Bar bar; bar.bar(); // error, unshared object may not call shared method I had only presented the concept, your idea of using two virtual tables for shared/unshared instances is also what I had in mind for the implementation, and it would give exactly the behavior I had in mind.

Bartosz took the concept one step further: when declared as shared, all methods are implicitly wrapped in synchronize blocks. He then added a keyword for more manual, lock-free style programming. But this syntactic sugar isn't implemented yet.

The current D keywords (synchronized and shared) are already designed for that, since synchronized implies shared. I don't want implicit synchronization, I'd much rather have a shared class marking all its members/properties as shared and letting me explicitely decide where the synchronization takes place.
 Allow me to illustrate my point with some code:

 class Foo {
     int bar() shared { return a; }
     __sync bar2() { synchronized(this) return a; }
     synchronized void foo() { a = 1; }
     int a;
 }
 auto foo1 = new shared(Foo)();
 auto foo2 = new Foo;

 foo1.foo(); // ok, synchronized call
 synchronized(foo1) foo1.foo(); // warning: recursive synchronization

Why a warning? Monitors are designed to handle recursive synchronization.

Its a performance issue that can easily be avoided, but still generates valid code.

Really? Every public method that calls another public method (of the same object) results in recursive synchronization. And if your example was longer than a one liner, you'd also have to have recursive synchronization. There are ways to reduce recursive synchronization, like public wrappers of protected/private methods, but they are not always appropriate or feasible for the use case. BTW, in general the threshold for what's a warning in DMD is generally a lot higher than other compilers (on the theory that if warnings are generated for every build you'll never read them)

Its a behavior that is really easy to avoid, and therefore the overhead easily avoided. The custom runtime I use doesn't use the reentrant attribute on pthread's mutexes and recursing into a monitor triggers a runtime exception, this is by design to better optimize the code. On Windows critical sections are sadly always reentrant. I haven't come across any case I wasn't able to easily design to avoid recursive mutexes yet.
 [snip]
 
 Bike-shed: I've always preferred the CSP/pi-calculas term 'mobile' for  
 the
 concept of 'unique'. I think mobile better expresses the concept with
 regard to multi-threading, where mobile is used to cheaply transfer data
 between threads (i.e. it moves around/can move between threads, but  
 isn't
 shared between them). I find 'unique' to mainly convey the memory  
 storage
 aspect of the concept, which is less important outside of C/C++.

Maybe this is where 'volatile' could come back, from what I know it's still a reserved keyword in D and would fit nicely this purpose.

The volatile keyword has a very precise meaning in C/C++, which D altered and then abandoned. I think using it for the concept of mobile/unique would confusing. It also lacks any connotations related to a mobile/unique type. (i.e. I don't see the logic behind the choice, besides the keyword being unused)

Ok, maybe not the greatest idea of all times, I agree :)
Sep 14 2009
prev sibling parent reply Jason House <jason.james.house gmail.com> writes:
Robert Jacques Wrote:

 On Sun, 13 Sep 2009 18:08:57 -0400, Jeremie Pelletier <jeremiep gmail.com>  
 wrote:
 .
 
 Bartosz took the concept one step further: when declared as shared, all  
 methods are implicitly wrapped in synchronize blocks. He then added a  
 keyword for more manual, lock-free style programming. But this syntactic  
 sugar isn't implemented yet.
 

That is not the design for D2. shared means shared. It is neither meant to mean synchronized nor lockfree. I worry about optimization opportunities for shared in D2. There may be way too many memory fences in synchronized code. Without a mapping of a monitor to what's protected under a monitor, the compiler/optimizer's hands are tied. At best, every object will be its own monitor, but that hardly makes any sense...
Sep 14 2009
parent Jeremie Pelletier <jeremiep gmail.com> writes:
Graham St Jack Wrote:

 So, what is the design of shared supposed to be then? Its time for Walter 
 to buy in and tell us where this is all going - I for one am very 
 confused right now.
 
 Currently I am working around it by not using synchronized methods (I put 
 synchronized blocks inside the methods), which is very bad form, but what 
 else can I do?

You're already more adventurous than I am, my current workaround has been to drop all threading support in my runtime, I'm focusing on different parts of my project that do not require threading for now and I'll piece it together when shared gets closer to a final concept.
Sep 15 2009
prev sibling next sibling parent "Robert Jacques" <sandford jhu.edu> writes:
On Sun, 13 Sep 2009 15:04:57 -0400, Jeremie Pelletier <jeremiep gmail.com>  
wrote:
[snip]
 Unique data could only be used for aggregate properties, const/immutable  
 data would also be implicitly unique. This qualifier alone would  
 simplify shared quite a lot, allowing the use of unshared objects in  
 shared contexts safely.

Neither const nor immutable data can be considered unique. First, any const data may be being mutated by another routine, so it can't be safely accessed without synchronization. Second, unique data is mutable while const/immutable data is not. Third, most implementations of unique allow for deterministic memory reclamation, which isn't possible if the unique data might actually be const/immutable.
 The compiler should make the distinction between shared code and shared  
 data and allow both shared and unshared instances to use shared methods,  
 just like both const and mutable instances may call const methods. An  
 error should also be triggered when calling a shared method of a shared  
 object without synchronization, and maybe have a __sync keyword to  
 override this. If a synchronized method is called from a non-shared  
 object, no synchronization takes place.

I think you have the wrong paradigm in mind. Shared and non-shared aren't mutable and const. They're mutable and immutable. From a technical perspective, synchronization of shared methods are handled by the callee, so there is no way not to call them and non-shared objects don't have a monitor that can be synchronized. Now you can have the compiler use the same code to generate two different object types (vtables, object layouts, etc) with have the same interface, but that doesn't sound like what you're suggesting.
 Allow me to illustrate my point with some code:

 class Foo {
     int bar() shared { return a; }
     __sync bar2() { synchronized(this) return a; }
     synchronized void foo() { a = 1; }
     int a;
 }
 auto foo1 = new shared(Foo)();
 auto foo2 = new Foo;

 foo1.foo(); // ok, synchronized call
 synchronized(foo1) foo1.foo(); // warning: recursive synchronization

Why a warning? Monitors are designed to handle recursive synchronization.
 foo2.foo(); // ok, unsynchronized call
 synchronized(foo2) foo2.foo(); // ok synchronized call

 foo1.bar(); // error, unsynchronized call to bar() shared
 synchronized(foo1) foo1.bar(); // ok, synchronized call
 foo2.bar(); // ok, unsynchronized call
 synchronized(foo1) foo1.bar(); // ok, synchronized call

 foo1.bar2(); // ok, method handles synchronized
 synchronized(foo1) foo1.bar2(); // warning, recursive synchronization
 foo2.bar2(); // ok, method handles synchronized, even on unshared object
 synchronized(foo2) foo2.bar2(); // warning, recursive synchronization,  
 even on unshared object

 That's about it, I believe this behavior would allow quite a number of  
 multi-threaded techniques to be easily implemented and documented. It  
 would only be the most natural thing since its quite similar to how  
 const works.

The major benefit of const isn't method declaration, but object use: i.e. only having to declare func(const T var) and not func(immutable T var) and func(T var). Currently, there's no planned type to fill this role though there have been some proposals. P.S. Shouldn't 'a' be either private or protected? P.S.S. Bartosz Milewski has a good series of blogs on multi-threading (with an eye on how to do it well in D). Bike-shed: I've always preferred the CSP/pi-calculas term 'mobile' for the concept of 'unique'. I think mobile better expresses the concept with regard to multi-threading, where mobile is used to cheaply transfer data between threads (i.e. it moves around/can move between threads, but isn't shared between them). I find 'unique' to mainly convey the memory storage aspect of the concept, which is less important outside of C/C++.
Sep 13 2009
prev sibling next sibling parent "Robert Jacques" <sandford jhu.edu> writes:
On Sun, 13 Sep 2009 18:08:57 -0400, Jeremie Pelletier <jeremiep gmail.com>  
wrote:

 Robert Jacques Wrote:

 On Sun, 13 Sep 2009 15:04:57 -0400, Jeremie Pelletier  
 <jeremiep gmail.com>
 wrote:
 [snip]
 Unique data could only be used for aggregate properties,  

 data would also be implicitly unique. This qualifier alone would
 simplify shared quite a lot, allowing the use of unshared objects in
 shared contexts safely.

Neither const nor immutable data can be considered unique. First, any const data may be being mutated by another routine, so it can't be safely accessed without synchronization. Second, unique data is mutable while const/immutable data is not. Third, most implementations of unique allow for deterministic memory reclamation, which isn't possible if the unique data might actually be const/immutable.

Good points, I can only agree with you here. However I still believe immutable data should be able to be used in shared contexts without being 'shared' or protected by a monitor.

One of the purposes behind immutable was lock-free access. As far as I know you can use immutable data in shared contexts today without any other modifiers. A quick test seems to indicate this works today, but if you've got a test case where it doesn't, I'd recommend filing it as a bug.
 The compiler should make the distinction between shared code and  

 data and allow both shared and unshared instances to use shared  

 just like both const and mutable instances may call const methods. An
 error should also be triggered when calling a shared method of a  

 object without synchronization, and maybe have a __sync keyword to
 override this. If a synchronized method is called from a non-shared
 object, no synchronization takes place.

I think you have the wrong paradigm in mind. Shared and non-shared aren't mutable and const. They're mutable and immutable. From a technical perspective, synchronization of shared methods are handled by the callee, so there is no way not to call them and non-shared objects don't have a monitor that can be synchronized. Now you can have the compiler use the same code to generate two different object types (vtables, object layouts, etc) with have the same interface, but that doesn't sound like what you're suggesting.

I know that shared/unshared is not const/mutable. What I meant is that right now in D if a method is 'shared' it cannot be called from a non-shared object, which makes unshared instance of the class unusable without plenty of dirty casts. Take the following objects: class Foo { void foo() const; } class Bar { void bar() shared; } Foo foo; foo.foo(); // ok, mutable object can call const method Bar bar; bar.bar(); // error, unshared object may not call shared method I had only presented the concept, your idea of using two virtual tables for shared/unshared instances is also what I had in mind for the implementation, and it would give exactly the behavior I had in mind.

Bartosz took the concept one step further: when declared as shared, all methods are implicitly wrapped in synchronize blocks. He then added a keyword for more manual, lock-free style programming. But this syntactic sugar isn't implemented yet.
 Allow me to illustrate my point with some code:

 class Foo {
     int bar() shared { return a; }
     __sync bar2() { synchronized(this) return a; }
     synchronized void foo() { a = 1; }
     int a;
 }
 auto foo1 = new shared(Foo)();
 auto foo2 = new Foo;

 foo1.foo(); // ok, synchronized call
 synchronized(foo1) foo1.foo(); // warning: recursive synchronization

Why a warning? Monitors are designed to handle recursive synchronization.

Its a performance issue that can easily be avoided, but still generates valid code.

Really? Every public method that calls another public method (of the same object) results in recursive synchronization. And if your example was longer than a one liner, you'd also have to have recursive synchronization. There are ways to reduce recursive synchronization, like public wrappers of protected/private methods, but they are not always appropriate or feasible for the use case. BTW, in general the threshold for what's a warning in DMD is generally a lot higher than other compilers (on the theory that if warnings are generated for every build you'll never read them) [snip]
 Bike-shed: I've always preferred the CSP/pi-calculas term 'mobile' for  
 the
 concept of 'unique'. I think mobile better expresses the concept with
 regard to multi-threading, where mobile is used to cheaply transfer data
 between threads (i.e. it moves around/can move between threads, but  
 isn't
 shared between them). I find 'unique' to mainly convey the memory  
 storage
 aspect of the concept, which is less important outside of C/C++.

Maybe this is where 'volatile' could come back, from what I know it's still a reserved keyword in D and would fit nicely this purpose.

The volatile keyword has a very precise meaning in C/C++, which D altered and then abandoned. I think using it for the concept of mobile/unique would confusing. It also lacks any connotations related to a mobile/unique type. (i.e. I don't see the logic behind the choice, besides the keyword being unused)
Sep 13 2009
prev sibling next sibling parent "Robert Jacques" <sandford jhu.edu> writes:
On Mon, 14 Sep 2009 07:44:44 -0400, Jason House  
<jason.james.house gmail.com> wrote:

 Robert Jacques Wrote:

 On Sun, 13 Sep 2009 18:08:57 -0400, Jeremie Pelletier  
 <jeremiep gmail.com>
 wrote:
 .

 Bartosz took the concept one step further: when declared as shared, all
 methods are implicitly wrapped in synchronize blocks. He then added a
 keyword for more manual, lock-free style programming. But this syntactic
 sugar isn't implemented yet.

That is not the design for D2. shared means shared. It is neither meant to mean synchronized nor lockfree. I worry about optimization opportunities for shared in D2. There may be way too many memory fences in synchronized code. Without a mapping of a monitor to what's protected under a monitor, the compiler/optimizer's hands are tied. At best, every object will be its own monitor, but that hardly makes any sense...

That is the Java model by the way. And really, no one knows what the shared design is for D2. The keyword has been added, but it has yet to be fleshed out. And once you start talking about how to flesh it out and what kinds of syntactic sugar are wanted/needed you need to start looking at previous solutions, which is what Bartosz has done in his blog posts. The specific issue you raise, that of excessive monitors, was addressed using the concept of unique/mobile objects which are both thread-safe and don't require locking. However, it appears that this won't make it into D2, which I feel is a shame.
Sep 14 2009
prev sibling parent Graham St Jack <graham.stjack internode.on.net> writes:
So, what is the design of shared supposed to be then? Its time for Walter 
to buy in and tell us where this is all going - I for one am very 
confused right now.

Currently I am working around it by not using synchronized methods (I put 
synchronized blocks inside the methods), which is very bad form, but what 
else can I do?
Sep 15 2009