On Wed, Oct 17, 2018 at 12:35 PM Steven Schveighoffer via Digitalmars-d <digitalmars-d@puremagic.com> wrote: > > On 10/17/18 2:46 PM, Manu wrote: > > On Wed, Oct 17, 2018 at 10:30 AM Steven Schveighoffer via > > >> What the example demonstrates is that while you are trying to disallow > >> implicit casting of a shared pointer to an unshared pointer, you have > >> inadvertently allowed it by leaving behind an unshared pointer that is > >> the same thing. > > > > This doesn't make sense... you're showing a thread-local program. > > The thread owning the unshared pointer is entitled to the unshared > > pointer. It can make as many copies at it likes. They are all > > thread-local. > > It's assumed that shared int pointer can be passed to another thread, > right? Do I have to write a full program to demonstrate?
And that shared(int)* provides no access. No other thread with that pointer can do anything with it. > > There's only one owning thread, and you can't violate that without unsafe > > casts. > > The what is the point of shared? Like why would you share data that > NOBODY CAN USE? You can call shared methods. They promise threadsafety. That's a small subset of the program, but that's natural; only a very small subset of the program is safe to be called from a shared context. In addition, traditional unsafe interactions which may involve acquiring locks and doing casts remain exactly the same, and the exact same design patterns must apply which assure that the object is handled correctly. I'm not suggesting any changes that affect that workflow. > At SOME POINT, shared data needs to be readable and writable. Any > correct system is going to dictate how that works. It's a good start to > make shared data unusable unless you cast. But then to make it > implicitly castable from unshared defeats the whole purpose. No. No casting! This is antiquated workflow.. I'm not trying to take it away from you, but it's not an interesting model for the future. `shared` can model more than just that. You can call threadsafe methods. Shared methods explicitly dictate how the system works, and in a very clear and obvious/intuitive way. The implicit cast makes using threadsafe objects more convenient when you only have one, which is extremely common. > >> In order for a datum to be > >> safely shared, it must be accessed with synchronization or atomics by > >> ALL parties. > > > > ** Absolutely ** > > > >> If you have one party that can simply change it without > >> those, you will get races. > > > > *** THIS IS NOT WHAT I'M PROPOSING *** > > > > I've explained it a few times now, but people aren't reading what I > > actually write, and just assume based on what shared already does that > > they know what I'm suggesting. > > You need to eject all presumptions from your mind, take the rules I > > offer as verbatim, and do thought experiments from there. > > What seems to be a mystery here is how one is to actually manipulate > shared data. If it's not usable as shared data, how does one use it? Call shared methods. It's not like I haven't been saying this in every post since my OP. The only possible thing that you can safely do with a shared object is call a method that has been carefully designed for thread-safe calling. Any other access is invalid under any circumstance. > >> It's true that only one thread will have thread-local access. It's not > >> valid any more than having one mutable alias to immutable data. > > > > And this is why the immutable analogy is invalid. It's like const. > > shared offers restricted access (like const), not a different class of > > thing. > > No, not at all. Somehow one must manipulate shared data. If shared data > cannot be read or written, there is no reason to share it. Call shared methods. > So LOGICALLY, we have to assume, yes there actually IS a way to > manipulate shared data through these very carefully constructed and > guarded things. Yes, call the methods, they were carefully constructed to be threadsafe. Only functions that have made a promise to be threadsafe and implemented that complexity are valid interactions. > > There is one thread with thread-local access, and many threads with > > shared access. > > > > If a shared (threadsafe) method can be defeated by threadlocal access, > > then it's **not threadsafe**, and the program is invalid. > > > > struct NotThreadsafe > > { > > int x; > > void local() > > { > > ++x; // <- invalidates the method below, you violate the other > > function's `shared` promise > > } > > void notThreadsafe() shared > > { > > atomicIncrement(&x); > > } > > } > > So the above program is invalid. Is it compilable with your added > allowance of implicit casting to shared? If it's not compilable, why > not? All my examples assume my implicit conversion rule. But regardless, under my proposal, the above program is invalid. This violates the threadsafe promise. > If it is compilable, how in the hell does your proposal help > anything? I get the exact behavior today without any changes (except > today, I need to explicitly cast, which puts the onus on me). My proposal doesn't help this program, it's invalid. I'm just demonstrating what an invalid program looks like. > > struct Atomic(T) > > { > > void opUnary(string op : "++")() shared { atomicIncrement(&val); } > > private T val; > > } > > struct Threadsafe > > { > > Atomic!int x; > > void local() > > { > > ++x; > > } > > void threadsafe() shared > > { > > ++x; > > } > > } > > > > Naturally, local() is redundant, and it's perfectly fine for a > > thread-local to call threadsafe() via implicit conversion. > > In this case, yes. But that's not because of anything the compiler can > prove. The compiler can't 'prove' anything at all related to threadsafety. We need to make one assumption; that `shared` methods are expected to be threadsafe, and from there the compiler can prove correctness with respect to that assumption. In this case, `Atomic.opUnary("++")` promises that it's threadsafe, and as such, the aggregate can safely use that tool to implement higher-level logic. > How does Atomic work? I thought shared data was not usable? I'm being > pedantic because every time I say "well at some point you must be able > to modify things", you explode. Atomic implements a safe utility using unsafe primitives (atomic increment intrinsic). Atomic wraps the unsafe call to an intrinsic into a box that's safe, and can be used by clients. In my worldview, atomic is at the bottom of the chain-of-trust. It's effectively a @trusted implementation of a foundational tool. Almost every low level tool is of this nature. > Complete the sentence: "In order to read or write shared data, you have > to ..." Call a shared method, or at the bottom of the stack, you need to do unsafe (@trusted?) implementations of the foundational machinery (possibly using casts), and package into boxes that are safe to interact with and build out from. > > Here's another one, where only a subset of the object is modeled to be > > threadsafe (this is particularly interesting to me): > > > > struct Threadsafe > > { > > int x; > > Atomic!int y; > > > > void notThreadsafe() > > { > > ++x; > > ++y; > > } > > void threadsafe() shared > > { > > ++y; > > } > > } > > > > In these examples, the thread-local function *does not* undermine the > > threadsafety of threadsafe(), it MUST NOT undermine the threadsafety > > of threadsafe(), or else threadsafe() **IS NOT THREADSAFE**. > > In the second example, you can see how it's possible and useful to do > > thread-local work without invalidating the objects threadsafety > > commitments. > > > > > > I've said this a bunch of times, there are 2 rules: > > 1. shared inhibits read and write access to members > > 2. `shared` methods must be threadsafe > > > >>From there, shared becomes interesting and useful. > > > > Given rule 1, how does Atomic!int actually work, if it can't read or > write shared members? It's an intrinsic. You use it the same as malloc() or free(). It's a piece of low-level mechanical tooling which you use in an unsafe way, but you then wrap it in a layer that introduces the type-safety. malloc() returns a void*, which you cast to the intended type, and then perform construction. You can't implement a typesafe new without malloc() at the bottom of the stack. You need to raise your vision one-level higher to users of Atomic to see interesting interactions. > For rule 2, how does the compiler actually prove this? > > Any programming by convention, we can do today. We can implement > Atomic!int with the current compiler, using unsafe casts inside @trusted > blocks. It can't. I don't know what can be done to mechanically enforce this requirement, but I would suggest that it's a goal to work towards in the future with any technology possible. In the meantime though, if we accept that the user writing a threadsafe tool is responsible for delivering on their promise, then the system that emerges is widely useful. The higher-level becomes generally interesting, the low-level will remain to be implemented by experts, and is no change from the situation today. The low level doesn't really care much about type-safety, it's the high-level I'm interested in. We can tell a MUCH better story about how users can interact with shared machinery, and we should.