www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Very limited shared promotion

reply Manu <turkeyman gmail.com> writes:
Is this valid?

int x;
void fun(scope ref shared(int) x) { ... }
fun(x); // implicit promotion to shared in this case

This appears to promote a thread-local to shared. The problem with
such promotion is that it's not valid that a thread-local AND a shared
reference to the same thing can exist at the same time.

With scope, we can guarantee that the reference doesn't escape the callee.
Since the argument is local to the calling thread, and since the
calling thread can not be running other code at the same time as the
call is executing, there is no way for any code to execute with a
thread-local assumption while the callee makes shared assumptions.

I think this might be safe?
Jun 17
next sibling parent reply FeepingCreature <feepingcreature gmail.com> writes:
On Monday, 17 June 2019 at 23:46:44 UTC, Manu wrote:
 Is this valid?

 int x;
 void fun(scope ref shared(int) x) { ... }
 fun(x); // implicit promotion to shared in this case

 This appears to promote a thread-local to shared. The problem 
 with such promotion is that it's not valid that a thread-local 
 AND a shared reference to the same thing can exist at the same 
 time.

 With scope, we can guarantee that the reference doesn't escape 
 the callee.
 Since the argument is local to the calling thread, and since the
 calling thread can not be running other code at the same time 
 as the
 call is executing, there is no way for any code to execute with 
 a
 thread-local assumption while the callee makes shared 
 assumptions.

 I think this might be safe?
Can `fun` execute a call to another nested function that accesses `x` unshared?
Jun 17
parent reply Manu <turkeyman gmail.com> writes:
On Tue, Jun 18, 2019 at 3:05 PM FeepingCreature via Digitalmars-d
<digitalmars-d puremagic.com> wrote:
 On Monday, 17 June 2019 at 23:46:44 UTC, Manu wrote:
 Is this valid?

 int x;
 void fun(scope ref shared(int) x) { ... }
 fun(x); // implicit promotion to shared in this case

 This appears to promote a thread-local to shared. The problem
 with such promotion is that it's not valid that a thread-local
 AND a shared reference to the same thing can exist at the same
 time.

 With scope, we can guarantee that the reference doesn't escape
 the callee.
 Since the argument is local to the calling thread, and since the
 calling thread can not be running other code at the same time
 as the
 call is executing, there is no way for any code to execute with
 a
 thread-local assumption while the callee makes shared
 assumptions.

 I think this might be safe?
Can `fun` execute a call to another nested function that accesses `x` unshared?
Well, under my proposal, if the capture were actually qualified, it wouldn't be possible to call another local function with a lesser-qualified capture; so that's another +1 for qualifying the capture! ;)
Jun 18
parent Timon Gehr <timon.gehr gmx.ch> writes:
On 18.06.19 14:00, Manu wrote:
 On Tue, Jun 18, 2019 at 3:05 PM FeepingCreature via Digitalmars-d
 <digitalmars-d puremagic.com> wrote:
 On Monday, 17 June 2019 at 23:46:44 UTC, Manu wrote:
 Is this valid?

 int x;
 void fun(scope ref shared(int) x) { ... }
 fun(x); // implicit promotion to shared in this case

 This appears to promote a thread-local to shared. The problem
 with such promotion is that it's not valid that a thread-local
 AND a shared reference to the same thing can exist at the same
 time.

 With scope, we can guarantee that the reference doesn't escape
 the callee.
 Since the argument is local to the calling thread, and since the
 calling thread can not be running other code at the same time
 as the
 call is executing, there is no way for any code to execute with
 a
 thread-local assumption while the callee makes shared
 assumptions.

 I think this might be safe?
Can `fun` execute a call to another nested function that accesses `x` unshared?
Well, under my proposal, if the capture were actually qualified, it wouldn't be possible to call another local function with a lesser-qualified capture; so that's another +1 for qualifying the capture! ;)
Bullshit.
Jun 18
prev sibling next sibling parent reply Steven Schveighoffer <schveiguy gmail.com> writes:
On 6/17/19 7:46 PM, Manu wrote:
 Is this valid?
 
 int x;
 void fun(scope ref shared(int) x) { ... }
 fun(x); // implicit promotion to shared in this case
 
 This appears to promote a thread-local to shared. The problem with
 such promotion is that it's not valid that a thread-local AND a shared
 reference to the same thing can exist at the same time.
 
 With scope, we can guarantee that the reference doesn't escape the callee.
 Since the argument is local to the calling thread, and since the
 calling thread can not be running other code at the same time as the
 call is executing, there is no way for any code to execute with a
 thread-local assumption while the callee makes shared assumptions.
 
 I think this might be safe?
 
Seems like it would be safe, but what is the use case for it? If you have a scope shared, which can't actually get shared with anything else, what does it buy you? -Steve
Jun 18
next sibling parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 18.06.19 15:29, Steven Schveighoffer wrote:
 On 6/17/19 7:46 PM, Manu wrote:
 Is this valid?

 int x;
 void fun(scope ref shared(int) x) { ... }
 fun(x); // implicit promotion to shared in this case

 This appears to promote a thread-local to shared. The problem with
 such promotion is that it's not valid that a thread-local AND a shared
 reference to the same thing can exist at the same time.

 With scope, we can guarantee that the reference doesn't escape the 
 callee.
 Since the argument is local to the calling thread, and since the
 calling thread can not be running other code at the same time as the
 call is executing, there is no way for any code to execute with a
 thread-local assumption while the callee makes shared assumptions.

 I think this might be safe?
Seems like it would be safe, but what is the use case for it? If you have a scope shared, which can't actually get shared with anything else, what does it buy you? -Steve
He would write system code that leaks the reference to other threads temporarily, but ensures those references disappear once the parallel part of the computation has finished. His use case is safe parallel foreach without having to explicitly qualify the variables in the outer scope as `shared`: void sum(){ // note: just to illustrate the concept int result=0; foreach(i;iota(1000).parallel){ static assert(typeof(result)==shared(int)); result.atomic!"+="(i); } static assert(typeof(result)==int); return result; }
Jun 18
next sibling parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 18.06.19 16:13, Timon Gehr wrote:
 
 int sum(){ // note: just to illustrate the concept
      int result=0;
      foreach(i;iota(1000).parallel){
          static assert(typeof(result)==shared(int));
          result.atomic!"+="(i);
      }
      static assert(typeof(result)==int);
      return result;
 }
Return type should have been int, of course...
Jun 18
parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 18.06.19 16:14, Timon Gehr wrote:
 On 18.06.19 16:13, Timon Gehr wrote:
 int sum(){ // note: just to illustrate the concept
      int result=0;
      foreach(i;iota(1000).parallel){
          static assert(is(typeof(result)==shared(int)));
          result.atomic!"+="(i);
      }
      static assert(is(typeof(result)==int));
      return result;
 }
Return type should have been int, of course...
And there should have been is expressions. Getting tired from pushing back against all the nonsense. Just note that if you can make the above work with useless qualified capturing, you can do so with useful qualified capturing and this is so blatantly obvious that it causes me physical pain that Manu honestly does not see it.
Jun 18
next sibling parent reply Manu <turkeyman gmail.com> writes:
On Wed, Jun 19, 2019 at 12:20 AM Timon Gehr via Digitalmars-d
<digitalmars-d puremagic.com> wrote:
 On 18.06.19 16:14, Timon Gehr wrote:
 On 18.06.19 16:13, Timon Gehr wrote:
 int sum(){ // note: just to illustrate the concept
      int result=0;
      foreach(i;iota(1000).parallel){
          static assert(is(typeof(result)==shared(int)));
          result.atomic!"+="(i);
      }
      static assert(is(typeof(result)==int));
      return result;
 }
Return type should have been int, of course...
And there should have been is expressions. Getting tired from pushing back against all the nonsense. Just note that if you can make the above work with useless qualified capturing, you can do so with useful qualified capturing and this is so blatantly obvious that it causes me physical pain that Manu honestly does not see it.
I can't see it because the design specifically inhibits that static assert in the loop body. But I will revoke my opinion on this matter; arguing will only slow it down. If you know how to make that *EXACT* code you wrote above work, then we are done here and I will buy you a years supply of beer. Coupled with removing read/write access from shared, that is everything I need to go away and leave you all alone.
Jun 18
parent Timon Gehr <timon.gehr gmx.ch> writes:
On 19.06.19 05:11, Manu wrote:
 On Wed, Jun 19, 2019 at 12:20 AM Timon Gehr via Digitalmars-d
 <digitalmars-d puremagic.com> wrote:
 On 18.06.19 16:14, Timon Gehr wrote:
 On 18.06.19 16:13, Timon Gehr wrote:
 int sum(){ // note: just to illustrate the concept
       int result=0;
       foreach(i;iota(1000).parallel){
           static assert(is(typeof(result)==shared(int)));
           result.atomic!"+="(i);
       }
       static assert(is(typeof(result)==int));
       return result;
 }
Return type should have been int, of course...
And there should have been is expressions. Getting tired from pushing back against all the nonsense. Just note that if you can make the above work with useless qualified capturing, you can do so with useful qualified capturing and this is so blatantly obvious that it causes me physical pain that Manu honestly does not see it.
I can't see it because the design specifically inhibits that static assert in the loop body.
Ok. I guess it suffices to say that it does not. (If you can promote the entire stack frame to `shared` temporarily, you can just as easily promote only the part of the stack frame that your delegate actually wants to capture.)
 But I will revoke my opinion on this matter; arguing will only slow it
 down. If you know how to make that *EXACT* code you wrote above work,
I don't know yet what the _most general_ implicit `shared` promotion rules could be, but I think I know how to do it in the type system for your specific case. For memory ordering, we can just say that because there is `scope` on the delegate, any trusted code that sends it to other threads temporarily is clearly already required to set up the respective memory barriers. (Because it needs to ensure that the deletion of all references to the closure context in all other threads is ordered before the owning thread returns.)
 then we are done here and I will buy you a years supply of beer.
 ...
I don't drink. :)
 Coupled with removing read/write access from shared, that is
 everything I need to go away and leave you all alone.
 
Then we are on the same page, but this will need a DIP, so it would make sense to think about it a bit more and to convince Walter that it can work.
Jun 19
prev sibling parent reply Manu <turkeyman gmail.com> writes:
On Wed, Jun 19, 2019 at 1:11 PM Manu <turkeyman gmail.com> wrote:
 On Wed, Jun 19, 2019 at 12:20 AM Timon Gehr via Digitalmars-d
 <digitalmars-d puremagic.com> wrote:
 On 18.06.19 16:14, Timon Gehr wrote:
 On 18.06.19 16:13, Timon Gehr wrote:
 int sum(){ // note: just to illustrate the concept
      int result=0;
      foreach(i;iota(1000).parallel){
          static assert(is(typeof(result)==shared(int)));
          result.atomic!"+="(i);
      }
      static assert(is(typeof(result)==int));
      return result;
 }
Return type should have been int, of course...
And there should have been is expressions. Getting tired from pushing back against all the nonsense. Just note that if you can make the above work with useless qualified capturing, you can do so with useful qualified capturing and this is so blatantly obvious that it causes me physical pain that Manu honestly does not see it.
I can't see it because the design specifically inhibits that static assert in the loop body. But I will revoke my opinion on this matter; arguing will only slow it down. If you know how to make that *EXACT* code you wrote above work, then we are done here and I will buy you a years supply of beer. Coupled with removing read/write access from shared, that is everything I need to go away and leave you all alone.
I'll just tweak your example one more little bit to make sure absolutely everything I care about is captured correctly: struct S { int result; void inc(int i) shared { result.atomic!"+="(i); } } int sum(){ S s; foreach(i; iota(1000).parallel){ static assert(is(typeof(s) == shared(S))); s.inc(i); } static assert(is(typeof(s) == S)); return s.result; } The combinations of primitives at work here will solve every challenge I'm aware of in our engine.
Jun 18
parent reply rikki cattermole <rikki cattermole.co.nz> writes:
On 19/06/2019 3:20 PM, Manu wrote:
 I'll just tweak your example one more little bit to make sure
 absolutely everything I care about is captured correctly:
 
 struct S {
      int result;
      void inc(int i) shared { result.atomic!"+="(i); }
 }
 int sum(){
      S s;
      foreach(i; iota(1000).parallel){
          static assert(is(typeof(s) == shared(S)));
          s.inc(i);
      }
      static assert(is(typeof(s) == S));
      return s.result;
 }
 
 The combinations of primitives at work here will solve every challenge
 I'm aware of in our engine.
Okay is everyone in agreement that this is a use case for D? If so, lets get this into bugzilla!
Jun 19
parent rikki cattermole <rikki cattermole.co.nz> writes:
Never mind its already in.

https://issues.dlang.org/show_bug.cgi?id=19984
Jun 19
prev sibling parent reply Manu <turkeyman gmail.com> writes:
On Wed, Jun 19, 2019 at 12:15 AM Timon Gehr via Digitalmars-d
<digitalmars-d puremagic.com> wrote:
 On 18.06.19 15:29, Steven Schveighoffer wrote:
 On 6/17/19 7:46 PM, Manu wrote:
 Is this valid?

 int x;
 void fun(scope ref shared(int) x) { ... }
 fun(x); // implicit promotion to shared in this case

 This appears to promote a thread-local to shared. The problem with
 such promotion is that it's not valid that a thread-local AND a shared
 reference to the same thing can exist at the same time.

 With scope, we can guarantee that the reference doesn't escape the
 callee.
 Since the argument is local to the calling thread, and since the
 calling thread can not be running other code at the same time as the
 call is executing, there is no way for any code to execute with a
 thread-local assumption while the callee makes shared assumptions.

 I think this might be safe?
Seems like it would be safe, but what is the use case for it? If you have a scope shared, which can't actually get shared with anything else, what does it buy you? -Steve
He would write system code that leaks the reference to other threads temporarily, but ensures those references disappear once the parallel part of the computation has finished. His use case is safe parallel foreach without having to explicitly qualify the variables in the outer scope as `shared`: void sum(){ // note: just to illustrate the concept int result=0; foreach(i;iota(1000).parallel){ static assert(typeof(result)==shared(int)); result.atomic!"+="(i); } static assert(typeof(result)==int); return result; }
👍 We must get here.
Jun 18
parent reply Exil <Exil gmall.com> writes:
On Wednesday, 19 June 2019 at 03:02:30 UTC, Manu wrote:
 On Wed, Jun 19, 2019 at 12:15 AM Timon Gehr via Digitalmars-d 
 <digitalmars-d puremagic.com> wrote:
 On 18.06.19 15:29, Steven Schveighoffer wrote:
 On 6/17/19 7:46 PM, Manu wrote:
 Is this valid?

 int x;
 void fun(scope ref shared(int) x) { ... }
 fun(x); // implicit promotion to shared in this case

 This appears to promote a thread-local to shared. The 
 problem with such promotion is that it's not valid that a 
 thread-local AND a shared reference to the same thing can 
 exist at the same time.

 With scope, we can guarantee that the reference doesn't 
 escape the
 callee.
 Since the argument is local to the calling thread, and 
 since the
 calling thread can not be running other code at the same 
 time as the
 call is executing, there is no way for any code to execute 
 with a
 thread-local assumption while the callee makes shared 
 assumptions.

 I think this might be safe?
Seems like it would be safe, but what is the use case for it? If you have a scope shared, which can't actually get shared with anything else, what does it buy you? -Steve
He would write system code that leaks the reference to other threads temporarily, but ensures those references disappear once the parallel part of the computation has finished. His use case is safe parallel foreach without having to explicitly qualify the variables in the outer scope as `shared`: void sum(){ // note: just to illustrate the concept int result=0; foreach(i;iota(1000).parallel){ static assert(typeof(result)==shared(int)); result.atomic!"+="(i); } static assert(typeof(result)==int); return result; }
👍 We must get here.
How is it to know it is supposed to be shared though? opApply is already kind of a disaster with attributes.
Jun 18
next sibling parent Manu <turkeyman gmail.com> writes:
On Wed, Jun 19, 2019 at 1:15 PM Exil via Digitalmars-d <
digitalmars-d puremagic.com> wrote:

 On Wednesday, 19 June 2019 at 03:02:30 UTC, Manu wrote:
 On Wed, Jun 19, 2019 at 12:15 AM Timon Gehr via Digitalmars-d
 <digitalmars-d puremagic.com> wrote:
 On 18.06.19 15:29, Steven Schveighoffer wrote:
 On 6/17/19 7:46 PM, Manu wrote:
 Is this valid?

 int x;
 void fun(scope ref shared(int) x) { ... }
 fun(x); // implicit promotion to shared in this case

 This appears to promote a thread-local to shared. The
 problem with such promotion is that it's not valid that a
 thread-local AND a shared reference to the same thing can
 exist at the same time.

 With scope, we can guarantee that the reference doesn't
 escape the
 callee.
 Since the argument is local to the calling thread, and
 since the
 calling thread can not be running other code at the same
 time as the
 call is executing, there is no way for any code to execute
 with a
 thread-local assumption while the callee makes shared
 assumptions.

 I think this might be safe?
Seems like it would be safe, but what is the use case for it? If you have a scope shared, which can't actually get shared with anything else, what does it buy you? -Steve
He would write system code that leaks the reference to other threads temporarily, but ensures those references disappear once the parallel part of the computation has finished. His use case is safe parallel foreach without having to explicitly qualify the variables in the outer scope as `shared`: void sum(){ // note: just to illustrate the concept int result=3D0; foreach(i;iota(1000).parallel){ static assert(typeof(result)=3D=3Dshared(int)); result.atomic!"+=3D"(i); } static assert(typeof(result)=3D=3Dint); return result; }
=F0=9F=91=8D We must get here.
How is it to know it is supposed to be shared though? opApply is already kind of a disaster with attributes.
opApply receives a delegate; you would infer the delegate qualifier to the lambda as it already does for the loop counters from the arguments. Ie: opApply(scope void delegate() shared) <- infer the shared qualifier to the foreach body lambda.
Jun 18
prev sibling parent Manu <turkeyman gmail.com> writes:
On Wed, Jun 19, 2019 at 1:24 PM Manu <turkeyman gmail.com> wrote:

 On Wed, Jun 19, 2019 at 1:15 PM Exil via Digitalmars-d <
 digitalmars-d puremagic.com> wrote:

 On Wednesday, 19 June 2019 at 03:02:30 UTC, Manu wrote:
 On Wed, Jun 19, 2019 at 12:15 AM Timon Gehr via Digitalmars-d
 <digitalmars-d puremagic.com> wrote:
 On 18.06.19 15:29, Steven Schveighoffer wrote:
 On 6/17/19 7:46 PM, Manu wrote:
 Is this valid?

 int x;
 void fun(scope ref shared(int) x) { ... }
 fun(x); // implicit promotion to shared in this case

 This appears to promote a thread-local to shared. The
 problem with such promotion is that it's not valid that a
 thread-local AND a shared reference to the same thing can
 exist at the same time.

 With scope, we can guarantee that the reference doesn't
 escape the
 callee.
 Since the argument is local to the calling thread, and
 since the
 calling thread can not be running other code at the same
 time as the
 call is executing, there is no way for any code to execute
 with a
 thread-local assumption while the callee makes shared
 assumptions.

 I think this might be safe?
Seems like it would be safe, but what is the use case for it? If you have a scope shared, which can't actually get shared with anything else, what does it buy you? -Steve
He would write system code that leaks the reference to other threads temporarily, but ensures those references disappear once the parallel part of the computation has finished. His use case is safe parallel foreach without having to explicitly qualify the variables in the outer scope as `shared`: void sum(){ // note: just to illustrate the concept int result=3D0; foreach(i;iota(1000).parallel){ static assert(typeof(result)=3D=3Dshared(int)); result.atomic!"+=3D"(i); } static assert(typeof(result)=3D=3Dint); return result; }
=F0=9F=91=8D We must get here.
How is it to know it is supposed to be shared though? opApply is already kind of a disaster with attributes.
opApply receives a delegate; you would infer the delegate qualifier to th=
e
 lambda as it already does for the loop counters from the arguments.
 Ie:
  opApply(scope void delegate() shared) <- infer the shared qualifier to
 the foreach body lambda.
The delegate is effectively a prototype for the lambda, it should be used verbatim.
Jun 18
prev sibling next sibling parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 6/18/2019 6:29 AM, Steven Schveighoffer wrote:
 Seems like it would be safe,
When dealing with concurrency, that's like throwing a bunch of chemicals into a vat and saying it seems safe to drink :-) Just remember that doubled checked locking fooled a lot of very smart people for many years. I got snookered by it, too. Not sure where I read about it recently, but some expert had come up with a lock free algorithm, and the bug was only discovered a couple years later. Unless you are really, really good at concurrency, it's best to stick with atomics and locks, every time.
Jun 18
next sibling parent reply Steven Schveighoffer <schveiguy gmail.com> writes:
On 6/18/19 4:05 PM, Walter Bright wrote:
 On 6/18/2019 6:29 AM, Steven Schveighoffer wrote:
 Seems like it would be safe,
When dealing with concurrency, that's like throwing a bunch of chemicals into a vat and saying it seems safe to drink :-)
Nope, what I wrote has nothing to do with that. I sense that you're not grasping that sending in unshared data to a place where it can't possibly be shared means it's probably safe to do. Why you would want to do this, I'm not sure. -Steve
Jun 18
parent Walter Bright <newshound2 digitalmars.com> writes:
On 6/18/2019 4:05 PM, Steven Schveighoffer wrote:
 [...]
The rude remarks aren't acceptable here.
Jun 19
prev sibling parent Bart <Bart gmail.com> writes:
On Tuesday, 18 June 2019 at 20:05:25 UTC, Walter Bright wrote:
 On 6/18/2019 6:29 AM, Steven Schveighoffer wrote:
 Seems like it would be safe,
When dealing with concurrency, that's like throwing a bunch of chemicals into a vat and saying it seems safe to drink :-)
That is precisely what many corporations do on a daily basis. You know rice is the #1 crop in the world and much of it is bleached... you know what they use for bleaching? And I'm sure your intelligent enough to know that no reaction or cleaning process is 100%....
Jun 21
prev sibling parent Manu <turkeyman gmail.com> writes:
On Tue, Jun 18, 2019 at 11:30 PM Steven Schveighoffer via
Digitalmars-d <digitalmars-d puremagic.com> wrote:
 On 6/17/19 7:46 PM, Manu wrote:
 Is this valid?

 int x;
 void fun(scope ref shared(int) x) { ... }
 fun(x); // implicit promotion to shared in this case

 This appears to promote a thread-local to shared. The problem with
 such promotion is that it's not valid that a thread-local AND a shared
 reference to the same thing can exist at the same time.

 With scope, we can guarantee that the reference doesn't escape the callee.
 Since the argument is local to the calling thread, and since the
 calling thread can not be running other code at the same time as the
 call is executing, there is no way for any code to execute with a
 thread-local assumption while the callee makes shared assumptions.

 I think this might be safe?
Seems like it would be safe, but what is the use case for it? If you have a scope shared, which can't actually get shared with anything else, what does it buy you?
parallel foreach.
Jun 18
prev sibling parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 6/17/2019 4:46 PM, Manu wrote:
 Is this valid?
 
 int x;
 void fun(scope ref shared(int) x) { ... }
 fun(x); // implicit promotion to shared in this case
 
 This appears to promote a thread-local to shared. The problem with
 such promotion is that it's not valid that a thread-local AND a shared
 reference to the same thing can exist at the same time.
 
 With scope, we can guarantee that the reference doesn't escape the callee.
 Since the argument is local to the calling thread, and since the
 calling thread can not be running other code at the same time as the
 call is executing, there is no way for any code to execute with a
 thread-local assumption while the callee makes shared assumptions.
 
 I think this might be safe?
 
Using the resulting value of x: int x; void fun(scope ref shared(int) x) { ... } fun(x); // implicit promotion to shared in this case int y = x; // add this line And now there's a problem. The compiler thinks x is thread local, so it doesn't add synchronization to the read of x. Not adding synchronization means that although fun() has terminated all the threads it spawned accessing x, it does not mean the memory caches of x are updated. While you may have coded fun() to update the caches of x before returning, the compiler can't know that, and so the compiler cannot allow the implicit conversion. In other words, adding `scope` does not guarantee SC-DRF (Sequential Consistency - Data Race Free).
Jun 19
next sibling parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 19.06.19 11:13, Walter Bright wrote:
 
 Using the resulting value of x:
 
    int x;
    void fun(scope ref shared(int) x) { ... }
    fun(x); // implicit promotion to shared in this case
    int y = x; // add this line
 
 And now there's a problem. The compiler thinks x is thread local, so it 
 doesn't add synchronization to the read of x.
Probably `fun` should be `immutable` or `shared` as well. Otherwise there is no way to tell at the call site that it does not also capture `x` mutably, in which case the implicit promotion couldn't be allowed.
 Not adding synchronization 
 means that although fun() has terminated all the threads it spawned 
 accessing x, it does not mean the memory caches of x are updated.
 ...
The trusted code that decided to send the `scope` reference to other threads has to make sure that all reads/writes to the `scope`d reference context are visible on the current thread before it returns. (Otherwise, from the perspective of the current thread, there are still copies of the `scope` reference around, even though the function call has terminated, which goes against `scope` guarantees.)
 While you may have coded fun() to update the caches of x before 
 returning, the compiler can't know that, and so the compiler cannot 
 allow the implicit conversion.
 ...
The compiler can allow the implicit conversion because in safe code there is no way to send `scope` data to other threads and trusted code has to respect `scope` guarantees.
 In other words, adding `scope` does not guarantee SC-DRF (Sequential 
 Consistency - Data Race Free).
I think it does. The challenge is to ensure that the implicit promotion to `shared` doesn't result in `shared`/unshared aliasing. I don't think there is a problem with implicitly casting back to unshared, as the called function promises not to leak the references.
Jun 19
parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 6/19/2019 6:45 AM, Timon Gehr wrote:
 On 19.06.19 11:13, Walter Bright wrote:
 Using the resulting value of x:

    int x;
    void fun(scope ref shared(int) x) { ... }
    fun(x); // implicit promotion to shared in this case
    int y = x; // add this line

 And now there's a problem. The compiler thinks x is thread local, so it 
 doesn't add synchronization to the read of x.
Probably `fun` should be `immutable` or `shared` as well. Otherwise there is no way to tell at the call site that it does not also capture `x` mutably, in which case the implicit promotion couldn't be allowed.
`scope` prevents capture of a reference to `x`, mutable or not, that persists after the return from `fun`.
 Not adding synchronization means that although fun() has terminated all the 
 threads it spawned accessing x, it does not mean the memory caches of x are 
 updated.
 ...
The trusted code that decided to send the `scope` reference to other threads has to make sure that all reads/writes to the `scope`d reference context are visible on the current thread before it returns. (Otherwise, from the perspective of the current thread, there are still copies of the `scope` reference around, even though the function call has terminated, which goes against `scope` guarantees.)
The way atomic synchronization of variables works is when a write is performed, a write access fence happens after. When a read access is performed, a read access fence is performed. The last write to shared x in fun will do a write access fence, but the read access fence won't happen until there's the read of shared x, the read access fence won't happen. But the x after the return of fun is not shared, no read access fence will happen, and the read may not see the results of the last write to shared x.
 While you may have coded fun() to update the caches of x before returning, the 
 compiler can't know that, and so the compiler cannot allow the implicit 
 conversion.
 ...
The compiler can allow the implicit conversion because in safe code there is no way to send `scope` data to other threads and trusted code has to respect `scope` guarantees.
The scope guarantees are NOT sequential consistency data-race-free guarantees for multithreaded access.
 In other words, adding `scope` does not guarantee SC-DRF (Sequential 
 Consistency - Data Race Free).
I think it does. The challenge is to ensure that the implicit promotion to `shared` doesn't result in `shared`/unshared aliasing. I don't think there is a problem with implicitly casting back to unshared, as the called function promises not to leak the references.
It does not. A write to an atomic is: write variable read fence A read from an atomic is: read fence read variable SC-DRF of atomics absolutely depends on this. The example code does: thread 1: write x read fence thread 2: read x This is wrong wrong wrong from what I know about atomics. The only way a local reference can be implicitly converted to a shared reference is if the compiler can prove there are no other local references to that memory location, which is essentially what Rust does. The read of `x` after `fun` returns violates that principle, and the result is a data race. --- There's another way to look at this: 1. When a politician promises a Free Lunch, you're gonna pay for it. 2. When a scientist discovers perpetual motion, he's made a mistake. 3. When a salesman sells you an effortless exercise machine, you won't get stronger. 4. When you respond to an ad for a "Get Rich Quick Through Real Estate" seminar, you're a sucker. -- and -- 5. When a data-race-free solution is found that doesn't follow fencing protocols, it doesn't work.
Jun 19
parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 19.06.19 23:03, Walter Bright wrote:
 On 6/19/2019 6:45 AM, Timon Gehr wrote:
 On 19.06.19 11:13, Walter Bright wrote:
 Using the resulting value of x:

    int x;
    void fun(scope ref shared(int) x) { ... }
    fun(x); // implicit promotion to shared in this case
    int y = x; // add this line

 And now there's a problem. The compiler thinks x is thread local, so 
 it doesn't add synchronization to the read of x.
Probably `fun` should be `immutable` or `shared` as well. Otherwise there is no way to tell at the call site that it does not also capture `x` mutably, in which case the implicit promotion couldn't be allowed.
`scope` prevents capture of a reference to `x`, mutable or not, that persists after the return from `fun`. ...
I was talking about this: int x0; void fun(scope ref shared(int) x1){ assert(&x0 is &x1); //uh, oh // ... } fun(x);
 ...
 The compiler can allow the implicit conversion because in  safe code 
 there is no way to send `scope` data to other threads and  trusted 
 code has to respect `scope` guarantees.
The scope guarantees are NOT sequential consistency data-race-free guarantees for multithreaded access. ...
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.
 
 In other words, adding `scope` does not guarantee SC-DRF (Sequential 
 Consistency - Data Race Free).
I think it does. The challenge is to ensure that the implicit promotion to `shared` doesn't result in `shared`/unshared aliasing. I don't think there is a problem with implicitly casting back to unshared, as the called function promises not to leak the references.
It does not. A write to an atomic is:     write variable     read fence A read from an atomic is:     read fence     read variable SC-DRF of atomics absolutely depends on this. The example code does:   thread 1:     write x     read fence   thread 2:     read x This is wrong wrong wrong from what I know about atomics. ...
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. 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. 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. 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?
 
 ...
 
 5. When a data-race-free solution is found that doesn't follow fencing 
 protocols, it doesn't work.
We are on the same page on this. :)
Jun 19
next sibling parent Timon Gehr <timon.gehr gmx.ch> writes:
On 20.06.19 00:04, Timon Gehr wrote:
 
 int x0;
 void fun(scope ref shared(int) x1){
      assert(&x0 is &x1); //uh, oh
      // ...
 }
 fun(x0);
(Sorry, was `fun(x)` instead of `fun(x0)`.)
Jun 19
prev sibling parent reply Walter Bright <newshound2 digitalmars.com> writes:
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. 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, 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.
 
 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, 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.
 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.
 5. When a data-race-free solution is found that doesn't follow fencing 
 protocols, it doesn't work.
We are on the same page on this. :)
Phew! Good!
Jun 19
parent reply Timon Gehr <timon.gehr gmx.ch> writes:
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.
Jun 21
parent reply Walter Bright <newshound2 digitalmars.com> writes:
1. `scope` requires that the reference to `x` passed to fun() does not persist 
past the return of fun(). If fun() is  trusted, fun() can do anything it likes, 
including passing the reference to other threads, as long as fun() does some 
sort of synchronization to ensure the other threads no longer hold that 
reference past the return of fun().

2. `scope` does not require memory cache coherency happen upon exit from fun(). 
Therefore, a non-atomic read of `x` after fun() exits is not guaranteed to have 
the latest value of `x`.

---

A corollary:

1. D has no ` safe` mechanism to convert shared references back to thread
local. 
In particular, `scope` isn't it.
Jun 21
parent Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Saturday, 22 June 2019 at 03:03:38 UTC, Walter Bright wrote:
 1. `scope` requires that the reference to `x` passed to fun() 
 does not persist past the return of fun(). If fun() is 
  trusted, fun() can do anything it likes, including passing the 
 reference to other threads, as long as fun() does some sort of 
 synchronization to ensure the other threads no longer hold that 
 reference past the return of fun().

 2. `scope` does not require memory cache coherency happen upon 
 exit from fun(). Therefore, a non-atomic read of `x` after 
 fun() exits is not guaranteed to have the latest value of `x`.
So you basically say that you need "x" to be typed as const when calling fun() if fun() transfers "x" to another thread. Ola
Jun 21
prev sibling parent reply Manu <turkeyman gmail.com> writes:
On Wed, Jun 19, 2019 at 7:15 PM Walter Bright via Digitalmars-d
<digitalmars-d puremagic.com> wrote:
 On 6/17/2019 4:46 PM, Manu wrote:
 Is this valid?

 int x;
 void fun(scope ref shared(int) x) { ... }
 fun(x); // implicit promotion to shared in this case

 This appears to promote a thread-local to shared. The problem with
 such promotion is that it's not valid that a thread-local AND a shared
 reference to the same thing can exist at the same time.

 With scope, we can guarantee that the reference doesn't escape the callee.
 Since the argument is local to the calling thread, and since the
 calling thread can not be running other code at the same time as the
 call is executing, there is no way for any code to execute with a
 thread-local assumption while the callee makes shared assumptions.

 I think this might be safe?
Using the resulting value of x: int x; void fun(scope ref shared(int) x) { ... } fun(x); // implicit promotion to shared in this case int y = x; // add this line And now there's a problem. The compiler thinks x is thread local, so it doesn't add synchronization to the read of x. Not adding synchronization means that although fun() has terminated all the threads it spawned accessing x, it does not mean the memory caches of x are updated. While you may have coded fun() to update the caches of x before returning, the compiler can't know that, and so the compiler cannot allow the implicit conversion. In other words, adding `scope` does not guarantee SC-DRF (Sequential Consistency - Data Race Free).
Well, you're assuming that `fun()` is an unsafe function that did violate the scope agreement and passed it out to worker threads. *ANY* deployment of trusted code requires that you know what you're doing. So, rather, let's assume that fun() does NOT do that, and it is in fact safe... your concern does not arise in that world where you're not manually violating the type system. So the issue you describe is only possible where you have deliberately written unsafe code to perform a very low-level operation. You're obviously incompetent if you don't implement the sequential consistency machinery at the conclusion of your low-level machine. I don't think saying "this person wasn't qualified to implement a trusted function" is a valid reason to reject an extremely useful improvement. TL;DR: I don't think your argument is valid. If it were, you would have to apply that reasoning to every trusted function out there. Language features are not required to consider that a trusted author may make a mistake; that is literally the point of trusted.
Jun 19
parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 6/19/2019 11:53 PM, Manu wrote:
 Well, you're assuming that `fun()` is an unsafe function that did
 violate the scope agreement and passed it out to worker threads.
Not at all. Scope means it does not leak a reference that survives the end of the function. As long as the threads terminate before the function returns, it is valid code.
Jun 21
parent reply Manu <turkeyman gmail.com> writes:
On Fri, Jun 21, 2019 at 5:55 PM Walter Bright via Digitalmars-d
<digitalmars-d puremagic.com> wrote:
 On 6/19/2019 11:53 PM, Manu wrote:
 Well, you're assuming that `fun()` is an unsafe function that did
 violate the scope agreement and passed it out to worker threads.
Not at all. Scope means it does not leak a reference that survives the end of the function. As long as the threads terminate before the function returns, it is valid code.
I agree it's 'theoretically' valid; that's why I suggested it and the construct makes sense. How would you write code that escapes the scope reference to another thread? This must require some trusted code to achieve sharing between threads; at that point, the trusted function is responsible for re-instating the thread-locality guarantees (which are trivial to implement). If you don't do trusted mischief, there's no way to migrate the value across threads, so your synchronisation issue shouldn't exist if no trusted code exists.
Jun 21
parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 6/21/2019 1:17 AM, Manu wrote:
 I agree it's 'theoretically' valid; that's why I suggested it and the
 construct makes sense.
 How would you write code that escapes the scope reference to another
 thread? This must require some  trusted code to achieve sharing
 between threads; at that point, the  trusted function is responsible
 for re-instating the thread-locality guarantees (which are trivial to
 implement). If you don't do  trusted mischief, there's no way to
 migrate the value across threads, so your synchronisation issue
 shouldn't exist if no  trusted code exists.
I attempted to explain this before. The trouble does NOT come from the shared function escaping a reference. It comes from the lack of memory coherency between threads. Scope says NOTHING about synchronizing access to shared memory. NOTHING. Just because there's no longer a reference to the shared memory location from another thread does not mean all the memory caches are synchronized. For that, synchronization is needed, and your load of thread local `x` does NOT include the necessary synchronization. If `x` was types as shared, the compiler WOULD include the necessary synchronization when reading it. That's what atomic reads are for. It's the entire point of atomic reads.
Jun 21
parent reply Manu <turkeyman gmail.com> writes:
On Fri, Jun 21, 2019 at 7:05 PM Walter Bright via Digitalmars-d
<digitalmars-d puremagic.com> wrote:
 On 6/21/2019 1:17 AM, Manu wrote:
 I agree it's 'theoretically' valid; that's why I suggested it and the
 construct makes sense.
 How would you write code that escapes the scope reference to another
 thread? This must require some  trusted code to achieve sharing
 between threads; at that point, the  trusted function is responsible
 for re-instating the thread-locality guarantees (which are trivial to
 implement). If you don't do  trusted mischief, there's no way to
 migrate the value across threads, so your synchronisation issue
 shouldn't exist if no  trusted code exists.
I attempted to explain this before.
And we heard you.
 The trouble does NOT come from the shared
 function escaping a reference. It comes from the lack of memory coherency
 between threads.
There is no "between threads" unless you have already entered trusted land. The reason for my proposal in the OP is precisely to prevent passing data to other threads, that's the key that makes this promotion safe. Can you show how you managed to get a ref to another thread without entering trusted?
 Scope says NOTHING about synchronizing access to shared memory.
Yes, obviously. But the point is to prevent that situation occurring in the first place. There's no possibility of the synchronisation issue arising unless there's some way to escape a scope ref to another thread, which scope prevents. Can you show how to escape a scope ref to another thread?
 NOTHING.
No really, we get it. This isn't a revelation to anyone.
 Just
 because there's no longer a reference to the shared memory location from
another
 thread does not mean all the memory caches are synchronized. For that,
 synchronization is needed, and your load of thread local `x` does NOT include
 the necessary synchronization.
So, as I attempted to explain before; there ARE NO OTHER THREADS unless you engage in trusted activity. There are no synchronisation issues unless there are other threads, and there are no other threads because you couldn't possibly get a reference to another thread. Can you show how you escaped a reference to another thread without entering trusted territory? Now, assuming we intend to write a trusted machine to implement a parallel for, it's trivial to insert the 4 memory barriers to maintain data consistency. This is fine, we're in unsafe land, and there is no way to get to this situation without going unsafe.
 If `x` was types as shared, the compiler WOULD include the necessary
 synchronization when reading it.
It had better not. I don't believe it can know how to do that correctly. I don't think a type qualifier is even remotely enough information for the compiler to implement data fencing correctly and efficiently.
 That's what atomic reads are for. It's the
 entire point of atomic reads.
We're not talking about atomic reads here, we're talking about memory barriers. `shared` just needs to separate thread-local from shared, and make transfer between those two states unsafe. Then we can write the libraries we need. Remove all access from shared, then the only way we can do anything with shared is to cast it away, which is a system operation and as such, places responsibility on us as library authors to do the right stuff. Novices don't write threading libraries. They tend to be very small in surface area, and written once. It'll be fine, and it gives us the control we need. Anything more from the language at this very early stage is an over-reach. If you want to explore safe expansions to shared, then propose them as follow up, because they will be very controversial, whereas the foundation stuff is uncomplicated and mostly non-controversial. `shared` the language primitive is just not safe, and let us write the libraries.
Jun 21
next sibling parent reply matheus <matheus gmail.com> writes:
On Friday, 21 June 2019 at 12:07:06 UTC, Manu wrote:
 Novices don't write threading libraries. They tend to be very 
 small in
 surface area, and written once. It'll be fine, and it gives us 
 the
 control we need.
 Anything more from the language at this very early stage is an 
 over-reach.
Sorry but I don't think this is true, or at least need some facts/numbers. I've seen "novices" or different type of people doing things that they shouldn't. For example, in the last 2 places that I worked, I saw management put developers (Like Desktop) without any experience in web to write Web APIs and the result was terrible, but they need to do otherwise they would be fired. So just say novices barely it's to shallow. By the way even experienced programmers can commit mistakes, and they way you talk you assume they not, and bugs are out there in most software and even from big companies. Matheus.
Jun 21
parent Manu <turkeyman gmail.com> writes:
On Fri, Jun 21, 2019 at 11:27 PM matheus via Digitalmars-d
<digitalmars-d puremagic.com> wrote:
 On Friday, 21 June 2019 at 12:07:06 UTC, Manu wrote:
 Novices don't write threading libraries. They tend to be very
 small in
 surface area, and written once. It'll be fine, and it gives us
 the
 control we need.
 Anything more from the language at this very early stage is an
 over-reach.
Sorry but I don't think this is true, or at least need some facts/numbers. I've seen "novices" or different type of people doing things that they shouldn't. For example, in the last 2 places that I worked, I saw management put developers (Like Desktop) without any experience in web to write Web APIs and the result was terrible, but they need to do otherwise they would be fired. So just say novices barely it's to shallow. By the way even experienced programmers can commit mistakes, and they way you talk you assume they not, and bugs are out there in most software and even from big companies.
1. This discussion only applies when writing system code; I think it's assumed that you take responsibility for your mistakes, no? 2. I'm not against safe improvements, I just think that's VERY hard to design, and we should have a useful low-level core in the language before spending years arguing about some semantics for safe interactions (which I'm not sure exist; I think they are libraries).
Jun 21
prev sibling next sibling parent reply Kagamin <spam here.lot> writes:
On Friday, 21 June 2019 at 12:07:06 UTC, Manu wrote:
 There is no "between threads" unless you have already entered 
  trusted land.
trusted doesn't break the guarantees advertized by the type system; when it does, it's a bug that should be fixed (in code, not in language). It's only the compiler is not smart enough to verify that trusted code uphold the type system.
Jun 21
parent reply Meta <jared771 gmail.com> writes:
On Friday, 21 June 2019 at 17:32:08 UTC, Kagamin wrote:
 On Friday, 21 June 2019 at 12:07:06 UTC, Manu wrote:
 There is no "between threads" unless you have already entered 
  trusted land.
trusted doesn't break the guarantees advertized by the type system; when it does, it's a bug that should be fixed (in code, not in language). It's only the compiler is not smart enough to verify that trusted code uphold the type system.
trusted is allowed to do whatever it wants internally, as long as it provides a *safe* interface. I believe that's what Manu's getting at in regards to his safe/ trusted distinction in regards to threading.
Jun 21
parent Kagamin <spam here.lot> writes:
On Friday, 21 June 2019 at 18:11:38 UTC, Meta wrote:
  trusted is allowed to do whatever it wants internally, as long 
 as it provides a *safe* interface. I believe that's what Manu's 
 getting at in regards to his  safe/ trusted distinction in 
 regards to threading.
Concurrency is indeed safe with that interface, so there's no reason to assume that it doesn't happen, and implicit conversion from thread local to shared is illegal.
Jun 22
prev sibling parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 6/21/2019 5:07 AM, Manu wrote:
 There is no "between threads" unless you have already entered  trusted land.
 The reason for my proposal in the OP is precisely to prevent passing
 data to other threads, that's the key that makes this promotion safe.
Scope doesn't do that. I keep saying this. But the premise is strange, anyway, as why would you want to convert a reference to shared if not to bass it to another thread?
 Can you show how you managed to get a ref to another thread without
 entering  trusted?
There's no such thing as a ref to another thread. You can pass a ref to another thread in a scope function, as long as the ref doesn't survive the scope of foo(). The compiler cannot check that, though, and so cannot bless the implicit conversion to shared. I've said this 4 or 5 times now.
 There's no possibility of the synchronisation
 issue arising unless there's some way to escape a scope ref to another
 thread, which scope prevents.
You're inventing semantics for scope that I've repeatedly told you are not there.
 We're not talking about atomic reads here, we're talking about memory barriers.
I recommend watching: https://www.youtube.com/watch?v=A8eCGOqgvH4 https://www.youtube.com/watch?v=KeLBd2EJLOU as you and I are not speaking the same language. In particular the sections on how atomics create sequentially consistent data-race-free reads and writes of variables. Not needing to read x atomically after foo() ends is just plain wrong.
Jun 21
next sibling parent reply Manu <turkeyman gmail.com> writes:
On Sat., 22 Jun. 2019, 9:40 am Walter Bright via Digitalmars-d, <
digitalmars-d puremagic.com> wrote:

 On 6/21/2019 5:07 AM, Manu wrote:
 There is no "between threads" unless you have already entered  trusted
land.
 The reason for my proposal in the OP is precisely to prevent passing
 data to other threads, that's the key that makes this promotion safe.
Scope doesn't do that. I keep saying this. But the premise is strange, anyway, as why would you want to convert a reference to shared if not to bass it to another thread?
 Can you show how you managed to get a ref to another thread without
 entering  trusted?
There's no such thing as a ref to another thread. You can pass a ref to another thread in a scope function, as long as the ref doesn't survive the scope of foo(). The compiler cannot check that, though, and so cannot bless the implicit conversion to shared. I've said this 4 or 5 times now.
 There's no possibility of the synchronisation
 issue arising unless there's some way to escape a scope ref to another
 thread, which scope prevents.
You're inventing semantics for scope that I've repeatedly told you are not there.
I've asked you 5 or 6 times to show how to pass a pointer to another thread without violating scope. Can you show it? You keep saying you can.
 We're not talking about atomic reads here, we're talking about memory
 barriers.

 I recommend watching:

 https://www.youtube.com/watch?v=A8eCGOqgvH4
 https://www.youtube.com/watch?v=KeLBd2EJLOU

 as you and I are not speaking the same language. In particular the
 sections on
 how atomics create sequentially consistent data-race-free reads and writes
 of
 variables. Not needing to read x atomically after foo() ends is just plain
 wrong.
You need an acquire fence before the function returns, it's trivial. In many cases though you don't need one, because you may have already required one to determine that the workload is complete in the first place. Don't attempt this in the system language. Just let it be, we know what we're doing. If you want safe expansion, that's future work, and I personally couldn't care less about that work. I can create safe libraries with trusted functions, and I could do it _right now_.

Jun 21
parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 6/21/2019 5:26 PM, Manu wrote:
 I've asked you 5 or 6 times to show how to pass a pointer to another thread 
 without violating scope.
 Can you show it? You keep saying you can.
void foo(scope shared(int)* p) trusted { auto t = startThread(); passToThread(t, p); waitForThreadToExit(t); }
 You need an acquire fence before the function returns, it's trivial.
That's right it is. But the compiler doesn't know you put one there, and scope does not cause it to be put there, and safe does not, either.
 If you want  safe expansion, that's future work, and I personally couldn't
care 
 less about that work. I can create safe libraries with  trusted functions, and
I 
 could do it _right now_.
You're assuming semantics for scope and safe that are not there, and so the compiler cannot assume them, either.
Jun 22
next sibling parent reply Manu <turkeyman gmail.com> writes:
On Sat, Jun 22, 2019 at 5:57 PM Walter Bright via Digitalmars-d
<digitalmars-d puremagic.com> wrote:
 On 6/21/2019 5:26 PM, Manu wrote:
 I've asked you 5 or 6 times to show how to pass a pointer to another thread
 without violating scope.
 Can you show it? You keep saying you can.
void foo(scope shared(int)* p) trusted { auto t = startThread(); passToThread(t, p); waitForThreadToExit(t); }
I don't believe `passToThread` could receive a `scope shared(int)*`... can you show an implementation of passToThread?
 You need an acquire fence before the function returns, it's trivial.
That's right it is. But the compiler doesn't know you put one there, and scope does not cause it to be put there, and safe does not, either.
There couldn't have been cross-thread communication if it was safe.
 If you want  safe expansion, that's future work, and I personally couldn't care
 less about that work. I can create safe libraries with  trusted functions, and
I
 could do it _right now_.
You're assuming semantics for scope and safe that are not there, and so the compiler cannot assume them, either.
The compiler can assume that you didn't pass a scope value to another thread, because it would be impossible to do. Show how to do it? Your example above shows passToThread, but I'm just going to ask you to show how that function works, and we'll recurse until either the value reaches another thread, or you agree that scope prevents a value from escaping...
Jun 22
parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 6/22/2019 1:02 AM, Manu wrote:
 I don't believe `passToThread` could receive a `scope shared(int)*`...
 can you show an implementation of passToThread?
In system code you can do whatever you like.
Jun 22
parent reply Manu <turkeyman gmail.com> writes:
On Sat., 22 Jun. 2019, 6:40 pm Walter Bright via Digitalmars-d, <
digitalmars-d puremagic.com> wrote:

 On 6/22/2019 1:02 AM, Manu wrote:
 I don't believe `passToThread` could receive a `scope shared(int)*`...
 can you show an implementation of passToThread?
In system code you can do whatever you like.
If you engage in system code to distribute across threads, it's a no brainer to expect that code to handle cache coherency measures. It would be broken if it didn't.

Jun 22
parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 6/22/2019 4:24 PM, Manu wrote:
 If you engage in  system code to distribute across threads, it's a no brainer
to 
 expect that code to handle cache coherency measures. It would be broken if it 
 didn't.
It is not a no-brainer. Nowhere is it specified, and all the examples and tutorials I've seen say DO NOT follow an atomic write with a non-atomic read of the same variable in another thread. Not only that, it's usually a CENTRAL THEME of these expositions. The only synchronization required (will be) for atomics, and that assumes atomic write followed by atomic read. Not atomic write followed by non-atomic read of the same memory. The code may appear to work on the x86 because Intel CPUs do some additional not-required synchronization on reads. But it'll be leaving a nightmare for some poor sap who tries to port the code to the ARM.
Jun 23
parent Timon Gehr <timon.gehr gmx.ch> writes:
On 24.06.19 04:44, Walter Bright wrote:
 On 6/22/2019 4:24 PM, Manu wrote:
 If you engage in  system code to distribute across threads, it's a no 
 brainer to expect that code to handle cache coherency measures. It 
 would be broken if it didn't.
It is not a no-brainer. Nowhere is it specified, and all the examples and tutorials I've seen say DO NOT follow an atomic write with a non-atomic read of the same variable in another thread. Not only that, it's usually a CENTRAL THEME of these expositions. ...
Are you saying code like the following would have UB? __gshared int x=0, y=0; void thread1(){ x.write(1,release); y.write(1,release); } void thread2(){ if(y.read(acquire)==1){ assert(x==1); // plain read } }
Jun 24
prev sibling parent Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Saturday, 22 June 2019 at 07:53:49 UTC, Walter Bright wrote:
 On 6/21/2019 5:26 PM, Manu wrote:
 You need an acquire fence before the function returns, it's 
 trivial.
That's right it is. But the compiler doesn't know you put one there, and scope does not cause it to be put there, and safe does not, either.
I think perhaps it would be more clear if you make a distinction between: 1. fences that inform the compiler that "virtual registers" could be stale (conceptual entities at compile time) 2. fences targeting cache coherency (that hardware caches are stale at runtime). If fun() is separately compiled then there should be no distinction. The calling context must assume that fun() might have changed the values. So it does not affect (1). And if fun() flushes the caches explicitly or explictly loads the values changed by other threads after they are done, then it should not affect (2) either. Right? Ola.
Jun 22
prev sibling parent reply Manu <turkeyman gmail.com> writes:
On Sat., 22 Jun. 2019, 9:40 am Walter Bright via Digitalmars-d, <
digitalmars-d puremagic.com> wrote:

 On 6/21/2019 5:07 AM, Manu wrote:
 There is no "between threads" unless you have already entered  trusted
land.
 The reason for my proposal in the OP is precisely to prevent passing
 data to other threads, that's the key that makes this promotion safe.
Scope doesn't do that. I keep saying this. But the premise is strange, anyway, as why would you want to convert a reference to shared if not to bass it to another thread?
Because it's safe to do from the calling scope's perspective, and that's the needed entry vector into a trusted solution. It is also useful too in some cases to reduce needlessly overloading methods which have a threadsafe implementation.
 Can you show how you managed to get a ref to another thread without
 entering  trusted?
There's no such thing as a ref to another thread. You can pass a ref to another thread in a scope function, as long as the ref doesn't survive the scope of foo(). The compiler cannot check that, though, and so cannot bless the implicit conversion to shared. I've said this 4 or 5 times now.
Compiler is not required to check the users work in trusted code. It assumes the user conformed with language requirements.
Jun 21
parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 6/21/2019 5:33 PM, Manu wrote:
 Because it's safe to do from the calling scope's perspective,
No, it is not.
 Compiler is not required to check the users work in  trusted code. It assumes 
 the user conformed with language requirements.
The language does not require you to put the fence in, hence it cannot assume you did. I don't know how else to explain it to you. Your example does not work.
Jun 22
next sibling parent reply Manu <turkeyman gmail.com> writes:
On Sat, Jun 22, 2019 at 6:00 PM Walter Bright via Digitalmars-d
<digitalmars-d puremagic.com> wrote:
 On 6/21/2019 5:33 PM, Manu wrote:
 Because it's safe to do from the calling scope's perspective,
No, it is not.
 Compiler is not required to check the users work in  trusted code. It assumes
 the user conformed with language requirements.
The language does not require you to put the fence in, hence it cannot assume you did. I don't know how else to explain it to you. Your example does not work.
Your code snippet is incomplete; show the implementation of passToThread()...
Jun 22
parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 6/22/2019 1:03 AM, Manu wrote:
 Your code snippet is incomplete; show the implementation of passToThread()...
Doesn't make any difference.
Jun 22
next sibling parent Paolo Invernizzi <paolo.invernizzi gmail.com> writes:
On Saturday, 22 June 2019 at 08:36:18 UTC, Walter Bright wrote:
 On 6/22/2019 1:03 AM, Manu wrote:
 Your code snippet is incomplete; show the implementation of 
 passToThread()...
Doesn't make any difference.
Oh, came on! /P
Jun 22
prev sibling parent reply Manu <turkeyman gmail.com> writes:
On Sat., 22 Jun. 2019, 6:40 pm Walter Bright via Digitalmars-d, <
digitalmars-d puremagic.com> wrote:

 On 6/22/2019 1:03 AM, Manu wrote:
 Your code snippet is incomplete; show the implementation of
passToThread()... Doesn't make any difference.
It really does. If you can't get the reference to another thread, then everything you say is moot, and that's the whole point. Show how to escape the pointer when it is scope...?

Jun 22
parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 6/22/2019 4:22 PM, Manu wrote:
 Show how to escape the pointer when it is scope...?
Use system code. The programmer only has to ensure the escaped reference ceases before the function returns to fulfill the 'scope' semantics. The scope semantics do not include memory barriers, though.
Jun 23
parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 24.06.19 04:31, Walter Bright wrote:
 On 6/22/2019 4:22 PM, Manu wrote:
 Show how to escape the pointer when it is scope...?
Use system code. The programmer only has to ensure the escaped reference ceases before the function returns to fulfill the 'scope' semantics. The scope semantics do not include memory barriers, though.
You keep saying "before" and then "but no memory barriers". I really don't understand this "before" without memory barriers. Without memory barriers, there is no "before". How to formalize this? What is it exactly that "scope" guarantees?
Jun 23
parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 6/23/2019 7:42 PM, Timon Gehr wrote:
 I really don't understand this "before" without memory barriers. Without
memory 
 barriers, there is no "before". How to formalize this? What is it exactly that 
 "scope" guarantees?
The reference to shared i does not extend past the end of the function call. That doesn't mean the cached value of i is propagated to all other threads. int x; void fun(scope ref shared(int) x) { x = 3; // (*) } // end of reference to x is guaranteed assert(x == 3); // all caches not updated, assert is not guaranteed (*) now replace this with a call to another thread to set x to 3.
Jun 23
parent reply aliak <something something.com> writes:
On Monday, 24 June 2019 at 02:55:39 UTC, Walter Bright wrote:
 On 6/23/2019 7:42 PM, Timon Gehr wrote:
 I really don't understand this "before" without memory 
 barriers. Without memory barriers, there is no "before". How 
 to formalize this? What is it exactly that "scope" guarantees?
The reference to shared i does not extend past the end of the function call. That doesn't mean the cached value of i is propagated to all other threads. int x; void fun(scope ref shared(int) x) { x = 3; // (*) } // end of reference to x is guaranteed assert(x == 3); // all caches not updated, assert is not guaranteed (*) now replace this with a call to another thread to set x to 3.
You can assign to a shared primitive without a compilation error? Does the compiler do the necessary synchronization under the hood? I would've assumed you'd have to use atomicStore or the like?
Jun 24
next sibling parent XavierAP <n3minis-git yahoo.es> writes:
On Monday, 24 June 2019 at 11:38:29 UTC, aliak wrote:
 On Monday, 24 June 2019 at 02:55:39 UTC, Walter Bright wrote:
     int x;
     void fun(scope ref shared(int) x) {
         x = 3; // (*)
     } // end of reference to x is guaranteed
     assert(x == 3); // all caches not updated, assert is not 
 guaranteed

 (*) now replace this with a call to another thread to set x to 
 3.
You can assign to a shared primitive without a compilation error? Does the compiler do the necessary synchronization under the hood? I would've assumed you'd have to use atomicStore or the like?
(*) now replace this with a call to another thread to set x to 3.
Jun 24
prev sibling next sibling parent aliak <something something.com> writes:
On Monday, 24 June 2019 at 11:38:29 UTC, aliak wrote:
 On Monday, 24 June 2019 at 02:55:39 UTC, Walter Bright wrote:
 On 6/23/2019 7:42 PM, Timon Gehr wrote:
 [...]
The reference to shared i does not extend past the end of the function call. That doesn't mean the cached value of i is propagated to all other threads. int x; void fun(scope ref shared(int) x) { x = 3; // (*) } // end of reference to x is guaranteed assert(x == 3); // all caches not updated, assert is not guaranteed (*) now replace this with a call to another thread to set x to 3.
You can assign to a shared primitive without a compilation error? Does the compiler do the necessary synchronization under the hood? I would've assumed you'd have to use atomicStore or the like?
ok so I guess you can indeed do that: https://d.godbolt.org/z/Ge7bTc Is that intended behavior? I would've expected an `xchg` or lock xchg or something?
Jun 24
prev sibling parent reply Jonathan M Davis <newsgroup.d jmdavisprog.com> writes:
On Monday, June 24, 2019 5:38:29 AM MDT aliak via Digitalmars-d wrote:
 On Monday, 24 June 2019 at 02:55:39 UTC, Walter Bright wrote:
 On 6/23/2019 7:42 PM, Timon Gehr wrote:
 I really don't understand this "before" without memory
 barriers. Without memory barriers, there is no "before". How
 to formalize this? What is it exactly that "scope" guarantees?
The reference to shared i does not extend past the end of the function call. That doesn't mean the cached value of i is propagated to all other threads. int x; void fun(scope ref shared(int) x) { x = 3; // (*) } // end of reference to x is guaranteed assert(x == 3); // all caches not updated, assert is not guaranteed (*) now replace this with a call to another thread to set x to 3.
You can assign to a shared primitive without a compilation error? Does the compiler do the necessary synchronization under the hood? I would've assumed you'd have to use atomicStore or the like?
A number of us think that simply reading or assigning shared variables should be illegal, because the compiler doesn't do the necessary synchronization stuff for you. Currently, the compiler prevents it for ++ and -- on than basis, but that's it. However, one issue with that from a usability perspective is that even if you're doing all of the right stuff like protecting access to the shared variable via a mutex and then temporarily casting it to thread-local to operate on it while the mutex is locked, if you want to actually assign anything to the shared variable before releasing the mutex, you'd have to use a pointer to it that had been cast to thread-local, whereas right now, you're actually allowed to just assign to it. e.g. shared MyClass obj; synchronized(mutex) { obj = cast(shared)someObj; } works right now, but if writing to shared variables were illegal like it arguably should be, then you'd have to do something ugly like shared MyClass obj; synchronized(mutex) { auto ptr = cast(MyClass*)&obj; *ptr = someObj; } Either way, it's quite clear that as things stand, reading or writing a shared variable without doing something with threading primitives is non-atomic and won't be synchronized properly. So, arguably, the type system shouldn't allow it (and that was started by disalowing ++ and --, but it wasn't ever finished). However, when this was discussed at this last dconf, Walter and Andrei didn't want to make any changes along those lines until we'd nailed down the exact semantics of how shared is supposed to work. Right now, shared is pretty much just defined as not being thread-local without the finer details really being ironed out. So, for now, shared does prevent you from accidentally converting between thread-local and shared, but on the whole, it doesn't actually do anything to prevent you from doing stuff to a shared variable that isn't going to be atomic or thread-safe, and it doesn't introduce stuff like fences to synchronize anything across threads. It's entirely up to the programmer to use it correctly with almost no help from the compiler. All it's really doing is segregating the shared data from the thread-local data and preventing you from crossing that barrier without casting, making such code system. And that's definitely something useful, but the specification needs to be fully nailed down, and there are clearly some improvements that need to be made - though of course, a lot of the arguing is over the changes that should be made. Jonathan M Davis
Jun 24
parent reply aliak <something something.com> writes:
On Monday, 24 June 2019 at 13:20:23 UTC, Jonathan M Davis wrote:
 However, one issue with that from a usability perspective is 
 that even if you're doing all of the right stuff like 
 protecting access to the shared variable via a mutex and then 
 temporarily casting it to thread-local to operate on it while 
 the mutex is locked, if you want to actually assign anything to 
 the shared variable before releasing the mutex, you'd have to 
 use a pointer to it that had been cast to thread-local, whereas 
 right now, you're actually allowed to just assign to it. e.g.

 shared MyClass obj;

 synchronized(mutex)
 {
     obj = cast(shared)someObj;
 }

 works right now, but if writing to shared variables were 
 illegal like it arguably should be, then you'd have to do 
 something ugly like

 shared MyClass obj;

 synchronized(mutex)
 {
     auto ptr = cast(MyClass*)&obj;
     *ptr = someObj;
 }
You can have the same usability/aesthetics: synchronized { cast(MyClass)obj = someObj; } Doesn't that work? Ideally though, I think you'd want: class MyClass { void opAssign(MyClass c) shared { ... } }
Jun 24
parent Jonathan M Davis <newsgroup.d jmdavisprog.com> writes:
On Monday, June 24, 2019 4:06:03 PM MDT aliak via Digitalmars-d wrote:
 On Monday, 24 June 2019 at 13:20:23 UTC, Jonathan M Davis wrote:
 However, one issue with that from a usability perspective is
 that even if you're doing all of the right stuff like
 protecting access to the shared variable via a mutex and then
 temporarily casting it to thread-local to operate on it while
 the mutex is locked, if you want to actually assign anything to
 the shared variable before releasing the mutex, you'd have to
 use a pointer to it that had been cast to thread-local, whereas
 right now, you're actually allowed to just assign to it. e.g.

 shared MyClass obj;

 synchronized(mutex)
 {

     obj = cast(shared)someObj;

 }

 works right now, but if writing to shared variables were
 illegal like it arguably should be, then you'd have to do
 something ugly like

 shared MyClass obj;

 synchronized(mutex)
 {

     auto ptr = cast(MyClass*)&obj;
     *ptr = someObj;

 }
You can have the same usability/aesthetics: synchronized { cast(MyClass)obj = someObj; } Doesn't that work?
I've never seen a cast used in that manner. It's been my understanding that casts always result in rvalues, but it's quite possible that your example works, and I've just made a wrong assumption about how casts work with regards to lvalues. Right or wrong though, I'm not the only one who has thought that, since this issue has been brought up before. But even if it doesn't work, it would be a possible improvement to make dealing with shared more palatable.
 Ideally though, I think you'd want:

 class MyClass {
    void opAssign(MyClass c) shared { ... }
 }
Sure, but if you're doing that, that means that the class itself is handling its own synchronization, which doesn't work in all cases, and even if it did, it's just pushing the problem into the class. The implementation for opAssign is going to have deal with assigning to types that don't have shared opAssign where either atomic writes or mutexes with casts or whatnot are going to be needed. In general, encapsulating such code is great where possible, but at some point, the built-in types are going to need to be manipulated, and if the type is a user-defined type that's designed to be thread-local but works as shared with mutexes and casts, then it's not going to have a shared opAssign any more than an int or string will. So, such an opAssign is just encapsulating the problem, not making it go away. - Jonathan M Davis
Jun 24
prev sibling next sibling parent reply Jonathan M Davis <newsgroup.d jmdavisprog.com> writes:
On Saturday, June 22, 2019 1:57:22 AM MDT Walter Bright via Digitalmars-d 
wrote:
 On 6/21/2019 5:33 PM, Manu wrote:
 Because it's safe to do from the calling scope's perspective,
No, it is not.
 Compiler is not required to check the users work in  trusted code. It
 assumes the user conformed with language requirements.
The language does not require you to put the fence in, hence it cannot assume you did. I don't know how else to explain it to you. Your example does not work.
If I understand correctly, Manu and Timon are arguing that because you're dealing with scope with only thread-local variables, the compiler knows that the state of the thread-local variable is properly up-to-date, because in safe code, it's impossible for it to be otherwise. And then if your trusted code ensures that the data on the original thread is up-to-date after it does its thing with using the object on other threads, then it all works. And as such, they think that it should be fine for the compiler to implicitly convert to shared with scope, because it's up to the programmer to make sure that all of the normal guarantees are in place and none of the guarantees are violated without trusted being involved. This makes sense to me as long as the programmer is doing the casting, because then safe still isn't doing anything with shared. It's just the programmer doing what trusted needs to do and making sure that the normal guarantees are maintained. The problem is that if the compiler then implicitly converts to scope shared in safe code, then all of a sudden, safe needs to worry about whether the variable is properly up-to-date, since it's no longer just assuming it based on it being thread-local. At best, it's assuming that the scope shared variable will never actually be shared across threads, since without trusted, it wouldn't be possible to share it across threads. And if it's assuming that, then why on earth would it implicitly convert the variable to shared? This whole idea seems to rest on the premise that the compiler will do a conversion for you, because it can't be a problem in safe code without trusted being involved, but it's also completely useless without trusted being involved, and the compiler doesn't make assumptions based on trusted. It makes them based on safe. trusted is just the way for the programmer to provide something that the compiler treats as safe even though it's the programmer making sure that it is instead of the compiler being able to actually verify it. It's not going to treat an trusted function any differently from an safe one aside from the mangling of the function name, and arguably, safe and trusted should have had the same name mangling in the first place, since there is no difference from the standpoint of the caller. The difference is only in how the implementation is checked by the compiler. As long as safe code doesn't actually benefit from the implicit conversion to scope shared, it makes no sense to me to have it happen. The entire point is for it to be used with trusted stuff. And if that's the case, then why not just have the cast be explicit and trusted like it normally would be? - Jonathan M Davis
Jun 22
parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 22.06.19 22:32, Jonathan M Davis wrote:
 As long as  safe code doesn't actually benefit from the implicit conversion
 to scope shared, it makes no sense to me to have it happen. The entire point
 is for it to be used with  trusted stuff. And if that's the case, then why
 not just have the cast be explicit and  trusted like it normally would be?
Why would safe code not benefit? Manu's use case is safe code calling into a trusted library.
Jun 22
next sibling parent Jonathan M Davis <newsgroup.d jmdavisprog.com> writes:
On Saturday, June 22, 2019 2:52:33 PM MDT Timon Gehr via Digitalmars-d 
wrote:
 On 22.06.19 22:32, Jonathan M Davis wrote:
 As long as  safe code doesn't actually benefit from the implicit
 conversion to scope shared, it makes no sense to me to have it happen.
 The entire point is for it to be used with  trusted stuff. And if
 that's the case, then why not just have the cast be explicit and
  trusted like it normally would be?
Why would safe code not benefit? Manu's use case is safe code calling into a trusted library.
Because if all of the code is safe, then the fact that it was converted to scope shared gains you nothing. It's not really shared across threads, and it doesn't enable any useful operations. The only time that you can do anything extra with it is when you then have trusted code within the safe code, and the trusted code then does something like temporarily pass a reference to another thread. And if the only time that the implicit conversion to scope shared is actually useful is when you have trusted code, then why not just have the cast be explicit and trusted? - Jonathan M Davis
Jun 22
prev sibling parent reply Manu <turkeyman gmail.com> writes:
On Sun., 23 Jun. 2019, 7:08 am Jonathan M Davis via Digitalmars-d, <
digitalmars-d puremagic.com> wrote:

 On Saturday, June 22, 2019 2:52:33 PM MDT Timon Gehr via Digitalmars-d
 wrote:
 On 22.06.19 22:32, Jonathan M Davis wrote:
 As long as  safe code doesn't actually benefit from the implicit
 conversion to scope shared, it makes no sense to me to have it happen.
 The entire point is for it to be used with  trusted stuff. And if
 that's the case, then why not just have the cast be explicit and
  trusted like it normally would be?
Why would safe code not benefit? Manu's use case is safe code calling into a trusted library.
Because if all of the code is safe, then the fact that it was converted to scope shared gains you nothing. It's not really shared across threads, and it doesn't enable any useful operations.
Not being required to duplicate every threadsafe function is a huge advantage. We went on about this for weeks 6 months ago.
Jun 22
parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 6/22/2019 4:44 PM, Manu wrote:
 Not being required to duplicate every threadsafe function is a huge advantage. 
 We went on about this for weeks 6 months ago.
You've suggested that all operations on shared data be done with atomic function calls rather than operators. This implies the function bodies should be different.
Jun 23
parent Manu <turkeyman gmail.com> writes:
On Mon, Jun 24, 2019 at 12:50 PM Walter Bright via Digitalmars-d
<digitalmars-d puremagic.com> wrote:
 On 6/22/2019 4:44 PM, Manu wrote:
 Not being required to duplicate every threadsafe function is a huge advantage.
 We went on about this for weeks 6 months ago.
You've suggested that all operations on shared data be done with atomic function calls rather than operators. This implies the function bodies should be different.
I don't follow that logic? There may be atomics, but there are also cast-away-shared patterns that may be used. It's all very situational. Whatever a shared function does is its own business, I'm interested in the API from the callers perspective here. Whatever a shared method does, it simply must be threadsafe. Such a function is still threadsafe whether there are many threads with a reference to the shared object, or just one. A method that performs a threadsafe operation on some object can safely perform it thread-locally, and we should be able to allow that assuming the callee does NOT retain a shared reference to the object beyond the life of the call. We should be able to rely on `scope` to guarantee references are not retained beyond the life of the function. This would be extremely useful.
Jun 25
prev sibling parent reply Manu <turkeyman gmail.com> writes:
On Sun., 23 Jun. 2019, 6:32 am Jonathan M Davis via Digitalmars-d, <
digitalmars-d puremagic.com> wrote:

 On Saturday, June 22, 2019 1:57:22 AM MDT Walter Bright via Digitalmars-d
 wrote:
 On 6/21/2019 5:33 PM, Manu wrote:
 Because it's safe to do from the calling scope's perspective,
No, it is not.
 Compiler is not required to check the users work in  trusted code. It
 assumes the user conformed with language requirements.
The language does not require you to put the fence in, hence it cannot assume you did. I don't know how else to explain it to you. Your example does not work.
If I understand correctly, Manu and Timon are arguing that because you're dealing with scope with only thread-local variables, the compiler knows that the state of the thread-local variable is properly up-to-date, because in safe code, it's impossible for it to be otherwise. And then if your trusted code ensures that the data on the original thread is up-to-date after it does its thing with using the object on other threads, then it all works. And as such, they think that it should be fine for the compiler to implicitly convert to shared with scope, because it's up to the programmer to make sure that all of the normal guarantees are in place and none of the guarantees are violated without trusted being involved. This makes sense to me as long as the programmer is doing the casting, because then safe still isn't doing anything with shared. It's just the programmer doing what trusted needs to do and making sure that the normal guarantees are maintained. The problem is that if the compiler then implicitly converts to scope shared in safe code, then all of a sudden, safe needs to worry about whether the variable is properly up-to-date, since it's no longer just assuming it based on it being thread-local. At best, it's assuming that the scope shared variable will never actually be shared across threads, since without trusted, it wouldn't be possible to share it across threads. And if it's assuming that, then why on earth would it implicitly convert the variable to shared?
You say this as someone who has obviously never or rarely tried to use shared (as I suspect basically everyone here is among, possibly including Walter). It's quite annoying to implement many identical shared and unshared method overloads. Most shared methods are threadsafe, and equally valid to call on thread-local data too. The hypothetical on trial is about trusted code, but there are many useful cases where no trusted code need be present for the mechanic to be useful and save a lot of noise. void atomicInc(ref scope shared(int) x); int x; atomicInc(x); // <-- perfectly safe That's a boring example, but I shouldn't need to write a second copy of the function when it is threadsafe. This whole idea seems to rest on the premise that the compiler will do a
 conversion for you, because it can't be a problem in  safe code without
  trusted being involved, but it's also completely useless without  trusted
 being involved,
Wrong. I wish people that have never tried to use shared would just let those of us that have, and want to make the language feature useful have some authority on the matter. and the compiler doesn't make assumptions based on trusted.
 It makes them based on  safe.  trusted is just the way for the programmer
 to
 provide something that the compiler treats as  safe even though it's the
 programmer making sure that it is instead of the compiler being able to
 actually verify it.
And in this case, the compiler may treat the scope ref as if it remained thread local and freely ignore the possibility of synchronisation issues, because that's the reality made possible by safe. Any trusted code must maintain those assumptions. It's
 not going to treat an  trusted function any
 differently from an  safe one aside from the mangling of the function name,
 and arguably,  safe and  trusted should have had the same name mangling in
 the first place, since there is no difference from the standpoint of the
 caller. The difference is only in how the implementation is checked by the
 compiler.

 As long as  safe code doesn't actually benefit from the implicit conversion
 to scope shared,
It does, because DRY and mitigating redundancy. it
 makes no sense to me to have it happen. The entire point
 is for it to be used with  trusted stuff.
No, that's just this one example on trial. We had a huge weeks-long discussion 6 months ago about how the promotion is useful without trusted code. And if that's the case, then why
 not just have the cast be explicit and  trusted like it normally would be?
Because you've overlooked the 99% case.
Jun 22
next sibling parent reply Kagamin <spam here.lot> writes:
On Saturday, 22 June 2019 at 23:41:49 UTC, Manu wrote:
 Wrong. I wish people that have never tried to use shared would 
 just let those of us that have, and want to make the language 
 feature useful have some authority on the matter.
Eh? No. You don't understand const, shared, scope, safe and trusted. What authority?
Jun 22
parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 23.06.19 08:27, Kagamin wrote:
 On Saturday, 22 June 2019 at 23:41:49 UTC, Manu wrote:
 Wrong. I wish people that have never tried to use shared would just 
 let those of us that have, and want to make the language feature 
 useful have some authority on the matter.
Eh? No. You don't understand const, shared, scope, safe and trusted.
This kind of statement is useless without a demonstration or explanation. Also, I think Manu argued correctly in this thread.
Jun 23
parent Kagamin <spam here.lot> writes:
On Sunday, 23 June 2019 at 22:15:18 UTC, Timon Gehr wrote:
 Also, I think Manu argued correctly in this thread.
His wish is known: he has an idea about his own language and wants a compiler for it. We already had this very argument about const and shared before, now it's about scope and safe.
Jun 24
prev sibling parent Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Saturday, 22 June 2019 at 23:41:49 UTC, Manu wrote:
 And in this case, the compiler may treat the scope ref as if it 
 remained thread local and freely ignore the possibility of 
 synchronisation issues, because that's the reality made 
 possible by  safe. Any  trusted code must maintain those 
 assumptions.
Although, it isn't quite as simple, since there are many factors that play into this. Including the specifics of the compiler optimization level and the concrete hardware (x86 does a lot to maintain cache coherency in hardware). So without language support you risk the compiler repeating all the sync work you do in your trusted code. Or you risk it breaking if you use a compiler with whole program optimization. What works with separate compilation does not necessarily work with full flow analysis. But this really depends on the details of the language semantics and how far the compiler can go with optimization. Any competitor to C++ ought to do better than C++ when it comes to optimization. Concurrency is an area where there is ample opportunity, as concurrency features in C++ are bolted on as an after thought. (Very primitive.) For instance, is the compiler allowed to elide atomic access if it marked as nonshared or immutable? It should be allowed to do it (assume it is known that the memory range is nonvolatile). So, if trusted means you can do anything, then optimization opportunities evaporates. In my view the type system should be very strict. I don't think trusted (or even system) should be allowed to break the core guarantees of type system without clearly marking the section and how it breaks it using designated language features. So trusted marks that you access language features that are not allowed in safe that could be used in a way that breaks memory safety. What you should ask for then is something similar for thread safety. What you might want to ask for is a rendezvous like language feature that temporarily turns nonshared to shared within its scope. Then you can implement parallel for with rendezvous as a primitive that the optimizer has to account for. That would be much safer than the programmer bypassing the type system without any proof that what they do is correct. Proving correctness for concurrency is very hard, you usually have to account for all possible combinations, so to do it in a reasonably convincing manner you'll have to build a model that proves all combinations. It isn't something that can be done on the back of an envelope. There are languages/tools for this (some use term rewriting languages to do such proofs), but I think it is safe to assume that few D programmers know how to do it or are willing to do it even if they know how. In C++ the type system is somewhat non-strict for historical and cultural reasons, but as the result the compiler can make few assumptions based on types, like with const. (Although they have made union more strict than i C). That means that other languages in theory can provide better optimization and code gen than C++. I think languages that want to compete with C++ should focus real hard on the weak spots of the C++ type system and find new opportunities in that area. So what you want is a stronger type system (e.g. language features like rendezvous), not to allow the programmer to bypass and weaken the type system. Ola.
Jun 23