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