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.

Reply via email to