shared - i need it to be useful

Stanislav Blinov stanislav.blinov at gmail.com
Thu Oct 18 23:06:18 UTC 2018


On Thursday, 18 October 2018 at 22:08:14 UTC, Simen Kjærås wrote:
> On Thursday, 18 October 2018 at 16:31:02 UTC, Stanislav Blinov

>> Now, if the compiler generated above in the presence of any 
>> `shared` members or methods, then we could begin talking about 
>> it being threadsafe...

> Again, this is good stuff. This is an actual example of what 
> can go wrong. Thanks!

You're welcome.

> No, void atomicInc(shared int*) is perfectly safe, as long as 
> it doesn't cast away shared.

Eh? If you can't read or write to your shared members, how *do* 
you implement your "safe" shared methods without casting away 
shared? Magic?!

> Again, the problem is int already has a non-thread-safe 
> interface, which Atomic!int doesn't.

*All* structs and classes have a non-thread-safe interface, as I 
have demonstrated, and thankfully you agree with that. But *there 
is no mention of it* in the OP, nor there was *any recognition* 
of it up to this point. When I asked about copying, assignment 
and destructors in the previous thread (thread, not post), Manu 
quite happily proclaimed those were fine given some arbitrary 
conditions. Yet those are quite obviously *not fine*, especially 
*if* you want to have a "safe" implicit conversion. *That* 
prompted me to assume that Manu didn't actually think long and 
hard about his proposal and what it implies. Without recognizing 
those issues:

struct S {
     private int x;
     void foo() shared;
}

void shareWithThread(shared S* s);

auto s = make!S;     // or new, whatever
shareWithThread(&s); // Manu's implicit conversion

// 10 kLOC below, written by some other guy 2 years later:
*s = S.init;

^ that is a *terrible*, and un-greppable BUG. How fast would you 
spot that in a review? Pretty fast if you saw or wrote the other 
code yesterday. A week later? A month? A year?..

Again, that is assuming *only* what I'm *certain about* in Manu's 
proposal, not something he or you assumed but didn't mention.

> And once more, for clarity, this interface includes any 
> function that has access to its private members, free function, 
> method, delegate return value from a function/method, what have 
> you. Since D's unit of encapsulation is the module, this has to 
> be the case. For int, the members of that interface include all 
> operators. For pointers, it includes deref and pointer 
> arithmetic. For arrays indexing, slicing, access to .ptr, etc. 
> None of these lists are necessarily complete.

Aight, now, *now* I can perhaps try to reason about this from 
your point of view. Still, I would need some experimentation to 
see if such approach could actually work. And that would mean 
digging out old non-`shared`-aware code and performing some... 
dubious... activities.

> I have no idea where I or Manu have said you can't make 
> functions that take shared(T)*.

Because that was the only way to reason about your 
interpretations of various examples until you said this:

> I think we have been remiss in the explanation of what we 
> consider the interface.
> For clarity: the interface of a type is any method, function, 
> delegate or otherwise that may affect its internals. That means 
> any free function in the same module, and any non-private 
> members.

Now compare that to what is stated in the OP and correlate with 
what I'm saying, you might understand where my opposition comes 
from.

> Now, Two very good points came up in this post, and I think 
> it's worth stating them again, because they do present possible 
> issues with MP:
>
> 1) How does MP deal with reorderings in non-shared methods?
>
> I don't know. I'd hide behind 'that's for the type implementor 
> to handle', but it's a subtle enough problem that I'm not happy 
> with that answer.
>
>
> 2) What about default members like opAssign and postblit?
>
> The obvious solution is for the compiler to not generate these 
> when a type has a shared method or is taken as shared by a free 
> function in the same module. I don't like the latter part of 
> that, but it should work.

Something I didn't yet stress about (I think only mentioned 
briefly somewhere) is, sigh, destructors. Right now, `shared` 
allows you to either have a `~this()` or a `~this() shared`, but 
not both. In my mind, `~this() shared` is an abomination. One 
should either:

1) have data that starts life shared (a global, or e.g. new 
shared(T)), and simply MUST NOT have a destructor. Such data is 
ownerless, or you can say that everybody owns it. Therefore 
there's no deterministic way of knowing whether or not or when to 
call the destructor. You can think of it as an analogy with 
current stance on finalizers with GC.

2) have data that starts life locally (e.g. it's not declared 
`shared`, but converted later). Such types MAY have a destructor, 
because they always have a cleanly defined owner: whoever holds 
the non-`shared` reference (recall that copying MUST be 
*disabled* for any shared-aware type). But that destructor MUST 
NOT be `shared`.

Consequently, types such as these:

shared struct S { /* ... */ }

MUST NOT define a destructor, either explicitly, or implicitly 
through members, i.e. it's a compile error if an __xdtor needs to 
be generated for such type.

However, in practice this would mean that practically all types 
couldn't have a destructor:

struct S {
     private shared X x; // X must not have a destructor, even 
though S can?..
}

Perhaps, an exception to (1) above could be made for such cases, 
but I'm too tired to think about wording at the moment.

Also, the proposal has no mention of interaction of `shared` and 
the GC, which AFAIK is also missing pretty much everywhere you 
can even get some information on current state of `shared`. This 
*needs* to be addressed.


More information about the Digitalmars-d mailing list