October 21, 2018
On Saturday, 20 October 2018 at 16:41:41 UTC, Stanislav Blinov wrote:
> Those are not "ok". They're only "ok" under Manu's proposal so long as the author of C promises (via documentation) that that's indeed "ok". There can be no statically-enforced guarantees that those calls are "ok", or that issuing them in that order is "ok". Yet Manu keeps insisting that somehow there is.

No he is not insisting you can statically enforce thread safety.

When I say ok, I mean assuming the implementer actually wrote correct code. This applies to any shared method today as well.

October 21, 2018
On Sunday, 21 October 2018 at 05:47:14 UTC, Manu wrote:
> On Sat, Oct 20, 2018 at 10:10 AM Stanislav Blinov via Digitalmars-d <digitalmars-d@puremagic.com> wrote:

>> Synchronized with what? You still have `a`, which isn't `shared` and doesn't require any atomic access or synchronization. At this point it doesn't matter if it's an int or a struct. As soon as you share `a`, you can't just pretend that reading or writing `a` is safe.

> `b` can't read or write `a`... accessing `a` is absolutely safe.

It's not, with or without your proposal. The purpose of sharing `a` into `b` is to allow someone to access `*a` in a threadsafe way (but un-@safe, as it *will* require casting away `shared` from `b`). That is what's making keeping an unshared reference `a` un-@safe: whoever accesses `*a` in their @trusted implementations via `*b` can't know that `*a` is being (@safe-ly!) accessed in a non-threadsafe way at the same time.

> Someone must do something unsafe to undermine your threadsafety... and
> if you write unsafe code and don't know what you're doing, there's
> nothing that can help you.

Ergo, it follows that anyone that is making an implicit cast from mutable to shared better know what they're doing, which mere mortal users (not "experts") might not. I.e. it's a way to giving a loaded gun to someone who never held a weapon before.

> Today, every interaction with shared is unsafe.

Nod.

> Creating a safe interaction with shared will lead to people not doing unsafe things at every step.

Triple nod.

>> Encapsulate it all you want, safety only remains a
>> contract of convention, the language can't enforce it.
>
> You're talking about @trusted code again. You're fixated on unsafe interactions... my proposal is about SAFE interactions. I'm trying to obliterate unsafe interactions with shared.

I know... Manu, I *know* what you're trying to do. We (me, Atila, Timon, Walter...) are not opposing your goals, we're pointing out the weakest spot of your proposal, which, it would seem, would require more changes to the language than just disallowing reading/writing `shared` members.

>> module expertcode;
>>
>> @safe:
>>
>> struct FileHandle {
>>      @safe:
>>
>>      void[] read(void[] storage) shared;
>>      void[] write(const(void)[] buffer) shared;
>> }
>>
>> FileHandle openFile(string path);
>> // only the owner can close
>> void closeFile(ref FileHandle);
>>
>> void shareWithThreads(shared FileHandle*); // i.e. generate a
>> number of jobs in some queue
>> void waitForThreads();                     // waits until all
>> processing is done
>>
>> module usercode;
>>
>> import expertcode;
>>
>> void processHugeFile(string path) {
>>      FileHandle file = openFile(path);
>>      shareWithThreads(&file);    // implicit cast
>>      waitForThreads();
>>      file.closeFile();
>> }
>
> This is a very strange program...

Why? That's literally the purpose of being able to `share`: you create/acquire a resource, share it, but keep a non-`shared` reference to yourself. If that's not required, you'd just create the data `shared` to begin with.

> I'm dubious it is in fact "expertcode"... but let's look into it.

You're fixating on it being file now. I give an abstract example, you dismiss it as contrived, I give a concrete one, you want to dismiss it as "strange".

Heh, replace 'FileHandle' with 'BackBuffer', 'openFile' with 'acquireBackBuffer', 'shareWithThreads' with 'generateDrawCommands', 'waitForThreads' with 'gatherCommandsAndDraw', 'closeFile' with 'postProcessAndPresent' ;)

> File handle seems to have just 2 methods... and they are both threadsafe. Open and Close are free-functions.

It doesn't matter if they're free functions or not. What matters is signature: they're taking non-`shared` (i.e. 'owned') reference. Methods are free functions in disguise.

> Close does not promise threadsafety itself (but of course, it doesn't violate read/write's promise, or the program is invalid).

Yep, and that's the issue. It SHALL NOT violate threadsafety, but it can't promise such in any way :(

> I expect the only possible way to achieve this is by an internal mutex to make sure read/write/close calls are serialised.

With that particular interface, yes.

> read and write will appropriately check their file-open state each time they perform their actions.

Why? The only purpose of giving someone a `shared` reference is to give a reference to an open file. `shared` references can't do anything with the file but read and write, they would expect to be able to do so.

> What read/write do in the case of being called on a closed file... anyones guess? I'm gonna say they do no-op... they return a null pointer to indicate the error state.
>
> Looking at the meat of the program; you open a file, and distribute it to do accesses (I presume?)....

> Naturally, this is a really weird thing to do, because even if the API is threadsafe such that it doesn't crash and reads/writes are
> serialised, the sequencing of reads/writes will be random, so I don't believe any sane person (let alone an expert) would write this
> program... but moving on.

Um, that's literally what std.stdio does, for writes at least, except it doesn't advertise `File` as `shared`. That's how we get interleaved, but not corrupted, output even when writing from multiple threads. Now, that's not *universally* useful, but nonetheless that's a valid use case.

> Then you wait for them to finish, and close the file.
> Fine. You have a file with randomly interleaved data... for whatever reason.

Or I have command lists, or images loaded in background...

> This program does appear to be safe (assuming that the implementations aren't invalid), but a very strange program nonetheless.
>
>> Remove the call to `waitForThreads()` (assume user just forgot
>> that, i.e. the "accident"). Nothing would change for the
>> compiler: all calls remain @safe.
>
> Yup.
>
>> And yet, if we're lucky, we get
>> a consistent instacrash. If we're unlucky, we get memory
>> corruption, or an unsolicited write to another currently open
>> file, either of which can go unnoticed for some time.

> Woah! Now this is way off-piste..
> Why would get a crash? Why would get memory corruption? None of those things make sense.

Because the whole reason to have `shared` is to avoid the extraneous checks that you mentioned above, and only write actual useful code (i.e. lock-write-unlock, or read-put-to-queue-repeat, or whatever), not busy-work (testing if the file is open on every call). If you have a `shared` reference, it better be to existing data. If it isn't, the program is invalid already: you've shared something that doesn't "exist" (good for marketing, not so good for multithreading). That's why having `shared` and un-`shared` references to the same data simultaneously is not safe: you can't guarantee in any way that the owning thread doesn't invalidate the data through it's non-`shared` reference while you're doing your threadsafe `shared` work; you can only "promise" that by convention (documentation).

> So, you call closeFile immediately and read/write start returning null.

And I have partially-read or partially-written data. Or Maybe I call closeFile(), main thread continues and opens another file, which gives the same file descriptor, `shared` references to FileHandle which the user forgot to wait on continue to work oblivious to the fact that it's a different file now. It's a horrible, but still @safe, implementation of FileHandle, yes, but the caller (user) doesn't know that, and can't know that just from the interface. The only advice against that is "don't do that", but that's irrespective of your proposal.

> I'm going to assume that `shareWithThreads()` was implemented  by an
> 'expert' who checked the function results for errors. It was detected that the reads/write failed, and an error "failed to read file" was emit, then the function returned promptly.
> The uncertainty of what happens in this program is however
> `shareWithThreads()` handles read/write emitting an error.

But you can only find out about these errors in `waitForThreads`, the very call that the user "forgot" to make!

>>> Of course the program becomes invalid if you do that, there's
>> no question about it, this goes for all buggy code.
>
> In this case, I wouldn't say the program becomes 'invalid'; it is
> valid for filesystem functions to return error states and you should handle them.
> In this case, read/write must return some "file not open" state, and it should be handled properly.
> This problem has nothing to do with threadsafety. It's a logic issue related to threading, but that's got nothing to do with this.

There's no question about it, it *is* a logic error. The point is, it's a logic error that ultimately can lead to UB despite being @safe. Just like this is: https://issues.dlang.org/show_bug.cgi?id=19316.

>> The problem is,
>> definition of "valid" lies beyond the type system: it's an
>> agreement between different parts of code, i.e. between expert
>> programmers who wrote FileHandle et al., and users who write
>> processHugeFile(). The main issue is that certain *runtime*
>> conditions can still violate @safe-ty.
>
> Perhaps you don't understand what @safe-ty means? It's a compiler assertion that the code is memory-safe. It's not a magic attribute that tells you that your program is right.

I know.

> Runtime conditions being in a valid state is a high-level problem for the program, and doesn't interacts with threadsafety in any
> fundamental way, and not in any way that @safe has anything to do with.

Yep.

> You're just describing normal high-level multi-threading logic
> problems. `shared` does not and can not help you with that; you need to look to libraries that offer threading support frameworks for that.
> It can help you not write code that does invalid access to memory and crash. That's the extent of its charter.

I understand that. So... it would seem that your proposal focuses more on @safe than on threadsafety?

> If a `shared` API is designed well, it can also offer strong implicit advice about how to correctly interact with API's. The compiler will coerce you to do the right things with error messages.

>> Your proposal makes the language more strict wrt. to writing
>> @safe 'expertmodule', thanks to disallowing reads and writes
>> through `shared`, which is great.
>> However the implicit conversion to `shared` doesn't in any way
>> improve the situation as far as user code is concerned, unless
>> I'm still missing something.

> It does, it eliminates unsafe user interactions. It must be that way to be safe. There were no casts above, it's great! And your program is safe!
> (although it's wrong)

It's @safe, but it's wrong because it's not threadsafe. Yay! :D

> FWIW, I doubt anybody in their right mind would attempt to write a threadsafe filesystem API this way.

std.stdio ;) (yes, I know there's no `shared` there, but that's what it does).

> Any such API would be structured COMPLETELY differently; it would likely have one `shared` method that would accept requests for deferred fulfillment, and handle unique objects associated with each request.

Perhaps. How would the user know that?
October 21, 2018
On Sunday, 21 October 2018 at 11:25:16 UTC, aliak wrote:
> On Saturday, 20 October 2018 at 16:41:41 UTC, Stanislav Blinov wrote:
>> Those are not "ok". They're only "ok" under Manu's proposal so long as the author of C promises (via documentation) that that's indeed "ok". There can be no statically-enforced guarantees that those calls are "ok", or that issuing them in that order is "ok". Yet Manu keeps insisting that somehow there is.
>
> No he is not insisting you can statically enforce thread safety.

I stand corrected, it would seem so.

> When I say ok, I mean assuming the implementer actually wrote correct code. This applies to any shared method today as well.

This ("ok") can only be achieved if the "implementor" (the "expert") writes every function self-contained, at which point sharing something from user code becomes a non-issue (i.e. it becomes unnecessary). But that's not a very useful API. As soon as you have more than one function operating on the same data, the onus is on the user (the caller) to call those functions in correct order, or, more generally, without invalidating the state of shared data.
October 21, 2018
On Sunday, 21 October 2018 at 09:50:09 UTC, Walter Bright wrote:
> On 10/20/2018 11:24 AM, Manu wrote:
>> This is an unfair dismissal.
>
> It has nothing at all to do with fairness. It is about what the type system guarantees in @safe code. To repeat, the current type system guarantees in @safe code that T* and shared(T)* do not point to the same memory location.
>
> Does your proposal maintain that or not? It's a binary question.

No. Instead, it proposes something more useful: once cast to shared(T)*, only thread-safe operations may be performed on it.


> > int* a;
> > shared(int)* b = a;
>
> This is not safe.

Under MP, this is perfectly safe - you can do nothing with a shared(int)*, except call un-@safe, non-thread-safe functions on it, which will *fail to compile* under @safe.


> ---- Manu's Proposal ---
> @safe:
> int i;
> int* a = &i;
> StartNewThread(a); // Compiles! Coder has no idea!
>
> ... in the new thread ...
> void StartOfNewThread(shared(int)* b) {
>
>     ... we have two threads accessing 'i',
>     one thinks it is shared, the other unshared,
>     and StartOfNewThread() has no idea and anyone
>     writing code for StartOfNewThread() has no way
>     to know anything is wrong ...
>
>     lockedIncrement(b);  // Data Race!
> }

Someone's messed up if they've marked lockedIncrement @safe - under MP, it shouldn't be. lockedIncrement is a very low-level piece of functionality, and should be @system. It also shouldn't take a shared(int)*, but a int*, forcing an unsafe cast and making it obvious the code is un@safe.

--
  Simen
October 21, 2018
On Sunday, 21 October 2018 at 09:58:18 UTC, Walter Bright wrote:
> On 10/20/2018 11:08 AM, Nicholas Wilson wrote:
>> You can if no-one else writes to it, which is the whole point of Manu's proposal. Perhaps it should be const shared instead of shared but still.
>
> There is no purpose whatsoever to data that can be neither read nor written. Shared data is only useful if, at some point, it is read/written, presumably by casting it to unshared in @trusted code. As soon as that is done, you've got a data race with the other existing unshared aliases.

No, because every part of the public interface has to work together to ensure thread-safety.

This code is invalid (but compiles) under MP:

module A;
struct S {
    private int n;
    void foo() @safe {
        n--; // Not thread-safe
    }
    void bar() shared @trusted {
        atomicOp!"++"(n.assumeUnshared);
    }
}

module B;
import A;
void passToOtherThread(shared(S)*); // Calls S.bar()

void main() {
    S* s = new S();
    passToOtherThread(s);
    s.foo();
}

The reason: foo() breaks bar()s promise of thread-safety. This means that S does not provide a thread-safe interface. It would be nice if the compiler could statically notice that, but I don't see how that'd work.

Now, for a thread-safe version:

module A;
struct S {
    int n;
    void foo() @trusted {
        atomicOp!"--"(n); // Thread-safe
    }
    void bar() shared @trusted {
        atomicOp!"++"(n.assumeUnshared);
    }
}

module B;
import A;
void passToOtherThread(shared(S)*); // Calls S.bar()

void main() {
    S* s = new S();
    passToOtherThread(s);
    s.foo();
}

In this case, passToOtherThread is free to call S.bar as often as it may feel like, since atomic operations are used in every possible access to S.n. This is true even though one thread has unshared access and other threads have shared access.

--
  Simen
October 21, 2018
On Sunday, 21 October 2018 at 12:45:43 UTC, Stanislav Blinov wrote:
> On Sunday, 21 October 2018 at 05:47:14 UTC, Manu wrote:
>> On Sat, Oct 20, 2018 at 10:10 AM Stanislav Blinov via Digitalmars-d <digitalmars-d@puremagic.com> wrote:
>
>>> Synchronized with what? You still have `a`, which isn't `shared` and doesn't require any atomic access or synchronization. At this point it doesn't matter if it's an int or a struct. As soon as you share `a`, you can't just pretend that reading or writing `a` is safe.
>
>> `b` can't read or write `a`... accessing `a` is absolutely safe.
>
> It's not, with or without your proposal. The purpose of sharing `a` into `b` is to allow someone to access `*a` in a threadsafe way (but un-@safe, as it *will* require casting away `shared` from `b`). That is what's making keeping an unshared reference `a` un-@safe: whoever accesses `*a` in their @trusted implementations via `*b` can't know that `*a` is being (@safe-ly!) accessed in a non-threadsafe way at the same time.

Then someone has not done their job. Since the pieces of code that will actually use the un-@safe building blocks at the bottom are few and far between, it is reasonable to assume that an expert will be writing this code, and that such code be placed in a separate module where all access to the shared type is controlled.

It seems you expect regular users to have calls to atomicOp!"++" scattered all over their code. I find this an unreasonable expectation, and fully agree that this will lead to problems.


>> Someone must do something unsafe to undermine your threadsafety... and
>> if you write unsafe code and don't know what you're doing, there's
>> nothing that can help you.
>
> Ergo, it follows that anyone that is making an implicit cast from mutable to shared better know what they're doing, which mere mortal users (not "experts") might not. I.e. it's a way to giving a loaded gun to someone who never held a weapon before.

No.


>> Close does not promise threadsafety itself (but of course, it doesn't violate read/write's promise, or the program is invalid).
>
> Yep, and that's the issue. It SHALL NOT violate threadsafety, but it can't promise such in any way :(

Can you demonstrate any system that can promise something like that? (apart from all-immutable)


>> read and write will appropriately check their file-open state each time they perform their actions.
>
> Why? The only purpose of giving someone a `shared` reference is to give a reference to an open file. `shared` references can't do anything with the file but read and write, they would expect to be able to do so.

Because otherwise it's not thread-safe. Exactly as you point out, the owner could call closeFile before some other thread was finished writing. If the implementer of FileHandle fails to take this into account, then no, it's not thread-safe.


>> I'm going to assume that `shareWithThreads()` was implemented  by an
>> 'expert' who checked the function results for errors. It was detected that the reads/write failed, and an error "failed to read file" was emit, then the function returned promptly.
>> The uncertainty of what happens in this program is however
>> `shareWithThreads()` handles read/write emitting an error.
>
> But you can only find out about these errors in `waitForThreads`, the very call that the user "forgot" to make!

Of course not. You can throw exceptions, you could add a destructor that reports on these errors, you could set an error flag somewhere and check that every now and then. The fact that you've managed to write a horribly broken API under MP and can't see a way to do better inside that system does not necessarily mean the problem is with MP.

--
  Simen
October 21, 2018
On Sunday, 21 October 2018 at 13:24:49 UTC, Stanislav Blinov wrote:
> On Sunday, 21 October 2018 at 11:25:16 UTC, aliak wrote:
>> When I say ok, I mean assuming the implementer actually wrote correct code. This applies to any shared method today as well.
>
> This ("ok") can only be achieved if the "implementor" (the "expert") writes every function self-contained, at which point sharing something from user code becomes a non-issue (i.e. it becomes unnecessary). But that's not a very useful API. As soon as you have more than one function operating on the same data, the onus is on the user (the caller) to call those functions in correct order, or, more generally, without invalidating the state of shared data.

The onus is *always* on the user to write function calls in the correct order, multi-threading or not. We expect programmers to be able to figure out why this doesn't print 'Hello world!':

void main() {
    import std.stdio;
    string hello;
    writeln(hello);
    hello = "Hello world!";
}

We also expect writeln to be written in such a way that it doesn't corrupt random data or cause life-threatening situations just because hello was uninitialized upon calling writeln, and assigned afterwards. We should expect the same of multi-threaded programs.

This places the onus of writing thread-safe code on the writer of the multi-threaded equivalent of writeln and string.opAssign. Only this way can the user of the library write code and not expect things to blow up in their face.

--
  Simen
October 21, 2018
On Sunday, 21 October 2018 at 09:58:18 UTC, Walter Bright wrote:
> On 10/20/2018 11:08 AM, Nicholas Wilson wrote:
>> You can if no-one else writes to it, which is the whole point of Manu's proposal. Perhaps it should be const shared instead of shared but still.
>
> There is no purpose whatsoever to data that can be neither read nor written.

Indeed but there is a subtle difference between that and Manu's proposal: access through the shared variable may not have non-atomic reads, not no reads.

> Shared data is only useful if, at some point, it is read/written,

Yes

> presumably by casting it to unshared in @trusted code.

That is one way to do it, others include atomics  and other @trusted primitives

> As soon as that is done, you've got a data race with the other existing unshared aliases.

You're in @trusted code, that is the whole point. The onus is on the programmer to make that correct, same with regular @safe/@trusted@system code.

October 21, 2018
On Sat, 20 Oct 2018 22:47:14 -0700, Manu wrote:
> Looking at the meat of the program; you open a file, and distribute it
> to do accesses (I presume?)....
> Naturally, this is a really weird thing to do, because even if the API
> is threadsafe such that it doesn't crash and reads/writes are
> serialised, the sequencing of reads/writes will be random, so I don't
> believe any sane person (let alone an expert) would write this
> program... but moving on.
> Then you wait for them to finish, and close the file.
> 
> Fine. You have a file with randomly interleaved data... for whatever reason.

I'd expect almost every nontrivial multithreaded program to do this. It's called a log file.

You don't need to read data pointed at by a log file, but you do need to read the FILE* or the file descriptor.

Database-like things using a journal file might need a shared file for both reading and writing.

> So, you call closeFile immediately and read/write start returning null.

You start threads. You give them access to the log file. You wait for the threads to exit. Then you close the file.
October 21, 2018
On Sun., 21 Oct. 2018, 2:55 am Walter Bright via Digitalmars-d, <digitalmars-d@puremagic.com> wrote:
>
> On 10/20/2018 11:24 AM, Manu wrote:
> > This is an unfair dismissal.
>
> It has nothing at all to do with fairness. It is about what the type system guarantees in @safe code. To repeat, the current type system guarantees in @safe code that T* and shared(T)* do not point to the same memory location.
>
> Does your proposal maintain that or not? It's a binary question.

By the definition Nick pulled from Wikipedia and posted for you a few posts back, yes, my proposal satisfies Wikipedia's definition of no aliasing. I understand that property is critical, and I have carefully designed for it.

> > I'm not sure you've understood the proposal.
> > This is the reason for the implicit conversion. It provides safe
> > transition.
>
> I don't see any way to make an implicit T* to shared(T)* safe, or vice versa. The T* code can create more aliases that the conversion doesn't know about, and the shared(T)* code can hand out aliases to other threads. So it all falls to pieces.

T* can't make additional T* aliases on other threads; there can only
be one thread with T*.
shared(T)* can not make a T*.
shared(T)* has no read or write access, so it's not an alias of T* by
Wikipedia's definition.

Only threadsafe functions can do anything to T.
The leap of faith is; some @trusted utility functions at the bottom of
the shared stack makes a promise that it is threadsafe, and must
deliver that promise.
I don't think this is unreasonable; this is the nature of @trusted
functions, they make a promise, and they must keep it.
If the trusted function does not lie, then the chain of trust holds
upwards through the stack.

The are very few such trusted functions in practise. Like, similar to the number of digits you have.

> Using a 'scope' qualifier won't work, because 'scope' isn't transitive, while shared is, i.e. U** and shared(U*)*.

I don't think I depend on scope in any way.
That was an earlier revision of thinking in an older thread.

>  > I'm not sure how to clarify it, what can I give you?
>
> Write a piece of code that does such an implicit conversion that you argue is @safe. Make the code as small as possible. Your example:
>
>  > int* a;
>  > shared(int)* b = a;
>
> This is not safe.
>
> ---- Manu's Proposal ---
> @safe:
> int i;
> int* a = &i;
> StartNewThread(a); // Compiles! Coder has no idea!
>
> ... in the new thread ...
> void StartOfNewThread(shared(int)* b) {
>
>      ... we have two threads accessing 'i',
>      one thinks it is shared, the other unshared,
>      and StartOfNewThread() has no idea and anyone
>      writing code for StartOfNewThread() has no way
>      to know anything is wrong ...
>
>      lockedIncrement(b);  // Data Race!
> }

This program doesn't compile. You receive an error because it is not safe.
The function is `lockedIncrement(int*)`. It can't receive a shared
argument; the function is not threadsafe by my definition.
You have written a program that produces the expected error that
alerts you that you have tried to do un-@safe and make a race.

Stanislav produced this same program, and I responded with the correct
program a few posts back.
I'll repeat it here; the @safe program to model this interaction is:

@safe:

// function is NOT threadsafe by my definition, can not be called on
shared arguments
void atomicIncrement(int*);

struct Atomic(T)
{
  // encapsulare the unsafe data so it's inaccessible by any unsafe means
  private T val;

  // perform the unsafe cast in a trusted function
  // we are able to assure a valid calling context by encapsulating
the data above
  void opUnary(string op : "++")() shared @trusted {
atomicIncrement(cast(T*)&val); }
}

Atomic!int i;
Atomic!int* a = &i;
StartNewThread(a); // Compiles, of course!
++i; // no race

... in the new thread ...
void StartOfNewThread(shared(Atomic!int)* b) {
  ... we have two threads accessing 'i', one has thread-local access,
this one has a restricted shared access.
  here, we have a shared instance, so we can only access `b` via
threadsafe functions.
  as such, we can manipulate `b` without fear.
  ++i; // no race!
}


> Your proposal means that the person writing the lockedIncrement(), which is a perfectly reasonable thing to do, simply cannot write it in a way that has a @safe interface

Correct, the rules of my proposal apply to lockedIncrement(). They
apply to `shared` generally.
lockedIncrement() is not a threadsafe function. You can't call it on a
shared instance, because `int`s API (ie, all intrinsic operations) are
not threadsafe.
lockedIncrement() can't promise threadsafe access to `shared(int)*`,
so the argument is not shared.

Your program made the correct compile error about doing unsafety, but
the location of the compile error is different under my proposal;
complexity is worn by the shared library author, rather than every
calling user ever.
I think my proposal places the complexity in the right location.
`shared` is intrinsically dangerous; it's not reasonable to ask every
user that ever calls a shared API to write unsafe code when when
calling. That's just plain bad design.

> because the person writing the lockedIncrement() library
> function has no way to know that the data it receives is actually unshared data.

The author of `shared` tooling must assure a valid context such that
its threadsafety promises are true. Atomic(T) does that with a private
member.
The @safe way to interact with atomics is to use the Atomic utility
type I showed above.
That is one such @trusted tool that I talk about as being "at the
bottom of the stack".
It is probably joined by a mutex/semaphore, and some
containers/queues. That is probably all the things, and other things
would be @safe compositions of those tools.

> I.e. @trusted code is obliged to proved a safe interface. Your proposal makes that impossible because the compiler would allow unshared data to be implicitly typed as shared.

What? No.
Please, try and understand my proposal...