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.