On Thursday, September 13, 2018 7:53:49 AM MDT Arafel via Digitalmars-d wrote: > Hi all, > > I know that many (most?) D users don't like using classes or old, > manually controlled, concurrency using "shared" & co., but still, since > they *are* in the language, I think they should at least be usable. > > After having had my share (no pun intended) of problems using shared, > I've finally settled for the following: > > * Encapsulate all the shared stuff in classes (personal preference, > easier to pass around). > * When possible, try to use "shared synchronized" classes, because even > if there are potential losses of performance, the simplicity is often > worth it. This mean that the classed is declared: > > ``` > shared synchronized class A { } > ``` > > and now, the important point: > > * Make all _private non-reference fields_ of shared, synchronized > classes __gshared. > > AIUI the access of those fields is already guaranteed to be safe by the > fact that *all* the methods of the class are already synchronized on > "this", and nothing else can access them. > > Of course, assuming you then don't escape references to them, but I > think that would be a *really* silly thing to do, at least in the most > common case... why on earth are they then private in the first place?. > > Now, the question is, would it make sense to have the compiler do this > for me in a transparent way? i.e. the compiler would automatically store > private fields of shared *and* synchronized classes in the global storage. > > Bonus points if it detects and forbids escaping references to them, > although it could also be enough to warn the user.
Have you read the concurrency chapter in The D Programming Language by Andrei? It sounds like you're trying to describe something vere similar to the synchronized classes from TDPL (which have never been fully implemented in the language). They would make it so that you had a class with shared members but where the outer layer of shared was stripped away inside member functions, because the compiler is able to guarantee that they don't escape (though it can only guarantee that for the outer layer). Every member function is synchronized and no direct access to the member variables outside of the class (even in the same module) is allowed. It would make shared easier to use in those cases where it makes sense to wrapped everything protected by a mutex in a class (though since it can only safely strip away the outer layer of shared, it's more limited than would be nice, and there are plenty of cases where it doesn't make sense to stuff something in a class just to use it as shared). > This way I think there would an easy and sane way of using shared, > because many of its worst quirks (for one, try using a struct like > SysTime that overrides OpAssign, but not for shared objects, as a field) > would be transparently dealt with. The fact that most operations are not allowed with shared is _on purpose_. If anything, too many operations are currently legal. What's really supposed to be happening is that every single operation on a shared object is either guaranteed to be thread-safe, or it's illegal. And if it's illegal, that means that you either need to use atomics to do an operation (since they're thread-safe), or you need to protect the shared object with a mutex and temporarily cast away shared while the mutex is locked so that you can actually do something with the object - and then make sure that no thread-local references exist when the mutex is released. Something like copying a shared object shouldn't even be legal in general. An object that defines opAssign prevents it now, but the fact that it's legal on any type where copying is not guaranteed to be thread-safe is a bug. It's one of those details of shared that has never been fully fleshed out like it should be. Walter and Andrei have been discussing finishing shared, but it hasn't been a high enough priority for it actually get fully sorted out yet. Once it is, unless you're dealing with a type that isn't guaranteed to be thread-safe when copying it, it won't be legal copy it without first casting away shared. Anything less than that would violate what shared is supposed to do. What you should be thinking when dealing with any shared object and whether a particular operation should be allowed is whether that operation is guaranteed to be thread-safe. If the compiler can't guarantee that the operation is thread-safe, then it's not supposed to be legal. The main area that Walter and Andrei haven't agreed upon yet is how much the compiler can or should do to ensure that something is thread-safe rather than just making an operation illegal (e.g. whether memory barriers should be involved). So, _maybe_ some operations will end up as legal thanks to the compiler adding extra code to do something to ensure thread-safety, but in most situations, it's just going to be illegal. So, ultimately, every type is either going to need to be designed such that it simply does not work as shared, or it manages the thread-safety stuff for you. If the object is not designed to be used as shared, then that means that if you want to, you need to protect it with a mutex (be that with synchronized or directly using mutexes) and cast away shared correctly when the object is protected by the mutex. It's annoying, but it prevents thread-safety bugs, and it allows the compiler (and the programmer) to treat the rest of the program as thread-local. On the other hand, if the type is designed to be used as shared (so it actually has shared member functions), then that means that the type itself is going to need to deal with all of the thread-safety stuff internally (be that by using mutexes and casting or using atomics or whatever). If we're going to find ways to make shared require less manual work, it means finding a way to protect a shared object (or group of shared objects) with a mutex in a way that is able to guarantee that when you operate on the data, it's protected by that mutex and that no reference to that data has escaped. TDPL's synchronized classes are one attempt to do that, but the requirement that no references escape (so that shared can safely be cast away) makes it so that only the outer layer of shared can be cast away, and it's extremely difficult to do better than that with having holes such that it isn't actually guaranteed to be thread-safe when shared is cast away. Maybe someone will come up with something that will work, but I wouldn't bet on it. Either way, I don't see how any solution is going to be acceptable which does not actually guarantee thread-safety, because it would be violating the guarantees of shared otherwise. A programmer can choose to cast away shared in an unsafe manner (or use __gshared) and rely on their ability to ensure that the code is thread-safe rather than letting shared do its job, but that's not the sort of thing that we're going to do with a language construct, and given that the compiler assumes that anything that isn't shared or immutable is thread-local, it's very much a risky thing to do. As for __gshared, it's intended specifically for C globals, and using it for anything else is just begging for bugs. Because the compiler assumes that anything which is not marked as shared or immutable is thread-local, having such an object actually be able to be mutated by another thread risks subtle bugs of the sort that shared was supposed to prevent in the first place. Unfortunately, due to some of the difficulties in using shared and some of the misunderstandings about it, a number of folks have just used __gshared instead of shared, but once you do that, you're risking subtle bugs, because that's not at all what __gshared is intended for. If you're using __gshared for anything other than a C global, it's arguably a bug. Certainly, it's a risky proposition. - Jonathan M Davis