On Thursday, 18 October 2018 at 18:24:47 UTC, Manu wrote:
I have demonstrated these usability considerations in
production. I am
confident it's the right balance.
Then convince us. So far you haven't.
I propose:
1. Normal people don't write thread-safety, a very small
number of
unusual people do this. I feel very good about biasing 100% of
the
cognitive load INSIDE the shared method. This means the expert,
and
ONLY the expert, must make decisions about thread-safety
implementation.
No argument.
2. Implicit conversion allows users to safely interact with
safe
things without doing unsafe casts. I think it's a complete
design fail
if you expect any user anywhere to perform an unsafe cast to
call a
perfectly thread-safe function. The user might not properly
understand
their obligations.
Disagreed. "Normal" people wouldn't be doing any unsafe casts and
*must not be able to* do something as unsafe as silent promotion
of thread-local mutable data to shared. But it's perfectly fine
for the "expert" user to do such a promotion, explicitly.
3. The practical result of the above is, any complexity
relating to
safety is completely owned by the threadsafe author, and not
cascaded
to the user. You can't expect users to understand, and make
correct
decisions about threadsafety. Safety should be default position.
Exactly. And an implicit conversion from mutable to shared isn't
safe at all.
I recognise the potential loss of an unsafe optimised
thread-local path.
1. This truly isn't a big deal. If this is really hurting you,
you
will notice on the profiler, and deploy a thread-exclusive path
assuming the context supports it.
2. I will trade that for confidence in safe interaction every
day of
the week. Safety is the right default position here.
Does not compute. Either you're an "expert" from above and live
with that burden, or you're not.
2. You just need to make the unsafe thread-exclusive variant
explicit, eg:
struct ThreadSafe
{
private int x;
void unsafeIncrement() // <- make it explicit
{
++x; // User has asserted that no sharing is possible,
no reason to use atomics
}
void increment() shared
{
atomicIncrement(&x); // object may be shared
}
}
No. The above code is not thread-safe at all. The private int
*must* be declared shared. Then it becomes:
struct ThreadSafe {
// These must be *required* if you want to assert any thread
safety. Be nice
// if the compiler did that for us.
@disable this(this);
@disable void opAssign(typeof(this));
private shared int x; // <- *must* be shared
void unsafeIncrement() @system {
x.assumeUnshared += 1;
}
// or deduced:
void unsafeIncrement()() // assumeUnshared must be @system,
thus this unsafeIncrement will also be deduced @system
{
x.assumeUnshared += 1;
}
void increment() shared {
x.atomicOp!"+="(1);
}
}
I think this is quiet a reasonable and clearly documented
compromise.
With the fixes above, it is. Without them, it will only be
apparent from documentation, and who writes, or reads, that?..
I think absolutely-reliably-threadsafe-by-default is the right
default position.
But it is exactly the opposite of automatic promotions from
mutable to shared!