shared - i need it to be useful

Simen Kjærås simen.kjaras at gmail.com
Thu Oct 18 13:09:10 UTC 2018


On Thursday, 18 October 2018 at 12:15:07 UTC, Stanislav Blinov 
wrote:
> On Thursday, 18 October 2018 at 11:35:21 UTC, Simen Kjærås 
> wrote:
>> On Thursday, 18 October 2018 at 10:08:48 UTC, Stanislav Blinov 
>> wrote:
>>> Manu,
>>>
>>> how is it that you can't see what *your own* proposal 
>>> means??? Implicit casting from mutable to shared means that 
>>> everything is shared by default! Precisely the opposite of 
>>> what D proclaims.
>>
>> Well, sorta. But that's not a problem, because you can't do 
>> anything that's not threadsafe to something that's shared.
>
> Yes you can. You silently agree to another function's 
> assumption that you pass shared data, while actually passing 
> thread-local data and keeping treating it as thread-local. I.e. 
> you silently agree to a race.

No, you don't. If I give you a locked box with no obvious way to 
open it, I can expect you not to open it. It's the same thing. If 
you have a shared(T), and it doesn't define a thread-safe 
interface, you can do nothing with it. If you are somehow able to 
cause a race with something with which you can do nothing, please 
tell me how, because I'm pretty sure that implies the very laws 
of logic are invalid.


>>> If *any* free function `foo(shared T* bar)`, per your 
>>> definition, is not threadsafe, then no other function with 
>>> shared argument(s) can be threadsafe at all. So how do you 
>>> call functions on shared data then? You keep saying "methods, 
>>> methods..."
>>>
>>> struct Other { /* ... */ }
>>>
>>> struct S {
>>>     void foo(shared Other*) shared;
>>> }
>>>
>>> Per your rules, there would be *nothing* in the language to 
>>> prevent calling S.foo with an unshared Other.
>>
>> That's true. And you can't do anything to it, so that's fine.
>
> Yes you can do "anything" to it.

No, you can't. You can do thread-safe things to it. That's 
nothing, *unless* Other defines a shared (thread-safe) interface, 
in which case it's safe, and everything is fine.

Example:

struct Other {
     private Data payload;

     // shared function. Thread-safe, can be called from a
     // shared object, or from an unshared object.
     void twiddle() shared { payload.doSharedStuff(); }

     // unshared function. Cannot be called from a shared object.
     // Promises not to interfere with shared data, or to so only
     // in thread-safe ways (by calling thread-safe methods, or
     // by taking a mutex or equivalent).
     void twaddle() { payload.doSharedThings(); }

     // Bad function. Promises not to interfere with shared data,
     // but does so anyway.
     // Give the programmer a stern talking-to.
     void twank() {
         payload.fuckWith();
     }
}

struct S {
    void foo(shared Other* o) shared {
        // No can do - can't call non-shared functions on shared 
object.
        // o.twaddle();

        // Can do - twiddle is always safe to call.
        o.twiddle();
    }
}

> If you couldn't, you wouldn't be able to implement `shared` at 
> all. Forbidding reads and writes isn't enough to guarantee that 
> you "can't do anything with it".

Alright, so I have this shared object that I can't read from, and 
can't write to. It has no public shared members. What can I do 
with it? I can pass it to other guys, who also can't do anything 
with it. Are there other options?


>>> The rest just follows naturally.
>
> Nothing follows naturally. The proposal doesn't talk at all 
> about the fact that you can't have "methods" on primitives,

You can't have thread-safe methods operating directly on 
primitives, because they already present a non-thread-safe 
interface. This is true. This follows naturally from the rules.


> that you can't distinguish between shared and unshared data if 
> that proposal is realized,

And you can't do that currently either. Just like today, 
shared(T) means the T may or may not be shared with other thread. 
Nothing more, nothing less.


> that you absolutely destroy D's TLS-by-default treatment...

I'm unsure what you mean by this.


>> There's actually one more thing: The one and only thing you 
>> can do (without unsafe casting) with a shared object, is call 
>> shared methods and free functions on it.
>
> Functions that you must not be allowed to write per this same 
> proposal. How quaint.

What? Which functions can't I write?

// Safe, regular function operating on shared data.
void foo(shared(Other)* o) {
     o.twiddle(); // Works great!
}

// Unsafe function. Should belong somewhere deep in druntime
// and only be used by certified wizards.
void bar(shared(int)* i) {
     atomicOp!"++"(i);
}

>>> 1. Primitive types can't be explicitly `shared`.
>>
>> Sure they can, they just can't present a thread-safe 
>> interface, so you can't do anything with a shared(int).
>
> Ergo... you can't have functions taking pointers to shared 
> primitives. Ergo, `shared <primitive type>` becomes a useless 
> language construct.

Yup, this is correct. But wrap it in a struct, like e.g. 
Atomic!int, and everything's hunky-dory.


>>> 2. Free functions taking `shared` arguments are not allowed.
>>
>> Yes, they are. They would be using other shared methods or 
>> free functions on the shared argument, and would thus be 
>> thread-safe. If defined in the same module as the type on 
>> which they operate, they would have access to the internal 
>> state of the object, and would have to be written in such a 
>> way as to not violate the thread-safety of other methods and 
>> free functions that operate on it.
>
> This contradicts (1). Either you can have functions taking 
> shared T* arguments, thus
> creating threadsafe interface for them, or you can't. If, per 
> (1) as you say, you can't

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


>>> 4. Every variable is implicitly shared, whether intended so 
>>> or not.
>
>> Well, yes, in the same sense that every variable is also 
>> implicitly const, whether intended so or not.
>
> I sort of expected that answer. No, nothing is implicitly 
> const. When you pass a reference to a function taking const, 
> *you keep mutable reference*, the function agrees to that, and 
> it's only "promise" is to not modify data through the reference 
> you gave it. But *you still keep mutable reference*. Just as 
> you would keep *unshared mutable* reference if implicit 
> conversion from mutable to shared existed.

Yup, and that's perfectly fine, because 'shared' means 
'thread-safe'. I think Manu might have mentioned that once.

If a type presents both a shared and a non-shared interface, and 
the non-shared interface may do things that impact the shared 
part, these things must be done in a thread-safe manner. If 
that's not the case, you have a bug. The onus is on the creator 
of a type to do this.

Let's say it together: for a type to be thread-safe, all of its 
public members must be written in a thread-safe way. If a 
non-shared method may jeopardize this, the type is not 
thread-safe, and shouldn't provide a shared interface.

--
   Simen


More information about the Digitalmars-d mailing list