October 12, 2019
On Sat, Oct 12, 2019 at 3:35 PM Ola Fosheim Grøstad via Digitalmars-d <digitalmars-d@puremagic.com> wrote:
>
> On Saturday, 12 October 2019 at 21:28:36 UTC, Jonathan M Davis wrote:
> > The fact that we have thread-local in the language requires that the type system then have the concept of shared. So, there should be no question about that.
>
> Well, my understanding from what you wrote is that the shared marker is 100% syntactical and has no semantic implications.

The semantic implication is that there is no read or write access... just the same as how const says there is no write access.

> In that case it can be done as a library meta programming construct
> (type wrapping).

We can't define type constructors in a library sadly. You could apply the same argument to const.

And aside from that, it belongs in the language due to the simple fact
that it defines D's thread-local by default.
If you think about it, it's not really the definition of thread-local
by default that leads to shared... it's actually that shared existing
in the language defines thread-local by default.
If there was no shared, then there couldn't possibly be thread-local
by default, as D is specified. shared is fundamental to D, it's just
that it's semantics have been poorly understood because it's
historically had relatively little use.

> You just need a way to restrict application of
> the constructs  based on code structure (@safe). So one could
> establish a syntactical solution that is perfectly suitable for
> meta programming?
>
> But you could probably do better by introducing an effectsbased typesystem.
>
> Anyway, based on what you said shared does not have to be in the language and could be replaced by more expressive metaprogramming mechanisms. I think.

It's like I said above; shared MUST be in the language, because it defines one of the fundamental language semantics; thread-local by default.

October 12, 2019
On Sat, Oct 12, 2019 at 4:40 PM IGotD- via Digitalmars-d <digitalmars-d@puremagic.com> wrote:
>
> On Saturday, 12 October 2019 at 21:28:36 UTC, Jonathan M Davis wrote:
> >
> > Right now, we basically have #2. What this DIP is trying to do is move us to #1. How libraries should work is exactly the same in either case. It's just that with #1, the places where you operate on shared data in a manner which isn't guaranteed to be atomic, the compiler prevents you from doing it unless you  use core.atomic or have @system code with casts. Even if we have #2 and thus no such compiler errors, the code should still have been doing what #1 would have required, since if it doesn't, then it isn't thread-safe.
>
> With this DIP, shared integers/small types will be automatically atomic.

That's not what this DIP says, as had been clarified by Walter a number of times now.

> For complex/large types, will you still be able to use
> them as before between threads and you have protect the type
> yourself at least for a transitional period?
>
> "Atomic" here as I get it also mean atomically updating complex types. This usually means that you need to guard the operations with some kind of mutex. The compiler can of course detect this and issue a warning/error to the user which doesn't seem to be the scope of this DIP.
>
> Correct me if I'm wrong but we have the following scenarios.
> 1. shared integer/simple type (size dependent?) -> automatically
> HW atomic operations

This is the misunderstanding through most of this thread.

> 2. shared complex type -> write to any member must be protected
> with a mutex.
> 3. shared complex type -> read to any member must be protected
> with a mutex or read/write mutex allowing multiple reads.
>
> The compiler is used the detect these scenarios so that the user doesn't forget protecting the shared types.
>
> As I get it this DIP is just a baby step towards this bigger scope for the shared keyword.

Useful libraries will follow.
October 13, 2019
On Sunday, 13 October 2019 at 00:13:12 UTC, Manu wrote:
> On Sat, Oct 12, 2019 at 3:35 PM Ola Fosheim Grøstad via The semantic implication is that there is no read or write access... just the same as how const says there is no write access.

Well, I understand what you mean, but if you can achieve the same effect by hiding, then it is syntactical in nature.

That would be difficult with const in D ( might work in other languages ).

> It's like I said above; shared MUST be in the language, because it defines one of the fundamental language semantics; thread-local by default.

Does the shared marker have any effect on generated code? If not, then it is just a way to bypass (break) the type system on a syntactical level.
October 12, 2019
On Sat, Oct 12, 2019 at 8:45 PM Ola Fosheim Grøstad via Digitalmars-d <digitalmars-d@puremagic.com> wrote:
>
> On Sunday, 13 October 2019 at 00:13:12 UTC, Manu wrote:
> > On Sat, Oct 12, 2019 at 3:35 PM Ola Fosheim Grøstad via The semantic implication is that there is no read or write access... just the same as how const says there is no write access.
>
> Well, I understand what you mean, but if you can achieve the same effect by hiding, then it is syntactical in nature.

We can't though.

> That would be difficult with const in D ( might work in other
> languages ).
>
> > It's like I said above; shared MUST be in the language, because it defines one of the fundamental language semantics; thread-local by default.
>
> Does the shared marker have any effect on generated code?

Not at this time. And I'm not sure what your point is... does const have any effect in generated code? (it does not)

> If not,
> then it is just a way to bypass (break) the type system on a
> syntactical level.

I don't understand this.. how is it bypassing/breaking the type system?

October 12, 2019
On Saturday, October 12, 2019 5:37:50 PM MDT IGotD- via Digitalmars-d wrote:
> On Saturday, 12 October 2019 at 21:28:36 UTC, Jonathan M Davis
>
> wrote:
> > Right now, we basically have #2. What this DIP is trying to do is move us to #1. How libraries should work is exactly the same in either case. It's just that with #1, the places where you operate on shared data in a manner which isn't guaranteed to be atomic, the compiler prevents you from doing it unless you  use core.atomic or have @system code with casts. Even if we have #2 and thus no such compiler errors, the code should still have been doing what #1 would have required, since if it doesn't, then it isn't thread-safe.
>
> With this DIP, shared integers/small types will be automatically atomic. For complex/large types, will you still be able to use them as before between threads and you have protect the type yourself at least for a transitional period?
>
> "Atomic" here as I get it also mean atomically updating complex types. This usually means that you need to guard the operations with some kind of mutex. The compiler can of course detect this and issue a warning/error to the user which doesn't seem to be the scope of this DIP.
>
> Correct me if I'm wrong but we have the following scenarios.
> 1. shared integer/simple type (size dependent?) -> automatically
> HW atomic operations
> 2. shared complex type -> write to any member must be protected
> with a mutex.
> 3. shared complex type -> read to any member must be protected
> with a mutex or read/write mutex allowing multiple reads.
>
> The compiler is used the detect these scenarios so that the user doesn't forget protecting the shared types.
>
> As I get it this DIP is just a baby step towards this bigger scope for the shared keyword.

When we're talking about atomic with regards to shared, we're talking about what core.atomic does. They're operations that are atomic with regards to CPU instructions. The most that that can work with is primitive types like integers or pointers. More complex types require stuff like mutexes to protect them in order to freely mutate them.

Walter needs to make the DIP clearer, but in the discussions in this thread, he's made it clear that the intention is that read/write operations will become illegal for all shared data, forcing code to either use core.atomic to do atomic operations on the data or to use synchronization mechanisms such as mutexes to allow for thread-safe reading and writing of data, with the code needing to cast away shared in order to operate on the data (meaning that the code will then be @system or @trusted).

In principle, the compiler could allow reading and writing shared variables by inserting the core.atomic stuff for you, but that's not the current plan. Either way, it's very difficult to have the compiler understand synchronization primitives well enough and have enough guarantees about what the code is doing to be able to do something like implicitly remove shared for you. So, it's highly unlikely that we'll ever get much in the language that would be able to implicitly remove shared for even simple pieces of code let alone complex types. AFAIK, only construct along those lines that's been proposed thus far that could work is TDPL's synchronized classes, and they can only implicitly remove a single layer of shared - and that can only do that much because of how restrictive they are. Having the compiler magically handle thread synchronization primitives for you is likely a pipe dream.

Rather, what's likely going to tend to happen is that complex objects that are supposed to be used as shared will handle the appropriate atomics or mutexes internally, providing an @safe API for the user. However, the internals will still have to do the dirty stuff and be appropriately vetted for thread-safety. That's already what you typically get in a language like C++. It's just that the type system doesn't have shared, so you have to manage everything yourself and don't have a way to segregate shared data and the functions operating on it other than by convention, whereas D's shared enforces it via the type system.

Ultimately, shared is about segregating the code that operates on shared data - both so that most code can just be thread-local without worrying about it and so that you can easily determine which pieces of the program have to be examined for threading issues - just like @system/@trusted/@safe isolates the portions of the program where you potentially have to worry about memory safety. shared isn't about magically handling threading stuff for you. If we can figure out how to add mechanisms on top of shared which make things easier, then great, but shared has to be properly locked down first, and given that D is a systems language (thus allowing all kinds of crazy stuff), and its type system really has no concept of ownership, I don't think that it's very likely that we're going to be able to add much to the language that's going to allow shared to be implicitly removed or allow you to otherwise operate on shared data without worrying about dealing with the synchronization primitives yourself.

- Jonathan M Davis



October 12, 2019
On Saturday, October 12, 2019 9:41:00 PM MDT Ola Fosheim Grøstad via Digitalmars-d wrote:
> > It's like I said above; shared MUST be in the language, because it defines one of the fundamental language semantics; thread-local by default.
>
> Does the shared marker have any effect on generated code? If not, then it is just a way to bypass (break) the type system on a syntactical level.

The compiler is free to assume that anything that isn't shared is thread-local and optimize code accordingly. Also, it _does_ matter on some level with how memory is laid out, because thread-local storage gets used (IIRC, there were problems on older versions of OS X, because we had to fake the TLS, because OS X didn't provide proper TLS). Module-level and static variables which are shared only get initialized once, whereas module-level and static variables which are thread-local get initialized once per thread, because each thread gets their own copy. So, while the vast majority of D objects are thread-local, the distinction between what's thread-local and what's shared is very much built into how D functions.

That's part of why it's so risky to cast away shared, and why it needs to be @system. If the programmer screws up that code and allows any shared data to be treated as thread-local when any other thread could be operating on that data, then not only does that incur all of the typical issues that come with screwing up synchronizing data access, but the compiler could make things worse, because it generated code based on the assumption that the data being operated on was thread-local.

Fortunately, as long as shared disallows non-atomic, read/write operations the code that needs to be examined for threading issues is restricted to @system/@trusted code that involves shared, so the amount of code that the programmer has to examine and verify to ensure that shared data doesn't end up being treated as thread-local when multiple threads can operate on it is limited and should be fairly easy to find.

- Jonathan M Davis




October 13, 2019
On Sunday, 13 October 2019 at 03:49:17 UTC, Manu wrote:
> On Sat, Oct 12, 2019 at 8:45 PM Ola Fosheim Grøstad via Digitalmars-d <digitalmars-d@puremagic.com> wrote:
>> Well, I understand what you mean, but if you can achieve the same effect by hiding, then it is syntactical in nature.
>
> We can't though.

Assume that symbols prefixed by @trusted_ are only visible in @trusted code. Then you can define @trusted_unwrap that returns a reference to the unwrapped type.

Then you have what you need for a simple solution to a library version of shared.


> Not at this time. And I'm not sure what your point is... does const have any effect in generated code? (it does not)

Const could also be done as a library wrapper, but that would require full fledeged meta programming and strong deductive functionality. So not realistic for D.

>
>> If not,
>> then it is just a way to bypass (break) the type system on a
>> syntactical level.
>
> I don't understand this.. how is it bypassing/breaking the type system?

If you need a cast to make use of shared...


October 13, 2019
On Sunday, 13 October 2019 at 04:09:26 UTC, Jonathan M Davis wrote:
> The compiler is free to assume that anything that isn't shared is thread-local and optimize code accordingly.

That has no real effect though, if you throw away shared before using it.

That said, if you had a formalization of the threads and what states different threads are in then you could get some performance benefits by eliding shared overhead when it can be proven that no other threads are accessing a shared variable.

But that is not on the table...

> Also, it _does_ matter on some level with how memory is laid out, because thread-local storage gets used (IIRC, there were problems on older versions of OS X, because we had to fake the TLS, because OS X didn't provide proper TLS).

Actually, how the memory is laid out is not a big deal. Vendors can do this with a library solution too. What is a big deal with TLS is that you need different codegen for obtaining a reference, but that also mean that you cannot throw away that type information before you have a obtained an effective address (reference)?

Same thing with shared. On an architecture that requires a different instruction sequences for shared memory and local memory, you would need to retain that information. Not for allocation, which can be dealt with at the library level, but for accessing it when executing generic functions.

If anything that would be an argument for never being allowed to throw away the shared marker, and also an argument for being able to add more markers (for different types of memory or I/O).


> Module-level and static variables which are shared only get initialized once, whereas module-level and static variables which are thread-local get initialized once per thread, because each thread gets their own copy.

I'd rather say that a TLS variable "value" is conceptually "value[thread_id]" or "value__12345".


> Fortunately, as long as shared disallows non-atomic, read/write operations the code that needs to be examined for threading issues is restricted to @system/@trusted code that involves shared, so the amount of code that the programmer has to examine and verify to ensure that shared data doesn't end up being treated as thread-local when multiple threads can operate on it is limited and should be fairly easy to find.

And you can achieve all that with a library type, because D doesn't actually deal with shared memory, it provides some instructions that results in barriers in the IR.

If you wrap all those barrier-generating instructions in library calls then they can just avoid emitting them if the type they are applied to is not wrapped in a  library provided Shared wrapper.

I don't see why the compiler has to know anything about shared for this to work the same way as described.

Anyway, I think D really needs to think twice about adding more and more narrow features and instead improve on the meta programming capabilities. Clearly many D features could be moved to the library (slicing, dicts etc).

If there is a limited pool of people working on the compiler it would make a lot of sense to put more power into the hands of library authors.  The meta-programming capabilities of C++ is evolving too...

October 13, 2019
On Saturday, 12 October 2019 at 23:36:55 UTC, Manu wrote:
> Here's a rough draft of one such sort of tool I use all the time in shared-intensive code: https://gist.github.com/TurkeyMan/c16db7a0be312e9a0a2083f5f4a6efec

Thanks! That looks quite low level, but now I understand more what you are looking for.

What I had in mind was writing APIs that allows ordinary programmers to do parallell programming safely.

Like taking the single threaded code they have written for processing a single array or merging two arrays with each other and then use a library for speeding it up.


Anyway, my core argument is to put more meta-programming power in hands of library authors like you so that people who have the shoes on can define the semantics they are after. I really don't think this has to be done at the compiler level, given the right meta programming tools, based on the proposed semantics. And I don't think providing those meta programming tools are more work than hardwiring more stuff into the compiler. Clearly that is just my opinion. Others might feel differently.

(Other semantics do require compiler support to work well, like heterogeneous memory architectures, but the proposal does not work with that.)

October 13, 2019
On Sun, Oct 13, 2019 at 12:55 AM Ola Fosheim Grøstad via Digitalmars-d <digitalmars-d@puremagic.com> wrote:
>
> On Sunday, 13 October 2019 at 04:09:26 UTC, Jonathan M Davis wrote:
> > The compiler is free to assume that anything that isn't shared is thread-local and optimize code accordingly.
>
> That has no real effect though, if you throw away shared before using it.
>
> That said, if you had a formalization of the threads and what states different threads are in then you could get some performance benefits by eliding shared overhead when it can be proven that no other threads are accessing a shared variable.

What does that even mean? You're just making things up.
Thread's don't have 'states', that's literally the point of threads!
No code-gen can ever assume any stateful details of any kind about
another thread.
Maybe there's a 10 year research project there, but that idea is so
ridiculous and unlikely to work that nobody would ever try.

I think what you're talking about is something more like a framework;
where different threads are running known code which is configured to
cooperate in particular ways, and implements safe data transmission
between threads.
This is what will exist, there will be elaborate libraries of this
kind. I don't expect many end-users would ever encounter a `shared`
object, it would likely be behind the curtains in all cases, unless
they're writing very low-level systems.

> But that is not on the table...

Definitely not. Not at the language level at least.