On 10/15/18 2:46 PM, Manu wrote:
Okay, so I've been thinking on this for a while... I think I have a
pretty good feel for how shared is meant to be.
1. shared should behave exactly like const, except in addition to
inhibiting write access, it also inhibits read access.
I think this is the foundation for a useful definition for shared, and
it's REALLY easy to understand and explain.
Current situation where you can arbitrarily access shared members
undermines any value it has. Shared must assure you don't access
members unsafely, and the only way to do that with respect to data
members, is to inhibit access completely.
I think shared is just const without read access.
Assuming this world... how do you use shared?
1. traditional; assert that the object become thread-local by
acquiring a lock, cast shared away
2. object may have shared methods; such methods CAN be called on
shared instances. such methods may internally implement
synchronisation to perform their function. perhaps methods of a
lock-free queue structure for instance, or operator overloads on
`Atomic!int`, etc.
In practise, there is no functional change in usage from the current
implementation, except we disallow unsafe accesses (which will make
the thing useful).
From there, it opens up another critical opportunity; T* -> shared(T)*
promotion.
Const would be useless without T* -> const(T)* promotion. Shared
suffers a similar problem.
If you write a lock-free queue for instance, and all the methods are
`shared` (ie, threadsafe), then under the current rules, you can't
interact with the object when it's not shared, and that's fairly
useless.
Assuming the rules above: "can't read or write to members", and the
understanding that `shared` methods are expected to have threadsafe
implementations (because that's the whole point), what are the risks
from allowing T* -> shared(T)* conversion?
All the risks that I think have been identified previously assume that
you can arbitrarily modify the data. That's insanity... assume we fix
that... I think the promotion actually becomes safe now...?
Destroy...
This is a step in the right direction. But there is still one problem --
shared is inherently transitive.
So casting away shared is super-dangerous, even if you lock the shared
data, because any of the subreferences will become unshared and
read/writable.
For instance:
struct S
{
int x;
int *y;
}
shared int z;
auto s1 = shared(S)(1, &z);
auto s2 = shared(S)(2, &z);
S* s1locked = s1.lock;
Now I have access to z via s1locked as an unshared int, and I never
locked z. Potentially one could do the same thing via s2, and now there
are 2 mutable references, potentially in 2 threads.
All of this, of course, is manual. So technically we could manually
implement it properly inside S. But this means shared doesn't help us much.
We really need on top of shared, a way to specify something is
tail-shared. That is, all the data in S is unshared, but anything it
points to is still shared. That at least helps the person implementing
the manual locking from doing stupid things himself.
-Steve