D 2.0 FAQ on `shared`

Sean Kelly via Digitalmars-d digitalmars-d at puremagic.com
Mon Oct 20 09:18:51 PDT 2014


On Monday, 20 October 2014 at 13:29:47 UTC, Marco Leise wrote:
>> What guarantees is shared supposed to provide?
>>
>> Shared means that multiple threads can access the data. The
>> guarantee is that if it is not shared, and not immutable, that
>> only the current thread can see it.
>
> What if I have a thread that contains some shared data? Should
> the thread be created as shared, be cast to shared after
> construction or not be shared and fine grained shared applied
> to the respective shared data fields?

Since Thread is by its very nature a shared thing, Thread should 
probably be defined as shared.  But more generally it depends on 
the use case.


>> What does shared have to do with synchronization?
>>
>> Only shared data can be synchronized. It makes no sense to
>> synchronize thread local data.
>
> Define synchronized. With atomic ops on word size items this
> is clear, but what does it mean for aggregates? The language
> shows us no connection between synchronization and the shared
> data. What is one unit of data that is to be synchronized?

I think there's some conflation of two separate uses of 
"synchronized" here.  I think the above is actually talking about 
synchronized methods (ie. involving a mutex).


>> What does shared have to do with memory barriers?
>>
>> Reading/writing shared data emits memory barriers to ensure
>> sequential consistency (not implemented).
>
> That's exactly the problem. It assumes the unit is a word size
> item.

I'd say the real problem is more that it assumes, or at least 
suggests, that sequential consistency of shared variables will 
result in a correct program.  It won't, for any non-trivial uses 
of shared variables.  Lock-free programming is really, really 
hard, even for experts.  Using shared variables in this way 
shouldn't be easy semantically because it provides a false sense 
of security, resulting in programs that are silently broken in 
weird ways under some conditions but not others.


> If I have a Mutex to protect my unit of shared data, I
> don't need "volatile" handling of shared data.
>
>     private shared class SomeThread : Thread
>     {
>     private:
>
>         Condition m_condition;
>         bool m_shutdown = false;
>         ...
>     }

Yep.  This is one of my biggest issues with shared as it applies 
to user-defined types.  I even raised it in the now defunct 
concurrency mailing list before the design was finalized.  Sadly, 
there's no good way to sort this out, because:

shared class A {
     int m_count = 0;
     void increment() shared {
         m_count.atomicOp!"+="(1);
     }

     int getCount() synchronized {
         return m_count;
     }
}

If we make accesses of shared variables non-atomic inside 
synchronized methods, there may be conflicts with their use in 
shared methods.  Also:

shared class A {
     void doSomething() synchronized {
         doSomethingElse();
     }

     private void doSomethingElse() synchronized {

     }
}

doSomethingElse must be synchronized even if I as a programmer 
know it doesn't have to be because the compiler insists it must 
be.  And I know that private methods are visible within the 
module, but the same rule applies.  In essence, we can't avoid 
recursive mutexes for implementing synchronized, and we're stuck 
with a lot of recursive locks and unlocks no matter what, as soon 
as we slap a "shared" label on something.


> m_shutdown will be shared and it is shared data, but it is
> synchronized by the Mutex contained in that condition.
> Automatic memory barriers and such would only slow down
> execution.

Yes.  Though there's no overhead for having a Mutex synchronize 
one more operation.  A Mutex is basically just a shared variable 
indicating locked state.  When you leave a Mutex a shared 
variable is written to to indicate that the Mutex is unlocked, 
and the memory model in that language/platform/cpu guarantees 
that all operations logically occurring before that shared write 
actually do complete before the shared write, at least to anyone 
who acquires that same mutex before looking at the protected data 
(ie. there's a reader-writer contract).


>> What are the semantics of casting FROM unshared TO shared?
>>
>> Make sure there are no other unshared references to that same
>> data.
>>
>> What are the semantics of casting FROM shared TO
>> unshared?
>>
>> Make sure there are no other shared references to that same
>> data.
>
> That's just wrong to ask. `SomeThread` is a worker thread and
> data is passed to it regularly through a shared reference that
> is certainly never going away until the thread dies.
> Yet I must be able to "unshare" it's list of work items to
> process them.

Sure, but at that point they are no longer referenced by the 
shared Thread, correct?  The rule is simply that you can't be 
trying to read or write data using both shared and unshared 
operations, because of that reader-writer contract I mentioned 
above.


> Now let's say I have an "empty" property. Shared or unshared?
>
> 	override @property bool empty() const
> 	{
> 		return m_list.empty;
> 	}
>
> It is only called internally by the thread itself after
> entering a certain critical section. I _know_ that m_list wont
> be accessible by other threads while .empty is running.

So one thing about shared that Walter confirmed at some point is 
that atomic ops won't be imposed on operations within a shared 
method.  But I suspect that someone is likely to read the 
preceding sentence and say "woah! We never said that!  And if we 
did, that's wrong!"  In short, I agree with you that shared, as 
described, kind of sucks here because you're stuck with a ton of 
inefficiency that you, as an intelligent programmer, know is 
unnecessary.


> But this seeming 1:1 relationship between entering "the"
> critical section and stripping shared is of course
> non-existent. Aggregates may contain Mutexes protecting
> different fields or even stacking on top of each other.
>
> So the text should read:
>
>> What are the semantics of casting FROM shared TO unshared?
>>
>> Make sure that during the period the data is unshared, no
>> other thread can modify those parts of it that you will be
>> accessing. If you don't use synchronization objects with
>> built-in memory-barriers like a Mutex, it is your
>> responsibility to properly synchronize data access through
>> e.g. atomicLoad/Store.
>
> That at least in general sanctifies casting away shared for
> the purpose of calling a method under protection of a user
> defined critical section.

It's more complicated than that, because you don't know how long 
a given operation needs to propagate to another CPU.  Simply 
performing a shared write is meaningless if something else is 
performing an unshared read because the optimization happens at 
both points--the write side and the read side.

In essence, the CPU performs the same optimizations as a 
compiler.  Depending on the architecture, reads may be rearranged 
to occur before other reads or writes, and writes may be 
rearranged to occur before other reads and writes.  On most 
architectures the CPU makes some intelligent guesses about what 
operations are safe to rearrange (look into "dependent loads"), 
though on some few others like the DEC Alpha (a CPU invented by 
crazy people), they do not and if you don't explicitly tell them 
what needs to happen, it won't.

Basically what's needed is some way to have the compiler optimize 
according to the same rules as the CPU (the goal of "shared").  
Or in lieu of that, to have some "don't optimize this" 
instruction to tell the compiler to keep it's dirty hands off 
your carefully constructed code so the only thing you need to 
worry about is what the CPU is trying to do.  This is what 
"volatile" was meant for in D1 and I really liked it, but I think 
I was the only one.

There's a paper on release consistency that I think is fantastic. 
  I'll link it later if I can find it on the interweb.  CPUs seem 
to be converging on even more strict memory ordering than release 
consistency, but the release consistency model is really 
fantastic as it's basically equivalent to how mutexes work and so 
it's a model everyone already understands.


More information about the Digitalmars-d mailing list