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