On 10/18/18 2:24 PM, Manu wrote:
I understand your argument, and I used to think this too... but I
concluded differently for 1 simple reason: usability.

You have not demonstrated why your proposal is usable, and the proposal to simply make shared not accessible while NOT introducing implicit conversion is somehow not usable.

I find quite the opposite -- the implicit conversion introduces more pitfalls and less guarantees from the compiler.

I have demonstrated these usability considerations in production. I am
confident it's the right balance.

Are these considerations the list below, or are they something else? If so, can you list them?

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.

Thread safety is not easy. But it's also not generic.

In terms of low-level things like atomics and lock-free implementations, those ARE generic and SHOULD only be written by experts. But other than that, you can't know how someone has designed all the conditions in their code.

For example, you can have an expert write mutex locks and semaphores. But they can't tell you the proper order to lock different objects to ensure there's no deadlock. That's application specific.

  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.

I also do not expect anyone to perform unsafe casts in normal use. I expect them to use more generic well-written types in a shared-object library. Casting should be very rare.

  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.

I think these are great rules, and none are broken by keeping the explicit cast requirement in place.

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.

This is a mischaracterization. The thread-local path is perfectly safe because only one thread can be accessing the data. That's why it's thread-local and not shared.

2. I will trade that for confidence in safe interaction every day of
the week. Safety is the right default position here.

You can be confident that any shared data is properly synchronized via the API provided. No confidence should be lost here.

2. You just need to make the unsafe thread-exclusive variant explicit, eg:

It is explicit, the thread-exclusive variant is not marked shared, and cannot be called on data that is actually shared and needs synchronization.


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
     }
}

This is more design by convention.


I think this is quiet a reasonable and clearly documented compromise.
I think absolutely-reliably-threadsafe-by-default is the right default
position. And if you want to accept unsafe operations for optimsation
circumstances, then you're welcome to deploy that in your code as you
see fit.

All thread-local operations are thread-safe by default, because there can be only one thread using it. That is the beauty of the current regime, regardless of how broken shared is -- unshared is solid. We shouldn't want to break that guarantee.

If the machinery is not a library for distribution and local to your
application, and you know for certain that your context is such that
thread-local and shared are mutually exclusive, then you're free to
make the unshared overload not-threadsafe; you can do this because you
know your application context.
You just shouldn't make widely distributed tooling this way.

I can make widely distributed tooling that does both shared and unshared versions of the code, and ALL are thread safe. No choices are necessary, no compromise on performance, and no design by convention.

I will indeed do this myself in some cases, because I know those facts
about my application.
But I wouldn't compromise the default design of shared for this
optimisation potential... deliberately deployed optimisation is okay
to be unsafe when taken in context.


Except it's perfectly thread safe to use data without synchronization in one thread -- which is supported by having unshared data. Unshared means only one thread. In your proposal, anything can be seen from one or more threads.

-Steve

Reply via email to