[dmd-concurrency] synchronized, shared, and regular methods inside the same class

Sean Kelly sean at invisibleduck.org
Mon Jan 4 19:49:08 PST 2010


On Jan 4, 2010, at 6:52 PM, Andrei Alexandrescu wrote:

> Sean Kelly wrote:
>> I'd planned to ask this a bit later in the discussion, but I've been wondering if all methods need to be labeled as shared or synchronized or if only the public (and possibly protected) ones do?  If I have:
>>    class A
>>    {
>>        void fnA() synchronized { fnB(); } // 1
>>        void fnB() shared { fnC(); } // 2
>>        private void fnC() {}
>>    }
>> Since fnC() may only be accessed through A's public interface, all of which is ostensibly safe, does fnC() have to be synchronized or shared?  I can see an argument for not allowing case 2, but it does seem safe for fnC() to be called by fnA() at least.  I know D uses recursive locks so there's no functional barrier to making fnC() synchronized, but this seems like it could be horribly slow.  I suppose the compiler could elide additional lock_acquire() calls though, based on static analysis.
> 
> We've considered using protection levels for inferring thread-related checks. One problem is that protection is suspended within the same module, which blunts its strength a lot.

Yeah, I thought about this and wasn't sure if it was fair to assert that if the user calls private methods then he should hopefully know what he's doing.  But static analysis should even work here (for the last point in my comment above).  You'd just have:

    class A {
        void fnA() synchronized { fnB(); } // 1
        private void fnB() synchronized {}
    }

    void mutateA( A val ) {
        val.fnB(); // 2
    }

At 1, the compiler can easily tell that the lock is already held so it doesn't acquire the lock again.  At 2 the lock is (probably) not held, so it's acquired again.  Since access to privates doesn't extend beyond the scope of the current module, the compiler should have all the information it needs to elide redundant locks via private method calls.

> Speaking of lock acquisition - Walter, one thing you may want to consider is to not insert synchronization code inside a method. Instead, put it at the call site. That way you have control over and you can optimize around lock calls.

Walter had mentioned that methods may have multiple entry points, I believe.  I don't know how common it is to use this approach though (I'd presume not very).

>>> Within such a shareable object, we can use low-level stuff like mutexes, semaphores and conditions to build the desired behaviour, wrapping it up and presenting a clean interface.
>> Since mutexes can override a class' monitor, it already works to do this:
>>    class A
>>    {
>>        this() {
>>            mut = new Mutex( this );
>>            cond = new Condition( mut );
>>        }
>>        void fnA() synchronized { // locks "mut"
>>            cond.wait(); // unlocks the lock acquired when fnA() was entered and blocks, etc.
>>        }
>>        Mutex mut; Condition cond;
>>    }
> 
> I didn't know you can do that. Where is Mutex documented? Does it steal/replace or supplement class' built-in mutex?

I've been very lazy about integrating the docs for Druntime into the Phobos docs,  but the comments are there if you look at src/core/sync/mutex.d.  In short, if you construct a Mutex with an Object argument then it makes that mutex the object's monitor (I believe there's an assert in there to make sure the object has no monitor yet as well).  Originally, I did this so objects could be constructed in shared memory and still use synchronized, but it's a nifty general solution.  The rest of the logic is in src/object_.d, if you're interested.  I consider the design to be one of my better D hacks.

>> I'm hoping this library-level trick will be enough to allow most of the classic synchronization mechanisms to be used in D 2.0.
>>> Re synchronized containers - I don't like the idea at all. That is going down the path of having many shared objects, which is notoriously difficult to get right, especially for non-experts. IMO, shared objects should be small in number, and serve as boundaries between threads, which otherwise play in their own separate sand-pits.
>> A good pathological but functionally correct example is the Thread class in core.thread.  Thread instances should really be labeled as shared, but I know that when this happens the compiler will throw a fit.  The thing is, while some of the methods are already synchronized, there are quite a few which are not and don't need to be and neither do they need the variables they access to be made lock-free.  Even weirder is:
> 
> Well I think Thread is a threading primitive so we shouldn't expect to be implementable without some oddities.

Sounds good.  Hopefully, it will be uniquely horrifying.


More information about the dmd-concurrency mailing list