Proposal to make "shared" (more) useful

Jonathan M Davis newsgroup.d at jmdavisprog.com
Thu Sep 13 19:49:18 UTC 2018


On Thursday, September 13, 2018 7:53:49 AM MDT Arafel via Digitalmars-d 
wrote:
> Hi all,
>
> I know that many (most?) D users don't like using classes or old,
> manually controlled, concurrency using "shared" & co., but still, since
> they *are* in the language, I think they should at least be usable.
>
> After having had my share (no pun intended) of problems using shared,
> I've finally settled for the following:
>
> * Encapsulate all the shared stuff in classes (personal preference,
> easier to pass around).
> * When possible, try to use "shared synchronized" classes, because even
> if there are potential losses of performance, the simplicity is often
> worth it. This mean that the classed is declared:
>
> ```
> shared synchronized class A { }
> ```
>
> and now, the important point:
>
> * Make all _private non-reference fields_ of shared, synchronized
> classes __gshared.
>
> AIUI the access of those fields is already guaranteed to be safe by the
> fact that *all* the methods of the class are already synchronized on
> "this", and nothing else can access them.
>
> Of course, assuming you then don't escape references to them, but I
> think that would be a *really* silly thing to do, at least in the most
> common case... why on earth are they then private in the first place?.
>
> Now, the question is, would it make sense to have the compiler do this
> for me in a transparent way? i.e. the compiler would automatically store
> private fields of shared *and* synchronized classes in the global storage.
>
> Bonus points if it detects and forbids escaping references to them,
> although it could also be enough to warn the user.

Have you read the concurrency chapter in The D Programming Language by
Andrei? It sounds like you're trying to describe something vere similar to
the synchronized classes from TDPL (which have never been fully implemented
in the language). They would make it so that you had a class with shared
members but where the outer layer of shared was stripped away inside member
functions, because the compiler is able to guarantee that they don't escape
(though it can only guarantee that for the outer layer). Every member
function is synchronized and no direct access to the member variables
outside of the class (even in the same module) is allowed. It would make
shared easier to use in those cases where it makes sense to wrapped
everything protected by a mutex in a class (though since it can only safely
strip away the outer layer of shared, it's more limited than would be nice,
and there are plenty of cases where it doesn't make sense to stuff something
in a class just to use it as shared).

> This way I think there would an easy and sane way of using shared,
> because many of its worst quirks (for one, try using a struct like
> SysTime that overrides OpAssign, but not for shared objects, as a field)
> would be transparently dealt with.

The fact that most operations are not allowed with shared is _on purpose_.
If anything, too many operations are currently legal. What's really supposed
to be happening is that every single operation on a shared object is either
guaranteed to be thread-safe, or it's illegal. And if it's illegal, that
means that you either need to use atomics to do an operation (since they're
thread-safe), or you need to protect the shared object with a mutex and
temporarily cast away shared while the mutex is locked so that you can
actually do something with the object - and then make sure that no
thread-local references exist when the mutex is released.

Something like copying a shared object shouldn't even be legal in general.
An object that defines opAssign prevents it now, but the fact that it's
legal on any type where copying is not guaranteed to be thread-safe is a
bug. It's one of those details of shared that has never been fully fleshed
out like it should be. Walter and Andrei have been discussing finishing
shared, but it hasn't been a high enough priority for it actually get fully
sorted out yet. Once it is, unless you're dealing with a type that isn't
guaranteed to be thread-safe when copying it, it won't be legal copy it
without first casting away shared. Anything less than that would violate
what shared is supposed to do.

What you should be thinking when dealing with any shared object and whether
a particular operation should be allowed is whether that operation is
guaranteed to be thread-safe. If the compiler can't guarantee that the
operation is thread-safe, then it's not supposed to be legal. The main area
that Walter and Andrei haven't agreed upon yet is how much the compiler can
or should do to ensure that something is thread-safe rather than just making
an operation illegal (e.g. whether memory barriers should be involved). So,
_maybe_ some operations will end up as legal thanks to the compiler adding
extra code to do something to ensure thread-safety, but in most situations,
it's just going to be illegal.

So, ultimately, every type is either going to need to be designed such that
it simply does not work as shared, or it manages the thread-safety stuff for
you. If the object is not designed to be used as shared, then that means
that if you want to, you need to protect it with a mutex (be that with
synchronized or directly using mutexes) and cast away shared correctly when
the object is protected by the mutex. It's annoying, but it prevents
thread-safety bugs, and it allows the compiler (and the programmer) to treat
the rest of the program as thread-local. On the other hand, if the type is
designed to be used as shared (so it actually has shared member functions),
then that means that the type itself is going to need to deal with all of
the thread-safety stuff internally (be that by using mutexes and casting or
using atomics or whatever).

If we're going to find ways to make shared require less manual work, it
means finding a way to protect a shared object (or group of shared objects)
with a mutex in a way that is able to guarantee that when you operate on the
data, it's protected by that mutex and that no reference to that data has
escaped. TDPL's synchronized classes are one attempt to do that, but the
requirement that no references escape (so that shared can safely be cast
away) makes it so that only the outer layer of shared can be cast away, and
it's extremely difficult to do better than that with having holes such that
it isn't actually guaranteed to be thread-safe when shared is cast away.
Maybe someone will come up with something that will work, but I wouldn't bet
on it. Either way, I don't see how any solution is going to be acceptable
which does not actually guarantee thread-safety, because it would be
violating the guarantees of shared otherwise. A programmer can choose to
cast away shared in an unsafe manner (or use __gshared) and rely on their
ability to ensure that the code is thread-safe rather than letting shared do
its job, but that's not the sort of thing that we're going to do with a
language construct, and given that the compiler assumes that anything that
isn't shared or immutable is thread-local, it's very much a risky thing to
do.

As for __gshared, it's intended specifically for C globals, and using it for
anything else is just begging for bugs. Because the compiler assumes that
anything which is not marked as shared or immutable is thread-local, having
such an object actually be able to be mutated by another thread risks subtle
bugs of the sort that shared was supposed to prevent in the first place.
Unfortunately, due to some of the difficulties in using shared and some of
the misunderstandings about it, a number of folks have just used __gshared
instead of shared, but once you do that, you're risking subtle bugs, because
that's not at all what __gshared is intended for. If you're using __gshared
for anything other than a C global, it's arguably a bug. Certainly, it's a
risky proposition.

- Jonathan M Davis





More information about the Digitalmars-d mailing list