On Sunday, 12 February 2017 at 20:08:05 UTC, bitwise wrote:
It seems like you're saying that 'shared' should mean both
'thread safe' and 'not thread safe' depending on context, which
doesn't make sense.
Makes sense to me: A `shared` variable is shared among threads.
Accesses are not thread-safe. When a function has a `shared`
parameter, it's expected to access that variable in a thread-safe
manner. A `shared` method is a function with a `shared` `this`
parameter.
Considering the alternative, that functions are not expected to
ensure thread-safety on their `shared` parameters, that would
mean you have to ensure it at the call site. And then what would
be the point of marking the parameter `shared`, if thread-safety
is already ensured?
Example:
shared A a;
struct A {
int x, y;
void foo() shared {
a.x = 1;
}
}
int main(string[] argv) {
a.x = 5;
a.y = 5;
a.foo();
return 0;
}
Qualifying 'a' with 'shared' means that it's shared between
threads, which means that accessing it is _not_ thread safe.
Yup. In my opinion, non-atomic accesses like that should be
rejected by the compiler. But you can write race conditions with
only atomic stores/loads, so in the end it's the programmer's
responsibility to write correct code.
Since the method 'foo' accesses 'a', 'foo' is also _not_ thread
safe.
Well, yes, that `foo` isn't thread-safe. But it should be written
differently so that it is.
Given that both the data and the method are 'shared', a caller
should know that race conditions are possible and that they
should aquire a lock before accessing either of them...or so it
would seem.
But when you have the lock, you can safely call any method,
including non-`shared` ones. I see no point in distinguishing
`shared` and unshared methods then.
Non-`shared` methods are obviously not safe to call on `shared`
objects. So `shared` methods must be other thing: safe.
I imagine that qualifying a method with 'shared' should mean
that it can access shared data, and hence, is _not_ thread safe.
Every function/method can access shared data. They're all
expected to do it safely. The `shared` attribute just qualifies
the `this` reference.
This prevent access to 'shared' data from any non 'shared'
context, without some kind of bridge/cast that a programmer
would use when they knew that they had aquired the lock or
ownership of the data. Although this is what would make sense
to me, it doesn't seem to match with the current implementation
of 'shared', or what you're saying.
It wouldn't exactly "prevent" it, would it? The compiler can't
check that you've got the correct lock. It would be expected of
the programmer to do so before calling the `shared` method.
That's easy to get wrong.
When `shared` methods are safe themselves, you can't get the
calls to them wrong. The ugly is nicely contained. To call an
unsafe method, you have to cast and that's a good indicator that
you're entering the danger zone.
It seems that methods qualified with 'shared' may be what
you're suggesting matches up with the 'bridge' I'm trying to
describe, but again, using the word 'shared' to mean both
'thread safe' and 'not thread safe' doesn't make sense.
Maybe don't think of it meaning "safe" or "unsafe" then. It just
means "shared".
A `shared` variable is just that: shared. The way you deal with
it can be thread-safe or not. Everyone is expected to deal with
it safely, though. "Everyone" includes `shared` methods.
Firstly, because the same keyword should not mean two strictly
opposite things. Also, if a 'shared' method is supposed to be
thread safe, then the fact that it has access to shared data is
irrelevant to the caller.
Non-`shared` methods are not thread-safe. They expect unshared
data. You can still call them on shared objects, though, with a
cast. And when you've ensured thread-safety beforehand, it's
perfectly fine to do so.
If `shared` methods were unsafe too, then that would only allow
calling unsafe code without a cast. Doesn't seem like an
improvement.
So 'shared' as a method qualifier doesn't really make sense.
What would make more sense is to have a region where 'shared'
data could be accessed - Maybe something like this:
struct S {
shared int x;
Lock lk;
private void addNum(int n) shared {
x += num;
}
int add(int a, int b)
{
shared {
lk.lock();
addNum(a);
addNum(b);
lk.unlock();
}
}
}
So above,
1) 'x' would be shared, and mutating it would not thread safe.
As it is now.
2) 'addNum' would have access to 'shared' data, and also be
non-thread-safe
Today, non-`shared` methods are unsafe, and they can access
shared data just like `shared` methods. But I imagine you'd have
`shared` methods alter `shared` data freely, without casting.
3) 'x' and 'addNum' would be inaccessible from 'add' since
they're 'shared'
As it is now. Can't just call a `shared` method from a
non-`shared` one.
4) a 'shared' block inside 'add' would allow access to 'x' or
'addNum', with the responsibility being on the programmer to
lock.
So the `shared` block as a whole is thread-safe and it's the
programmer's duty to make sure of that. While today, a `shared`
method as a whole is thread-safe and it's the programmer's duty
to make sure of that. Not much of a difference, is it?
5) alternatively, 'shared' data could be accessed from within a
'synchronized' block.
I thought 'shared' was a finished feature, but it's starting to
seem like it's a WIP. This kind of feature seems like it has
great potential, but is mostly useless in it's current state.
That may be so or not. I don't think you've made an argument for
"unfinished" or "useless", though. You've argued "inconsistent",
and maybe "surprising" or simply "bad". I wouldn't expect further
development of the feature to meet your vision.
After more testing with shared, it seems that 'shared' data is
mutable from many contexts, from which it would be unsafe to
mutate it without locking first, which basically removes any
gauruntee that would make 'shared' useful.
There is no guarantee of thread-safety, yes. There cannot be, as
far as I understand, because the compiler cannot know which
operations must happen without interruption.
However, as I've said above, I'd like non-atomic accesses of
`shared` variables to be rejected. Non-atomic increment is being
rejected, so it makes no sense to me that non-atomic writes and
reads are allowed.
Overall, I think `shared` is solid. It's not magic. It mainly
prevents you from accidentally doing unsafe stuff by highlighting
shared data and forcing you to cast or use special functions that
deal safely with shared data.