Very limited shared promotion
Timon Gehr
timon.gehr at gmx.ch
Fri Jun 21 20:49:43 UTC 2019
On 20.06.19 03:30, Walter Bright wrote:
> On 6/19/2019 3:04 PM, Timon Gehr wrote:
>> I was talking about this:
>>
>> int x0;
>> void fun(scope ref shared(int) x1){
>> assert(&x0 is &x1); //uh, oh
>> // ...
>> }
>> fun(x);
>
> I see.
>
>
>> Right now, in `@safe` code it guarantees that there is _no_
>> multithreaded access, no? How do you leak a `scope` reference to
>> another thread? If there is _no_ multithreaded access, you don't get
>> any data races, so `scope` guarantees no data races on the `scope`d
>> memory location given that the reference wasn't accessible from
>> multiple threads before.
>
> `scope` only guarantees no references to the scoped variable exist
> following the exit of the function. What happens inside is anything goes
> as long as that invariant is maintained.
So in purely @safe code without any calls to @trusted functions you
can't send that reference anywhere.
> That would include starting up
> a new thread that can access the scope variable, as long as the thread
> terminates its use of that variable before the function exits.
> ...
Yes, but terms such as "before" are tricky when multiple threads are
involved. Semi-formally, what you are saying is that there has to be a
path from each write access in each spawned thread to the return of the
function in the happens- before graph. I don't think it is possible to
establish this without making it valid to access an unique reference
without further memory barriers. I don't think you can allow a race to
exist between the function return and any write accesses to `scope`'d
parameters.
>
>> Yes, what's above certainly does not work. My point was that that is
>> not what the code would do, at least not legally. If the above
>> happens, there would be some `@trusted` code somewhere that you can
>> blame for it.
>>
>> Sending a `scope` variable to another thread is a `@system` operation.
>> In order to ensure that `scope` guarantees are met, the `@trusted`
>> code that performs the send needs to add memory barriers that ensure
>> that all writes to the `scope` variable are visible on the current
>> thread before it returns. So on thread 2 you would actually also have
>> barriers before `x` is accessed again.
>
> `scope` does not offer such guarantees. It only guarantees no leaks of
> the reference that survive the end of the function. It does not
> guarantee synchronization of memory caches.
> ...
My complaint is that the last two sentences appear mutually contradictory.
>>
>> Let's consider a case _without_ implicit promotion:
>>
>> shared(int) x=0; // x is shared all the way
>> static void foo(scope ref shared(int) x)@trusted{
>> // possibly send &x to other threads that mutate it and
>> // then forget about &x
>> // ...
>> }
>> foo(x);
>> int y=x; // read x
>> enforce(x==y); // read x again
>>
>> My claim is that if the enforcement fails, this indicates a bug in
>> `foo`, because it violates guarantees you can derive from the fact
>> that `x` is a local variable that is only passed to some other code
>> via a `scope`d parameter: when `x` is read for the first time, we know
>> that actually no other thread can have a reference to it which it can
>> use for a write, so its value has to remain constant.
>
> That is not what `scope` does,
We are using the same definition of `scope`. You haven't however
provided a formal definition of what "before" means.
> but the example is still correct because
> the language does not say that x can be converted to a local, and so it
> is not converted, and the memory synchronization of x is maintained and
> it works.
> ...
(Obviously that example works. That's not the point.)
>
>> It should therefore be possible to read `x` without any further
>> barriers after `foo` returns. This is because `foo` already needs to
>> ensure all writes from other threads are visible.
>
> `scope` does not add such barriers. The implementer of foo may add a
> fence to do that, but the compiler doesn't know that and cannot rely on it.
>
>
>> It is possible that I am missing something (even though what you wrote
>> is not it). Is there a possible implementation of `foo` that would be
>> correct (in a language that does not have any implicit `shared`
>> promotion), but for which a data race on `x` would result if we didn't
>> add another fence before reading `x` after `foo` has returned?
>
> AFAIK, another fence is required upon return of foo and before reading
> x. It's not just about whether an extra reference exists, it's about
> when the memory cache coherency happens.
> ...
I don't think it makes any sense to claim "no other reference exists" at
a specific program point if at that point you didn't properly
synchronize with parties potentially still holding such a reference.
More information about the Digitalmars-d
mailing list