digitalmars.D - Proposal to make "shared" (more) useful
- Arafel (33/33) Sep 13 2018 Hi all,
- Adam D. Ruppe (2/4) Sep 13 2018 so __gshared implies static. Are you sure that's what you want?
- Arafel (8/13) Sep 13 2018 Indeed it isn't! Why must __gshared be static?? (BTW, thanks a lot, you
- Adam D. Ruppe (29/35) Sep 13 2018 The memory location differences of shared doesn't apply to class
- Arafel (17/61) Sep 13 2018 Thanks a lot!! I remember having tried casting shared away, and ending
- Kagamin (15/15) Sep 13 2018 struct Unshared(T)
- Arafel (34/51) Sep 13 2018 Doesn't work:
- Kagamin (18/18) Sep 14 2018 struct Unshared(T)
- Arafel (93/112) Sep 14 2018 Almost there, you have to make "get" ref for it to work when calling
- Kagamin (4/21) Sep 14 2018 You can do
- Arafel (39/56) Sep 13 2018 My current attempt, still work in progress:
- Jonathan M Davis (88/120) Sep 13 2018 Have you read the concurrency chapter in The D Programming Language by
- Arafel (56/107) Sep 14 2018 I hadn't read the book, but that's indeed the gist of what I'm
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. 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. A.
Sep 13 2018
On Thursday, 13 September 2018 at 13:53:49 UTC, Arafel wrote:* Make all _private non-reference fields_ of shared, synchronized classes __gshared.so __gshared implies static. Are you sure that's what you want?
Sep 13 2018
On 09/13/2018 04:27 PM, Adam D. Ruppe wrote:On Thursday, 13 September 2018 at 13:53:49 UTC, Arafel wrote:Indeed it isn't! Why must __gshared be static?? (BTW, thanks a lot, you have just saved me a lot of debugging!!). Then, how on earth are we supposed to have a struct like SysTime as a field in a shared class? Other than the "fun" of having a shared *pointer* to such a struct that then you can cast as non-shared as needed... But let's say that this solution seems quite...... sub-optimal... at so many levels...* Make all _private non-reference fields_ of shared, synchronized classes __gshared.so __gshared implies static. Are you sure that's what you want?
Sep 13 2018
On Thursday, 13 September 2018 at 14:43:51 UTC, Arafel wrote:Why must __gshared be static?? (BTW, thanks a lot, you have just saved me a lot of debugging!!).The memory location differences of shared doesn't apply to class members. All members are stored with the instance, and shared only changes the type. (Contrast to global variables, where shared changes where they are stored - the default is to put them in thread-local storage, and shared moves it back out of that.) Class static variables btw follow the same TLS rules. A static member is really the same as a global thing, just in a different namespace. Now, the rule of __gshared is it puts it in that global memory storage using the unshared type. Unshared type you like here, but also, since normally, class members are stored in the object, changing the memory storage to the global shared area means it is no longer with the object... thus it becomes static.Then, how on earth are we supposed to have a struct like SysTime as a field in a shared class? Other than the "fun" of having a shared *pointer* to such a struct that then you can cast as non-shared as needed...What you want is an unshared type without changing the memory layout. There's no syntax for this at declaration, but there is one at usage point: you can cast away attributes on an lvalue: shared class Foo { void update() { // the cast below strips it of all attributes, including shared, // allowing the assignment to succeed cast() s = Clock.currTime; } SysTime s; } Using the private field with a public/protected/whatever accessor method, you can encapsulate this assignment a little and make it more sane to the outside world.
Sep 13 2018
On 09/13/2018 05:11 PM, Adam D. Ruppe wrote:On Thursday, 13 September 2018 at 14:43:51 UTC, Arafel wrote:Thanks a lot!! I remember having tried casting shared away, and ending up with a duplicate, but I have just tried it now and indeed it seems to work, will have to try with more complex use cases (comparing, adding dates and intervals, etc.), but it looks promising. The problem might have been that I think I tried: shared SysTime s_; SysTime s = cast () s_; // Now I've got a duplicate! Ugh! Because that works with classes... but (in hindsight) obviously not with value types. I still think that it would be useful: 1) Allow __gshared for non-static fields to have this meaning, it would make it much more intuitive. A library solution is perhaps possible, but cumbersome. 2) Make it (sometimes) automatic as the original proposal. Of course 1) is the most important part. A.Why must __gshared be static?? (BTW, thanks a lot, you have just saved me a lot of debugging!!).The memory location differences of shared doesn't apply to class members. All members are stored with the instance, and shared only changes the type. (Contrast to global variables, where shared changes where they are stored - the default is to put them in thread-local storage, and shared moves it back out of that.) Class static variables btw follow the same TLS rules. A static member is really the same as a global thing, just in a different namespace. Now, the rule of __gshared is it puts it in that global memory storage using the unshared type. Unshared type you like here, but also, since normally, class members are stored in the object, changing the memory storage to the global shared area means it is no longer with the object... thus it becomes static.Then, how on earth are we supposed to have a struct like SysTime as a field in a shared class? Other than the "fun" of having a shared *pointer* to such a struct that then you can cast as non-shared as needed...What you want is an unshared type without changing the memory layout. There's no syntax for this at declaration, but there is one at usage point: you can cast away attributes on an lvalue: shared class Foo { void update() { // the cast below strips it of all attributes, including shared, // allowing the assignment to succeed cast() s = Clock.currTime; } SysTime s; } Using the private field with a public/protected/whatever accessor method, you can encapsulate this assignment a little and make it more sane to the outside world.
Sep 13 2018
struct Unshared(T) { private T value; T get() shared { return cast(T)value; } alias get this; void opAssign(T v) shared { value=cast(shared)v; } } shared synchronized class A { private Unshared!(int[]) a; int[] f() { return a; } }
Sep 13 2018
On 09/13/2018 05:16 PM, Kagamin wrote:struct Unshared(T) { private T value; T get() shared { return cast(T)value; } alias get this; void opAssign(T v) shared { value=cast(shared)v; } } shared synchronized class A { private Unshared!(int[]) a; int[] f() { return a; } }Doesn't work: ``` import std.datetime.systime; struct Unshared(T) { private T value; T get() shared { return cast(T)value; } alias get this; void opAssign(T v) shared { value=cast(shared)v; } } shared synchronized class A { private Unshared!SysTime t; this() { t = Clock.currTime; } } void main() { shared A a = new shared A; } ``` Gives you: onlineapp.d(6): Error: non-shared const method std.datetime.systime.SysTime.opCast!(SysTime).opCast is not callable using a shared mutable object onlineapp.d(6): Consider adding shared to std.datetime.systime.SysTime.opCast!(SysTime).opCast onlineapp.d(8): Error: template std.datetime.systime.SysTime.opAssign cannot deduce function from argument types !()(shared(SysTime)) shared, candidates are: /dlang/dmd/linux/bin64/../../src/phobos/std/datetime/systime.d(612): std.datetime.systime.SysTime.opAssign()(auto ref const(SysTime) rhs) onlineapp.d(12): Error: template instance `onlineapp.Unshared!(SysTime)` error instantiating
Sep 13 2018
struct Unshared(T) { private T value; this(T v) shared { opAssign(v); } T get() shared { return *cast(T*)&value; } alias get this; void opAssign(T v) shared { *cast(T*)&value=v; } } shared synchronized class A { private Unshared!(int[]) a; private Unshared!SysTime t; this(){ t=Clock.currTime; } int[] f() { return a; } }
Sep 14 2018
On 09/14/2018 09:32 AM, Kagamin wrote:struct Unshared(T) { private T value; this(T v) shared { opAssign(v); } T get() shared { return *cast(T*)&value; } alias get this; void opAssign(T v) shared { *cast(T*)&value=v; } } shared synchronized class A { private Unshared!(int[]) a; private Unshared!SysTime t; this(){ t=Clock.currTime; } int[] f() { return a; } }Almost there, you have to make "get" ref for it to work when calling methods that mutate the instance, though. ``` import std.datetime.systime; import std.stdio; import core.time; shared struct Unshared(T) { private T value; this(T v) shared { opAssign(v); } ref T get() shared { return *cast(T*)&value; } alias get this; void opAssign(T v) shared { *cast(T*)&value=v; } } shared synchronized class A { private Unshared!(int[]) a; private Unshared!SysTime t; this(){ t=Clock.currTime; } int[] f() { return a; } void g() { writeln(t); t += 1.dur!"minutes"; // Doesn't work without "ref" writeln(t); t = t + 1.dur!"minutes"; writeln(t); } } void main() { shared A a = new shared A; a.g; } ``` Still, I don't know how to deal with structs with disabled this() and/or specific constructors and other corner cases, it currently doesn't work 100% as it should: ``` import std.stdio; struct S { disable this(); int i; this(int i_) { i = 2 * i_; } void opAssign(int i_) { i = 2 * i_; } void f() { i *= 2; } } shared struct Unshared(T) { private T value; this(T v) shared { *cast(T*)&value=v; } ref T get() shared { return *cast(T*)&value; } alias get this; void opAssign(T v) shared { *cast(T*)&value=v; } } shared synchronized class A { private Unshared!S s; // Should this even be possible? What about the disable this?? // I would expect at least one, if not both of the following, to work //private Unshared!S s2 = S(1); //private Unshared!S s3 = 1; this(){ s = S(1); //s = 1; s.f; } void f() { writeln(s.i); } } void main() { // This is rightly not possible: disable this() // S s1; S s = 2; // This doesn't work in Unshared s = 1; // Neither does this s.f; writeln(s.i); // 4 as it should shared A a = new shared A; a.f; // 4 as it should } ``` That's why I think it should be in the language itself, or at a minimum in Phobos once all the bugs are found and ironed out, if possible. A.
Sep 14 2018
On Friday, 14 September 2018 at 08:18:25 UTC, Arafel wrote:shared synchronized class A { private Unshared!S s; // Should this even be possible? What about the disable this?? // I would expect at least one, if not both of the following, to work //private Unshared!S s2 = S(1); //private Unshared!S s3 = 1; this(){ s = S(1); //s = 1; s.f; } void f() { writeln(s.i); } }You can do private Unshared!(S*) s; this(){ s = new S(1); }
Sep 14 2018
You can do private Unshared!(S*) s; this(){ s = new S(1); }Yeah, there are workarounds, also some other minor issues. For example, I wanted to use it with a pointer type, and then take the pointer of that (don't ask me: C library), and I had to find a workaround for this as well. My point is that: 1) It's not transparent, not even remotely clear how to get this working. 2) It should be if shared is to be used. If shared is to be disowned / left as it is, then there needs to be an alternative to casting voodoo because right now doing "manual" multithreading is hell. A.
Sep 14 2018
On Friday, 14 September 2018 at 09:36:44 UTC, Arafel wrote:1) It's not transparent, not even remotely clear how to get this working. 2) It should be if shared is to be used. If shared is to be disowned / left as it is, then there needs to be an alternative to casting voodoo because right now doing "manual" multithreading is hell.If you prefer C-style multithreading, D supports it with __gshared. There's std.concurrency too.
Sep 14 2018
If you prefer C-style multithreading, D supports it with __gshared.As I recently discovered, "__gshared" means "static", so not so easy to use for class instance members. In fact, that's exactly what I'd like to have.
Sep 14 2018
On Friday, 14 September 2018 at 11:13:00 UTC, Arafel wrote:As I recently discovered, "__gshared" means "static", so not so easy to use for class instance members. In fact, that's exactly what I'd like to have.__gshared is for global storage. If you don't use global storage, you can simply not qualify anything shared, and you won't have to deal with it.
Sep 14 2018
On 09/14/2018 01:37 PM, Kagamin wrote:On Friday, 14 September 2018 at 11:13:00 UTC, Arafel wrote:Sure, then let's remove the "shared" keyword altogether. Now, seriously, I understand that manually managed shared classes are not the preferred paradigm for many (most?) D programmers, and I'm not even against removing it altogether and then making it clear that it's not supported. But in my view this situation where there *seems* to be support for it in the language, but it's just a minefield once you try, just gives a bad impression of the language. Since I think this is commonly agreed, I was only trying to suggest a possible way to improve it (see my other messages in the thread), that's it. I'll anyway keep working with (against) "shared" and finding workarounds because for me the benefits in other areas of the language compensate, but I'm sure many people won't, and it's a pity because I think it has the potential to become something useful. A.As I recently discovered, "__gshared" means "static", so not so easy to use for class instance members. In fact, that's exactly what I'd like to have.__gshared is for global storage. If you don't use global storage, you can simply not qualify anything shared, and you won't have to deal with it.
Sep 14 2018
On Friday, 14 September 2018 at 12:00:19 UTC, Arafel wrote:Since I think this is commonly agreed, I was only trying to suggest a possible way to improve it (see my other messages in the thread), that's it.Well, indeed synchronized classes are already treated a little special, e.g. they don't allow public fields. As they are unlikely to be used in low-level multithreading, I think their behavior can be changed to provide a middle ground between C and D concurrency - java style: https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html If `synchronized` class is changed to not imply `shared`, then it all can work. 1. `synchronized` class won't imply `shared` (java disallows synchronized constructors because it doesn't make sense) 2. methods of a synchronized class are callable on both shared and unshared instances 3. maybe even make it applicable only to class, not individual methods AFAIK Andrei wanted some sort of compiler-assisted concurrency, maybe he will like such proposal.
Sep 14 2018
On 09/13/2018 05:16 PM, Kagamin wrote:struct Unshared(T) { private T value; T get() shared { return cast(T)value; } alias get this; void opAssign(T v) shared { value=cast(shared)v; } } shared synchronized class A { private Unshared!(int[]) a; int[] f() { return a; } }My current attempt, still work in progress: ``` import std.stdio; import std.datetime.systime; shared struct GShared(T) { ubyte[T.sizeof] payload; this(T t) { *(cast(T*) &payload) = t; } this(shared T t) { *(cast(T*) &payload) = cast() t; } void opAssign(T t) { *(cast(T*) &payload) = t; } void opAssign(shared T t) { *(cast(T*) &payload) = cast() t; } ref T data() { return *(cast(T*) &payload); } alias data this; } shared synchronized class A { this() { t = Clock.currTime; } void printIt() { writeln(t); } private: GShared!SysTime t; } void main() { shared A a = new shared A; a.printIt; } ```
Sep 13 2018
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
Sep 13 2018
On 09/13/2018 09:49 PM, Jonathan M Davis wrote: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).I hadn't read the book, but that's indeed the gist of what I'm proposing. I think it could be enough to restrict it to value types, where it's easier to assume (and even check) that there are no external references.[snip] 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.I completely agree with this argument, however please note that there must be a sensible way to work with shared, otherwise we enter in the "the perfect is the enemy of the good" area. For reference types it's somehow workable, because you can just cast away and store it in a new variable: ``` class A { this() { } } shared synchronized class B { this(A a) { a_ = cast (shared) new A; // no shared this() } void foo() { A a = cast () a_; // Work with it } private: A a_; } ``` It's still somewhat cumbersome, specially if you have many such members, but still doable. However, this is not possible for value types, and it makes it nigh on impossible to work with them in a sensible way. You have either to use pointers, or cast away every type you want to use it. None of them are what I would call "practical". While not the biggest problem (see the later point), I still think that synchronized classes are a good compromise, specially with the restriction of only applying to full value types (no internal references allowed). Of course it is still perhaps possible to bypass that mechanism, but so is the case with many other ones (assumeUnique?). If it's hard enough to do by mistake, it can be assumed that the people messing with it should know what they are doing. Finally, you suggest using __gshared, and I'm not sure you're not having the same misunderstanding I had: __gshared implies "static", so it's not a valid solution for class fields in most cases.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.As I said, the current semantics of __gshared doesn't allow it to be a "drop-in" replacement of "shared". I also agree that it's not what it was meant for, and that changing that right now would risk breaking a lot of code. However, I think that there should be *some* way in the language itself to express that without having to cast all over the place: access this member of the shared class as if it were local, I'll take care of controlling the access to it. You can use a wrapper type, and that's what I'm trying to do right now, but I'm pretty sure there will be a ton of corner cases and interactions that will make it really hard to work reliably in a generic way. Then, implementing a kind of "synchronized classes" that would automate this when possible would of course be a further and welcome improvement. A.
Sep 14 2018