Very limited shared promotion

Jonathan M Davis newsgroup.d at jmdavisprog.com
Wed Jun 19 08:45:10 UTC 2019


On Wednesday, June 19, 2019 1:29:21 AM MDT Ola Fosheim Grøstad via 
Digitalmars-d wrote:
> On Tuesday, 18 June 2019 at 22:30:28 UTC, Jonathan M Davis wrote:
> > Storing a reference to a scope object in @trusted code would
> > violate what scope is supposed to guarantee.
>
> Ok, so you are only considering stack allocated objects?
>
> My question is more like this. If I use GC throughout, then I
> might write a @trusted framework with that in mind, and take
> references wherever it makes sense. No problem, right?
>
> However, when someone pass a shared (still GC managed) object
> that has been temporarily "unshared", and pass it into that to
> the framework... then it will break badly.
>
> So, from a memory-management perspective the framework is sound,
> but not from a thread safety point of view.
>
> Is that right?

scope doesn't care where the object is stored. If something is scope, then
you can't take any references to it in @safe code. The whole point is to
ensure that no references escape. Whether the objects are GC-allocated are
not is irrelevant.

> > but if your @trusted code does anything which violates the
> > guarantees that @safe is supposed to make, then the programmer
> > who verified that it was okay to mark it as @trusted screwed up.
>
> But only if it was vetted with "shared" in mind, because if it
> was vetted for safe release of memory, and you use GC or
> reference counting, then it was perfectly OK, but still breaks
> for "unshared" shared?
>
> Maybe this will demand too much from the person doing the vetting
> of generic code, to both get it right for memory management and
> thread safety?

The only thing that @safe has to do with shared is that casting to or from
shared is @system. And _any_ code which is @trusted needs to maintain all of
the guarantees that go with @safe, or the programmer screwed up.

For Manu's suggestion to work, it would have to be guaranteed that @trusted
code vetted purely on the basis of memory safety (which is what
@safe/@trusted/@system is all about) would not run afoul of threading
issues, because the compiler implicitly cast a thread-local object to
shared. If that can't be guaranteed, then Manu's suggestion isn't tenable.

> > @safe really only deals with memory safety, not thread-safety.
> > Converting between shared and thread-local does require a cast,
> > which is @trusted, because you're basically stepping outside of
> > the type system.
>
> Ok, but what if you didn't step outside the type system? What
> would D need to cover this within the type system?

D's type system would actually have to understand ownership and concurrency
so that it would know when it was safe to implicitly cast to or from shared.
As it is, in general, D can't even know if there are multiple references to
the same object. Without that kind of information, it's not even possible to
know whether it's safe to pass an object from one thread to another. And
without some understanding of which concurrency primitives are being used to
protect an object or group of objects, it can't do something as simple as
know that locking a mutex has actually protected those objects such that
they can safely be manipulated from the current thread.

I don't know exactly what it would look like for the type system to have the
kind of information it would need to be able to implicitly cast to or from
shared, but I think that it's pretty clear that we're not going to actually
do whatever would be necessary to make that happen, because it would require
significant changes to how D works. Even the basics of how pointers
currently work wouldn't work with a system that had to keep track of
ownership or how many references to an object existed, and even if we were
willing to make such changes to D (and I'm quite sure that Walter is not),
how pointers currently work in D is pretty critical for how D code interacts
with C/C++ code. I'm pretty sure that most anything that requires
significant changes to D's type system is never going to happen. Even
something like what DIP 1000 has done with scope has been a royal pain to
get to where it is.

I think that it's quite clear at this point that most anything involving
shared is going to require that the programmer handle it all properly
(including converting to or from shared) and that there will be little to no
help from the compiler outside of preventing certain things in @safe code
and/or without casting. _Maybe_, there will be a few, small places like with
what Manu is suggesting here where we will be able to leverage what the
compiler knows to implicitly convert something, but in general, that's
really not going to work.

> > What Manu is proposing is a scenario where the type system is
> > able to guarantee that no references to the variable escape and
> > that based on that assumption, temporarily converting to shared
> > wouldn't violate the guarantees that come with the object
> > actually being thread-local.
>
> That sounds reasonable.
>
> > scope object actually escaped. If the programmer screws that
> > up, then the implicit conversion to shared will have violated
> > the guarantees that are supposed to go with the object being
> > thread-local, and unlike now, the point of the conversion
>
> Yes, but this is where a more elaborate type system would help. I
> haven't mentioned Pony
> ( https://www.ponylang.io/ ) in a while, so to recap:
>
> Pony tracks whether an object-reference is unique among other
> things.
>
> https://tutorial.ponylang.io/reference-capabilities/reference-capabilities
> .html
>
> So, that might be worth considering.
>
> I don't think it has to be tedious if you use auto/type
> deduction/flow typing.

I'd be very surprised if anything like that were considered acceptable for
D. When stuff like Rust's borrowing has been brought up before, Walter and
Andrei have made it pretty clear that we're not going to do something like
that with D's type system. Even getting something like DIP 1000 has been a
major ordeal, and I don't think that it would have ever happened unless
Walter had been convinced that it was absolutely needed for @safe code to be
able to do stuff like reference counting.

> > Having a function that's expecting a shared variable be given a
> > thread-local one seems off to me.
>
> Well, if you use reference-capabilites in the vein of Pony then I
> think it follows naturally.
>
> And that appears to be what D is trying to do with "shared", but
> without the precision needed to solve issues that seems
> reasonable to people working in the trenches (like Manu).

All that D is trying to do with shared is separate thread-local data from
shared data. That's why shared exists. That way, the vast majority of code
doesn't have to care about threads, and the code that does have to care
about it is clearly segregated. Not much thought beyond that went into the
initial creation of shared. It was simply the natural side effect of making
everything thread-local by default.

And shared works fine as-is if you're dealing with concurrency in the same
way that you would in C++. It's just that it requires some additional
casting, because C++ doesn't have thread-local vs shared as part of its type
system. The primary things that shared lacks at this point are:

1. Making read/write operations illegal on shared objects, since they're not
thread-safe.
2. Updating the library primitives for concurrency to use shared properly
(in particular, the stuff in core.sync was written prior to shared really
being a thing, and it hasn't been properly updated).
3. The memory model needs to be ironed out so that it's clear what exactly
happens with shared. Presumably, we're going to end up with something that's
basically C++'s memory model with any tweaks that need to be made for D, but
the details still need to be figured out.

There are likely some other things that would come up with ironing out the
details, but with those three main things ironed out, shared should largely
be where it needs to be. It does mean that casting to/from shared will be
required, and such code will have to be @trusted, but everything necessary
to use shared is there. It just doesn't involve the compiler doing much for
you except protecting you from doing stuff that's clearly wrong unless you
cast, at which point, the code is @system/@trusted and easily tracked down
so that it can be properly vetted to make sure that it does the concurrency
stuff correctly.

Additional improvements on top of that would be nice, but since there's
pretty much no way that significant changes are going to be made to D's type
system at this point, it's pretty questionable that we're going to be able
to do much in the way of things like implicitly casting away shared. The
only viable feature along those lines that's been proposed thus far is
TDPL's synchronized classes, and even those could only strip away the outer
layer of shared (i.e. what's directly in the object), making them pretty
useless - not to mention, they require classes, which doesn't fit in well
with how D code is normally written.

> > shared without letting it escape, but I'm still inclined to
> > think that the conversion should be vetted by the programmer
> > rather than being considered okay and done implicitly just
> > because scope is involved.
>
> But, to play the devil's advocate:
>
> If the compiler doesn't provide strong semantic passes for
> "shared" why do you then need "shared" to be part of the type
> system?  Why couldn't you then just let "shared" be implemented
> as a template within a templated pointer framework?
>
> For "shared" to be justified as a language feature it has to
> provide something that cannot be done within the meta-programming
> capabilities of the language.
>
> For "const" it is obvious, the transitive const cannot be done
> within the meta-programming capabilities of the language (or
> maybe it can, I am making an assumption).
>
> But how does "shared" justify itself?  Is it all about
> @safe/@trusted/@system?
>
> In that case, maybe those capabilities could be available as
> meta-programming mechanisms so that "shared" could be done as a
> library feature.
>
> Or rather, what prevents "shared" from being a library feature
> (assuming all pointers are templates).

shared exists so that almost everything in D can be treated as thread-local,
and the type system prevents you from converting between thread-local and
shared without casting. Even immutable ends up as part of it, because it's
implicitly shared. The compiler knows which objects are thread-local and
which are shared and can use that information in code generation. It also
means that it's possible to prevent operations on shared objects that are
not clearly thread-safe, thereby making it so that if the programmer is
going to screw up with objects that are shared across threads, they're going
to have to cast to force the matter. @system/@trusted then gets involved in
the sense that the cast is @system, but beyond that, @safety doesn't have
anything to do with it. I don't see how any of that could be done via a
library rather than the type system.

- Jonathan M Davis






More information about the Digitalmars-d mailing list