View mode: basic / threaded / horizontal-split · Log in · Help
June 07, 2012
valid uses of shared
I am having a quite interesting debate on pure and shared with Artur  
Skawina in another thread, and I thought about how horrible a state shared  
is in.  It's not implemented as designed, and the design really leaves  
more questions than it has answers.  In addition, it has not real  
connection with thread synchronization whatsoever, and Michel Fortin  
suggested some improvements to synchronized that look really cool that  
would involve shared.

So I thought about, what are the truly valid uses of shared?  And then I  
thought, more importantly, what are the *invalid* uses of shared?

Because I think one of the biggest confusing pieces of shared is, we have  
no idea when I should use it, or how to use it.  So far, the only benefit  
I've seen from it is when you mark something as not shared, the things you  
can assume about it.

I think a couple usages of shared make very little sense:

1. having a shared piece of data on the stack.
2. shared value types.

1 makes little sense because a stack is a wholly-owned subsidiary of a  
thread.  Its existence depends completely on the stack frame staying  
around.  If I share a piece of my stack with another thread, then I return  
from that function, I have just sent a dangling pointer over to the other  
thread.

2 makes little sense because when you pass around a value type, it's  
inherently not shared, you are making a copy!  What is the point of  
passing a shared int to another thread?  Might as well pass an int (this  
is one of the sticking points I have with pure functions accepting or  
dealing with shared data).

I have an idea that might fix *both* of these problems.

What if we disallowed declarations of shared type constructors on any  
value type?  So shared(int) x is an error, but shared(int)* x is not (and  
actually shared(int *) x is also an error, because the pointer is passed  
by value).  However, the type shared(int) is valid, it just can't be used  
to declare anything.

The only types that could be shared, would be:

ref shared T => local reference to shared data
shared(T) * => local pointer to shared data
shared(C) => local reference to shared class

And that's it.

The following would be illegal:

struct X
{
  shared int x; // illegal
  shared(int)* y; // legal

  shared(X) *next; // legal
}

shared class C  // legal, C is always a reference type
{
   shared int x; // illegal, but useless, since C is already shared
}

If you notice, I never allow shared values to be stored on the stack, they  
are always going to be stored on the heap.  We can use this to our  
advantage -- using special allocators that are specific to shared data, we  
can ensure the synchronization tools necessary to protect this data gets  
allocated on the heap along side it.  I'm not sure exactly how this could  
work, but I was thinking, instead of allocating a monitor based on the  
*type* (i.e. a class), you allocate it based on whether it's *shared* or  
not.  Since I can never create a shared struct X on the stack, it must be  
in the heap, so...

struct X
{
   int y;
}

shared(X) *x = new shared(X);

synchronized(x) // scope-locks hidden allocated monitor object
{
   x.y = 5;
}

x.y = 5; // should we disallow this, or maybe even auto-lock x?

Hm... another idea -- you can't extract any piece of an aggregate.  That  
is, it would be illegal to do:

shared(int)* myYptr = &x.y;

because that would work around the synchronization.

auto would have to strip shared:

auto myY = x.y; // typeof(myY) == int.

This is definitely not a complete proposal.  But I wonder if this is the  
right direction?

-Steve
June 08, 2012
Re: valid uses of shared
On 06/08/12 01:51, Steven Schveighoffer wrote:
> I am having a quite interesting debate on pure and shared with Artur Skawina in another thread, and I thought about how horrible a state shared is in.  It's not implemented as designed, and the design really leaves more questions than it has answers.  In addition, it has not real connection with thread synchronization whatsoever, and Michel Fortin suggested some improvements to synchronized that look really cool that would involve shared.
> 
> So I thought about, what are the truly valid uses of shared?  And then I thought, more importantly, what are the *invalid* uses of shared?
> 
> Because I think one of the biggest confusing pieces of shared is, we have no idea when I should use it, or how to use it.  So far, the only benefit I've seen from it is when you mark something as not shared, the things you can assume about it.
> 
> I think a couple usages of shared make very little sense:
> 
> 1. having a shared piece of data on the stack.
> 2. shared value types.
> 
> 1 makes little sense because a stack is a wholly-owned subsidiary of a thread.  Its existence depends completely on the stack frame staying around.  If I share a piece of my stack with another thread, then I return from that function, I have just sent a dangling pointer over to the other thread.
> 
> 2 makes little sense because when you pass around a value type, it's inherently not shared, you are making a copy!  What is the point of passing a shared int to another thread?  Might as well pass an int (this is one of the sticking points I have with pure functions accepting or dealing with shared data).
> 
> I have an idea that might fix *both* of these problems.
> 
> What if we disallowed declarations of shared type constructors on any value type?  So shared(int) x is an error, but shared(int)* x is not (and actually shared(int *) x is also an error, because the pointer is passed by value).  However, the type shared(int) is valid, it just can't be used to declare anything.
> 
> The only types that could be shared, would be:
> 
> ref shared T => local reference to shared data
> shared(T) * => local pointer to shared data
> shared(C) => local reference to shared class
> 
> And that's it.
> 
> The following would be illegal:
> 
> struct X
> {
>   shared int x; // illegal
>   shared(int)* y; // legal
> 
>   shared(X) *next; // legal
> }

Note that the type of 'x' in

  shared struct S {
     int x;
  }

should probably be 'shared(int)'.
Which lets you safely take an address of an aggregates field.

And I'm not sure if marking a struct and class as shared would work
correctly right now, it's probably too easy to lose the 'shared' qualifier.


> shared class C  // legal, C is always a reference type
> {
>    shared int x; // illegal, but useless, since C is already shared
> }

Redundant, but should be accepted, just like 'static' is.


> If you notice, I never allow shared values to be stored on the stack, they are always going to be stored on the heap.  We can use this to our advantage -- using special allocators that are specific to shared data, we can ensure the synchronization tools necessary to protect this data gets allocated on the heap along side it.  I'm not sure exactly how this could work, but I was thinking, instead of allocating a monitor based on the *type* (i.e. a class), you allocate it based on whether it's *shared* or not.  Since I can never create a shared struct X on the stack, it must be in the heap, so...
> 
> struct X
> {
>    int y;
> }
> 
> shared(X) *x = new shared(X);
> 
> synchronized(x) // scope-locks hidden allocated monitor object
> {
>    x.y = 5;
> }
> 
> x.y = 5; // should we disallow this, or maybe even auto-lock x?
> 
> Hm... another idea -- you can't extract any piece of an aggregate.  That is, it would be illegal to do:
> 
> shared(int)* myYptr = &x.y;
> 
> because that would work around the synchronization.

That's too restrictive. It would overload 'shared' even more. If you
want that kind of synchronize magic to work, just allow:

  shared synchronized(optional_locking_primitive) struct S {
     ...
  }

And *now* 'x.y = 5' can do its magic, while '&x.y' can be disallowed.


  shared struct S {
     Atomic!int i;
  }
  shared(S)* p = ...
  p.i += 1;

should work, so accessing fields must remain possible.


> auto would have to strip shared:
> 
> auto myY = x.y; // typeof(myY) == int.

Hmm, i'm not sure about this, maybe it should be disallowed; it could
only strip 'shared' from the head anyway.

> This is definitely not a complete proposal.  But I wonder if this is the right direction?

I think it is.

artur
June 08, 2012
Re: valid uses of shared
On Thursday, 7 June 2012 at 23:51:27 UTC, Steven Schveighoffer 
wrote:
> I am having a quite interesting debate on pure and shared with 
> Artur Skawina in another thread, and I thought about how 
> horrible a state shared is in.  It's not implemented as 
> designed, and the design really leaves more questions than it 
> has answers.  In addition, it has not real connection with 
> thread synchronization whatsoever, and Michel Fortin suggested 
> some improvements to synchronized that look really cool that 
> would involve shared.
>
> So I thought about, what are the truly valid uses of shared?  
> And then I thought, more importantly, what are the *invalid* 
> uses of shared?
>
> Because I think one of the biggest confusing pieces of shared 
> is, we have no idea when I should use it, or how to use it.  So 
> far, the only benefit I've seen from it is when you mark 
> something as not shared, the things you can assume about it.
>
> I think a couple usages of shared make very little sense:
>
> 1. having a shared piece of data on the stack.
> 2. shared value types.
>
> 1 makes little sense because a stack is a wholly-owned 
> subsidiary of a thread.  Its existence depends completely on 
> the stack frame staying around.  If I share a piece of my stack 
> with another thread, then I return from that function, I have 
> just sent a dangling pointer over to the other thread.
>
> 2 makes little sense because when you pass around a value type, 
> it's inherently not shared, you are making a copy!  What is the 
> point of passing a shared int to another thread?  Might as well 
> pass an int (this is one of the sticking points I have with 
> pure functions accepting or dealing with shared data).
>
> I have an idea that might fix *both* of these problems.
>
> What if we disallowed declarations of shared type constructors 
> on any value type?  So shared(int) x is an error, but 
> shared(int)* x is not (and actually shared(int *) x is also an 
> error, because the pointer is passed by value).  However, the 
> type shared(int) is valid, it just can't be used to declare 
> anything.
>
> The only types that could be shared, would be:
>
> ref shared T => local reference to shared data
> shared(T) * => local pointer to shared data
> shared(C) => local reference to shared class
>
> And that's it.
>
> The following would be illegal:
>
> struct X
> {
>   shared int x; // illegal
>   shared(int)* y; // legal
>
>   shared(X) *next; // legal
> }
>
> shared class C  // legal, C is always a reference type
> {
>    shared int x; // illegal, but useless, since C is already 
> shared
> }
>
> If you notice, I never allow shared values to be stored on the 
> stack, they are always going to be stored on the heap.  We can 
> use this to our advantage -- using special allocators that are 
> specific to shared data, we can ensure the synchronization 
> tools necessary to protect this data gets allocated on the heap 
> along side it.  I'm not sure exactly how this could work, but I 
> was thinking, instead of allocating a monitor based on the 
> *type* (i.e. a class), you allocate it based on whether it's 
> *shared* or not.  Since I can never create a shared struct X on 
> the stack, it must be in the heap, so...
>
> struct X
> {
>    int y;
> }
>
> shared(X) *x = new shared(X);
>
> synchronized(x) // scope-locks hidden allocated monitor object
> {
>    x.y = 5;
> }
>
> x.y = 5; // should we disallow this, or maybe even auto-lock x?
>
> Hm... another idea -- you can't extract any piece of an 
> aggregate.  That is, it would be illegal to do:
>
> shared(int)* myYptr = &x.y;
>
> because that would work around the synchronization.
>
> auto would have to strip shared:
>
> auto myY = x.y; // typeof(myY) == int.
>
> This is definitely not a complete proposal.  But I wonder if 
> this is the right direction?
>
> -Steve


You're forgetting about Global data.
I think rather the head shared should be striped as this fits 
better with how D treats meaningless specifiers. And trying to 
put structs that contain shared data on the stack should be 
illegal.

shared int global_shared_int; // type is shared(int);

struct struct_with_shared_data
{
  shared int shared_data_in_a_struct; // type is shared(int)
}

int main()
{
  shared int not_really_shared; // type is int
  shared int* unshared_ptr_to_shared_int; // type is shared(int)*;

  struct_with_shared_data foo; // illegal, shared data can't 
stored on the stack
                     // nor can the sharedness be striped.
}
June 08, 2012
Re: valid uses of shared
On Thu, 07 Jun 2012 22:16:21 -0400, Robert DaSilva <spunit262@yahoo.com>  
wrote:


> You're forgetting about Global data.

I wasn't so much forgetting it as I was ignoring it :)

My thought on that is that the shared keyword in that case is truly a  
storage class.  It's the one place where having a value-type based shared  
value makes sense.  If we had some kind of synchronized/shared pairing,  
the compiler would have to allocate mutex space for that too.

> I think rather the head shared should be striped as this fits better  
> with how D treats meaningless specifiers.

I don't think we can do that with type constructors, but I'm not sure.   
I'm certainly against it, as I am against the current abuses of that  
methodology.

> And trying to put structs that contain shared data on the stack should  
> be illegal.

First, I can't imagine that you'd actually need a block on the heap that  
was partially thread-local and partially shared.  So allowing  
partially-shared types does not make sense to me.  For the cases where it  
may make sense, a value-type + shared pointer might work, or a heap block  
of TLS data which has a member pointing to another heap block of shared  
data could make sense.

Can you think of good use cases that show it is important to have hybrid  
blocks?

Second, I kind of like the idea that the monitor for the data in the block  
protects the entire block.  This would not make sense if part of the block  
is not shared.

-Steve
June 08, 2012
Re: valid uses of shared
On Thu, 07 Jun 2012 20:58:13 -0400, Artur Skawina <art.08.09@gmail.com>  
wrote:

> On 06/08/12 01:51, Steven Schveighoffer wrote:
>>
>> The following would be illegal:
>>
>> struct X
>> {
>>   shared int x; // illegal
>>   shared(int)* y; // legal
>>
>>   shared(X) *next; // legal
>> }
>
> Note that the type of 'x' in
>
>    shared struct S {
>       int x;
>    }
>
> should probably be 'shared(int)'.
> Which lets you safely take an address of an aggregates field.

That's one of the things I'm wondering about.  Should it be allowed?

I agree that the type should be shared(int), but the type should not  
transfer to function calls or auto, it should be sticky to the particular  
variable.  Only references should be sticky typed.

> And I'm not sure if marking a struct and class as shared would work
> correctly right now, it's probably too easy to lose the 'shared'  
> qualifier.

Right, I was thinking shared structs do not make sense, since I don't  
think shared members do not make sense.  Either a whole struct/class is  
shared or it is not.  Because you can only put classes on the heap, shared  
makes sense as an attribute for a class.

But then again, it might make sense to say "this struct is only ever  
shared, so it should be required to go on the heap".  I like your idea  
later about identifying shared struct types that should use  
synchronization.

>> shared class C  // legal, C is always a reference type
>> {
>>    shared int x; // illegal, but useless, since C is already shared
>> }
>
> Redundant, but should be accepted, just like 'static' is.

That's probably fine.

>> If you notice, I never allow shared values to be stored on the stack,  
>> they are always going to be stored on the heap.  We can use this to our  
>> advantage -- using special allocators that are specific to shared data,  
>> we can ensure the synchronization tools necessary to protect this data  
>> gets allocated on the heap along side it.  I'm not sure exactly how  
>> this could work, but I was thinking, instead of allocating a monitor  
>> based on the *type* (i.e. a class), you allocate it based on whether  
>> it's *shared* or not.  Since I can never create a shared struct X on  
>> the stack, it must be in the heap, so...
>>
>> struct X
>> {
>>    int y;
>> }
>>
>> shared(X) *x = new shared(X);
>>
>> synchronized(x) // scope-locks hidden allocated monitor object
>> {
>>    x.y = 5;
>> }
>>
>> x.y = 5; // should we disallow this, or maybe even auto-lock x?
>>
>> Hm... another idea -- you can't extract any piece of an aggregate.   
>> That is, it would be illegal to do:
>>
>> shared(int)* myYptr = &x.y;
>>
>> because that would work around the synchronization.
>
> That's too restrictive. It would overload 'shared' even more. If you
> want that kind of synchronize magic to work, just allow:
>
>    shared synchronized(optional_locking_primitive) struct S {
>       ...
>    }
>
> And *now* 'x.y = 5' can do its magic, while '&x.y' can be disallowed.
>
>
>    shared struct S {
>       Atomic!int i;
>    }
>    shared(S)* p = ...
>    p.i += 1;
>
> should work, so accessing fields must remain possible.

OK.  You are right, synchronized may be overkill for basic types.

>> auto would have to strip shared:
>>
>> auto myY = x.y; // typeof(myY) == int.
>
> Hmm, i'm not sure about this, maybe it should be disallowed; it could
> only strip 'shared' from the head anyway.

Yes, head stripping.  I think it should be allowed.  For instance, if you  
wanted to read a shared double, and take the cosine of it, this should be  
allowed:

auto n = cos(sharedValue);

If it's not, the alternatives are:

auto n = cos(cast()sharedValue);

or

double v = sharedValue; // explicit removal of shared
auto m = cos(v);

Neither of these look necessary, I think just allowing shared value types  
to automatically convert to non-shared versions works the best.

>> This is definitely not a complete proposal.  But I wonder if this is  
>> the right direction?
>
> I think it is.

good.

-Steve
June 08, 2012
Re: valid uses of shared
On 08.06.2012 8:03, Steven Schveighoffer wrote:
> On Thu, 07 Jun 2012 20:58:13 -0400, Artur Skawina <art.08.09@gmail.com>
> wrote:
>
>> On 06/08/12 01:51, Steven Schveighoffer wrote:
>>>
>>> The following would be illegal:
>>>
>>> struct X
>>> {
>>> shared int x; // illegal
>>> shared(int)* y; // legal
>>>
>>> shared(X) *next; // legal
>>> }
>>
>> Note that the type of 'x' in
>>
>> shared struct S {
>> int x;
>> }
>>
>> should probably be 'shared(int)'.
>> Which lets you safely take an address of an aggregates field.
>
> That's one of the things I'm wondering about. Should it be allowed?
>
> I agree that the type should be shared(int), but the type should not
> transfer to function calls or auto, it should be sticky to the
> particular variable. Only references should be sticky typed.
>
>> And I'm not sure if marking a struct and class as shared would work
>> correctly right now, it's probably too easy to lose the 'shared'
>> qualifier.
>
> Right, I was thinking shared structs do not make sense, since I don't
> think shared members do not make sense. Either a whole struct/class is
> shared or it is not. Because you can only put classes on the heap,
> shared makes sense as an attribute for a class.
>
> But then again, it might make sense to say "this struct is only ever
> shared, so it should be required to go on the heap". I like your idea
> later about identifying shared struct types that should use
> synchronization.
>
>>> shared class C // legal, C is always a reference type
>>> {
>>> shared int x; // illegal, but useless, since C is already shared
>>> }
>>
>> Redundant, but should be accepted, just like 'static' is.
>
> That's probably fine.
>
>>> If you notice, I never allow shared values to be stored on the stack,
>>> they are always going to be stored on the heap. We can use this to
>>> our advantage -- using special allocators that are specific to shared
>>> data, we can ensure the synchronization tools necessary to protect
>>> this data gets allocated on the heap along side it. I'm not sure
>>> exactly how this could work, but I was thinking, instead of
>>> allocating a monitor based on the *type* (i.e. a class), you allocate
>>> it based on whether it's *shared* or not. Since I can never create a
>>> shared struct X on the stack, it must be in the heap, so...
>>>
>>> struct X
>>> {
>>> int y;
>>> }
>>>
>>> shared(X) *x = new shared(X);
>>>
>>> synchronized(x) // scope-locks hidden allocated monitor object
>>> {
>>> x.y = 5;
>>> }
>>>
>>> x.y = 5; // should we disallow this, or maybe even auto-lock x?
>>>
>>> Hm... another idea -- you can't extract any piece of an aggregate.
>>> That is, it would be illegal to do:
>>>
>>> shared(int)* myYptr = &x.y;
>>>
>>> because that would work around the synchronization.
>>
>> That's too restrictive. It would overload 'shared' even more. If you
>> want that kind of synchronize magic to work, just allow:
>>
>> shared synchronized(optional_locking_primitive) struct S {
>> ...
>> }
>>
>> And *now* 'x.y = 5' can do its magic, while '&x.y' can be disallowed.
>>
>>
>> shared struct S {
>> Atomic!int i;
>> }
>> shared(S)* p = ...
>> p.i += 1;
>>
>> should work, so accessing fields must remain possible.
>
> OK. You are right, synchronized may be overkill for basic types.
>
>>> auto would have to strip shared:
>>>
>>> auto myY = x.y; // typeof(myY) == int.
>>
>> Hmm, i'm not sure about this, maybe it should be disallowed; it could
>> only strip 'shared' from the head anyway.
>
> Yes, head stripping. I think it should be allowed. For instance, if you
> wanted to read a shared double, and take the cosine of it, this should
> be allowed:
>
> auto n = cos(sharedValue);

what if it's a
auto n = cos(sharedValues[k]); //type is shared(double) right?

and somebody in the other thread is halfway through writing it?

>
> If it's not, the alternatives are:
>
> auto n = cos(cast()sharedValue);
>
> or
>
> double v = sharedValue; // explicit removal of shared
> auto m = cos(v);
>
> Neither of these look necessary, I think just allowing shared value
> types to automatically convert to non-shared versions works the best.
>
>>> This is definitely not a complete proposal. But I wonder if this is
>>> the right direction?
>>
>> I think it is.
>
> good.

Indeed.

-- 
Dmitry Olshansky
June 08, 2012
Re: valid uses of shared
On Fri, 08 Jun 2012 03:13:04 -0400, Dmitry Olshansky  
<dmitry.olsh@gmail.com> wrote:

> On 08.06.2012 8:03, Steven Schveighoffer wrote:

>> Yes, head stripping. I think it should be allowed. For instance, if you
>> wanted to read a shared double, and take the cosine of it, this should
>> be allowed:
>>
>> auto n = cos(sharedValue);
>
> what if it's a
> auto n = cos(sharedValues[k]); //type is shared(double) right?
>
> and somebody in the other thread is halfway through writing it?

At this point, I am not sure shared means synchronized or atomic.  It just  
means multiple threads can see it.  Perhaps you have already taken the  
lock that protects this variable.

My point is, the transfer of a shared type into an rvalue should strip the  
head-shared part of it *automatically*.  Especially since it would be  
illegal to store such a type on the stack (and quite literally, a shared  
rvalue makes no sense whatsoever), we don't want to force casting  
everywhere.

-Steve
June 08, 2012
Re: valid uses of shared
On 06/08/12 06:03, Steven Schveighoffer wrote:
> On Thu, 07 Jun 2012 20:58:13 -0400, Artur Skawina <art.08.09@gmail.com> wrote:
> 
>> On 06/08/12 01:51, Steven Schveighoffer wrote:
>>>
>>> The following would be illegal:
>>>
>>> struct X
>>> {
>>>   shared int x; // illegal
>>>   shared(int)* y; // legal
>>>
>>>   shared(X) *next; // legal
>>> }
>>
>> Note that the type of 'x' in
>>
>>    shared struct S {
>>       int x;
>>    }
>>
>> should probably be 'shared(int)'.
>> Which lets you safely take an address of an aggregates field.
>
> That's one of the things I'm wondering about.  Should it be allowed?

Must be. We're talking about the next generation of a high level assembler,
not logo. :)


> 
> I agree that the type should be shared(int), but the type should not transfer to function calls or auto, it should be sticky to the particular variable.  Only references should be sticky typed.

The problem with this is that it should be symmetrical, IOW the conversion
from non-shared to shared would also have to be (implicitly) allowed.
A type that converts to both would be better, even if harder to implement.

>> And I'm not sure if marking a struct and class as shared would work
>> correctly right now, it's probably too easy to lose the 'shared' qualifier.
> 
> Right, I was thinking shared structs do not make sense, since I don't think shared members do not make sense.  Either a whole struct/class is shared or it is not.  Because you can only put classes on the heap, shared makes sense as an attribute for a class.
> 
> But then again, it might make sense to say "this struct is only ever shared, so it should be required to go on the heap".  I like your idea later about identifying shared struct types that should use synchronization.

Of course shared structs make sense, it's what allows implementing any
non-trivial shared type. 

  static Atomic!int counter;

inside a function is perfectly fine. And, as somebody already mentioned
in this thread, omitting 'static' should cause a build failure; right
now it is accepted, even when written as

  shared Atomic!int counter;

The problem? 'shared' is silently dropped. Move the counter from a struct
into a function after realizing it's only accessed from one place, forget
to add 'static' - and the result will compile w/o even a warning. 

> 
>>> If you notice, I never allow shared values to be stored on the stack, they are always going to be stored on the heap.  We can use this to our advantage -- using special allocators that are specific to shared data, we can ensure the synchronization tools necessary to protect this data gets allocated on the heap along side it.  I'm not sure exactly how this could work, but I was thinking, instead of allocating a monitor based on the *type* (i.e. a class), you allocate it based on whether it's *shared* or not.  Since I can never create a shared struct X on the stack, it must be in the heap, so...
>>>
>>> struct X
>>> {
>>>    int y;
>>> }
>>>
>>> shared(X) *x = new shared(X);
>>>
>>> synchronized(x) // scope-locks hidden allocated monitor object
>>> {
>>>    x.y = 5;
>>> }
>>>
>>> x.y = 5; // should we disallow this, or maybe even auto-lock x?
>>>
>>> Hm... another idea -- you can't extract any piece of an aggregate.  That is, it would be illegal to do:
>>>
>>> shared(int)* myYptr = &x.y;
>>>
>>> because that would work around the synchronization.
>>
>> That's too restrictive. It would overload 'shared' even more. If you
>> want that kind of synchronize magic to work, just allow:
>>
>>    shared synchronized(optional_locking_primitive) struct S {
>>       ...
>>    }
>>
>> And *now* 'x.y = 5' can do its magic, while '&x.y' can be disallowed.
>>
>>
>>    shared struct S {
>>       Atomic!int i;
>>    }
>>    shared(S)* p = ...
>>    p.i += 1;
>>
>> should work, so accessing fields must remain possible.
> 
> OK.  You are right, synchronized may be overkill for basic types.
> 
>>> auto would have to strip shared:
>>>
>>> auto myY = x.y; // typeof(myY) == int.
>>
>> Hmm, i'm not sure about this, maybe it should be disallowed; it could
>> only strip 'shared' from the head anyway.
> 
> Yes, head stripping.  I think it should be allowed.  For instance, if you wanted to read a shared double, and take the cosine of it, this should be allowed:
> 
> auto n = cos(sharedValue);
> 
> If it's not, the alternatives are:
> 
> auto n = cos(cast()sharedValue);
> 
> or
> 
> double v = sharedValue; // explicit removal of shared
> auto m = cos(v);
> 
> Neither of these look necessary, I think just allowing shared value types to automatically convert to non-shared versions works the best.

If 'shared(VT)' implicitly converts to VT, then

  auto myY = x.y; // typeof(myY) == shared(int)

would still be fine. So would

 auto n = cos(x,y); // assuming some weird cos() that works on ints ;)

But I'm not sure allowing these implicit conversions is a good idea.
At least not yet. :)

>>> This is definitely not a complete proposal.  But I wonder if this is the right direction?
>>
>> I think it is.
> 
> good.

It's a small step in the right direction.

artur
June 08, 2012
Re: valid uses of shared
On Fri, 08 Jun 2012 10:57:15 -0400, Artur Skawina <art.08.09@gmail.com>  
wrote:

> On 06/08/12 06:03, Steven Schveighoffer wrote:

>>
>> I agree that the type should be shared(int), but the type should not  
>> transfer to function calls or auto, it should be sticky to the  
>> particular variable.  Only references should be sticky typed.
>
> The problem with this is that it should be symmetrical, IOW the  
> conversion
> from non-shared to shared would also have to be (implicitly) allowed.
> A type that converts to both would be better, even if harder to  
> implement.

It should be allowed (and is today).  I am talking about stripping  
head-shared, so shared(int *) automatically converts to shared(int)* when  
used as an rvalue.

>> Right, I was thinking shared structs do not make sense, since I don't  
>> think shared members do not make sense.  Either a whole struct/class is  
>> shared or it is not.  Because you can only put classes on the heap,  
>> shared makes sense as an attribute for a class.
>>
>> But then again, it might make sense to say "this struct is only ever  
>> shared, so it should be required to go on the heap".  I like your idea  
>> later about identifying shared struct types that should use  
>> synchronization.
>
> Of course shared structs make sense, it's what allows implementing any
> non-trivial shared type.
>
>    static Atomic!int counter;
>
> inside a function is perfectly fine. And, as somebody already mentioned
> in this thread, omitting 'static' should cause a build failure; right
> now it is accepted, even when written as
>
>    shared Atomic!int counter;
>
> The problem? 'shared' is silently dropped. Move the counter from a struct
> into a function after realizing it's only accessed from one place, forget
> to add 'static' - and the result will compile w/o even a warning.

The difference is that static is not a type constructor.

e.g.:

shared int x; // typeof(x) == int

void foo(shared int *n){...}

foo(&x); // compiler error?  huh?

I think this is a no-go.  Shared has to be statically disallowed for local  
variables.


>> Yes, head stripping.  I think it should be allowed.  For instance, if  
>> you wanted to read a shared double, and take the cosine of it, this  
>> should be allowed:
>>
>> auto n = cos(sharedValue);
>>
>> If it's not, the alternatives are:
>>
>> auto n = cos(cast()sharedValue);
>>
>> or
>>
>> double v = sharedValue; // explicit removal of shared
>> auto m = cos(v);
>>
>> Neither of these look necessary, I think just allowing shared value  
>> types to automatically convert to non-shared versions works the best.
>
> If 'shared(VT)' implicitly converts to VT, then
>
>    auto myY = x.y; // typeof(myY) == shared(int)
>
> would still be fine.

No, because then &myY yields a reference to shared data on the stack,  
which is what I think should be disallowed.

My recommendation is that typeof(myY) == int.

> But I'm not sure allowing these implicit conversions is a good idea.
> At least not yet. :)

Implicit conversions to and from shared already are valid.  i.e. int x =  
sharedInt; is valid code.  I'm talking about changing the types of  
expressions, such that the expression type is always the tail-shared  
version.  In fact, simply using a shared piece of data as an rvalue will  
convert it into a tail-shared version.

-Steve
June 08, 2012
Re: valid uses of shared
On 06/08/12 19:13, Steven Schveighoffer wrote:
> On Fri, 08 Jun 2012 10:57:15 -0400, Artur Skawina <art.08.09@gmail.com> wrote:
> 
>> On 06/08/12 06:03, Steven Schveighoffer wrote:
> 
>>>
>>> I agree that the type should be shared(int), but the type should not transfer to function calls or auto, it should be sticky to the particular variable.  Only references should be sticky typed.
>>
>> The problem with this is that it should be symmetrical, IOW the conversion
>> from non-shared to shared would also have to be (implicitly) allowed.
>> A type that converts to both would be better, even if harder to implement.
> 
> It should be allowed (and is today).

Hmm. I think it shouldn't be. This is how it is today:

  shared Atomic!int ai;
  shared Atomic!(void*) ap;

  void f(Atomic!int i) {} // Atomic() struct template temporarily made unshared for this test.
  void fp(Atomic!(void*) i) {}

  void main() {
     f(ai);
     fp(ap);
  }

  Error: function f (Atomic!(int) i) is not callable using argument types (shared(Atomic!(int)))
  Error: cannot implicitly convert expression (ai) of type shared(Atomic!(int)) to Atomic!(int)
  Error: function fp (Atomic!(void*) i) is not callable using argument types (shared(Atomic!(void*)))
  Error: cannot implicitly convert expression (ap) of type shared(Atomic!(void*)) to Atomic!(void*)

It seems to work for built-in value types, which i didn't even realize, because the thought of
using them with 'shared' never crossed my mind. I don't really see why those should be treated
differently from user defined types, which should not allow implicit shared<>unshared conversions.


> I am talking about stripping head-shared, so shared(int *) automatically converts to shared(int)* when used as an rvalue.

Where would the 'shared(int*) type come from? IOW, given 'shared struct S { int i; } S s;'
what would the type of '&s.i' be? In your model; because right now it is 'shared(int)*'.


>>> Right, I was thinking shared structs do not make sense, since I don't think shared members do not make sense.  Either a whole struct/class is shared or it is not.  Because you can only put classes on the heap, shared makes sense as an attribute for a class.
>>>
>>> But then again, it might make sense to say "this struct is only ever shared, so it should be required to go on the heap".  I like your idea later about identifying shared struct types that should use synchronization.
>>
>> Of course shared structs make sense, it's what allows implementing any
>> non-trivial shared type.
>>
>>    static Atomic!int counter;
>>
>> inside a function is perfectly fine. And, as somebody already mentioned
>> in this thread, omitting 'static' should cause a build failure; right
>> now it is accepted, even when written as
>>
>>    shared Atomic!int counter;
>>
>> The problem? 'shared' is silently dropped. Move the counter from a struct
>> into a function after realizing it's only accessed from one place, forget
>> to add 'static' - and the result will compile w/o even a warning.
> 
> The difference is that static is not a type constructor.

The problem is that 'shared' is lost, resulting in an incorrect program.
When you explicitly declare something as shared the compiler better treat
it as such, or fail to compile it; silently changing the meaning is never
acceptable.


> e.g.:
> 
> shared int x; // typeof(x) == int

This could be made illegal, but if it is accepted then it should retain its type.


> void foo(shared int *n){...}
> 
> foo(&x); // compiler error?  huh?
> 
> I think this is a no-go.  Shared has to be statically disallowed for local variables.

It's a possibility. Except _static_ local variables, those must work.


>>> Yes, head stripping.  I think it should be allowed.  For instance, if you wanted to read a shared double, and take the cosine of it, this should be allowed:
>>>
>>> auto n = cos(sharedValue);
>>>
>>> If it's not, the alternatives are:
>>>
>>> auto n = cos(cast()sharedValue);
>>>
>>> or
>>>
>>> double v = sharedValue; // explicit removal of shared
>>> auto m = cos(v);
>>>
>>> Neither of these look necessary, I think just allowing shared value types to automatically convert to non-shared versions works the best.
>>
>> If 'shared(VT)' implicitly converts to VT, then
>>
>>    auto myY = x.y; // typeof(myY) == shared(int)
>>
>> would still be fine.
> 
> No, because then &myY yields a reference to shared data on the stack, which is what I think should be disallowed.

The only problem with shared data on the stack i can think of is portability.
But this is something that can be decided at a much later time, it wouldn't
be used much in practice anyway.

> My recommendation is that typeof(myY) == int.
> 
>> But I'm not sure allowing these implicit conversions is a good idea.
>> At least not yet. :)
> 
> Implicit conversions to and from shared already are valid.  i.e. int x = sharedInt; is valid code. 

yes, but see above. shared(BVT)->BVT and shared(P*)->shared(P)* are allowed,
and i don't think the latter is necessarily sound. Yes, the current shared
model practically requires this, but i don't think raw access to shared data
is the best approach.

Note that inside synchronized() statements such conversions would be fine.
And this also reminds me - mixed shared/unshared fields inside an aggregate
should be legal. Why? Consider a mutex-like field that protects other data.
Of course, synchronized() must imply a compiler barrier for this to work,
but it needs it anyway. Memory barriers should always be explicit, btw; but
for some architectures that might not be very practical.

> I'm talking about changing the types of expressions, such that the expression type is always the tail-shared version.  In fact, simply using a shared piece of data as an rvalue will convert it into a tail-shared version.

Could you provide an example? Because I'm not sure what problem this is supposed
to solve. Eg. what is "a shared piece of data" and where does it come from?

artur
« First   ‹ Prev
1 2 3 4 5
Top | Discussion index | About this forum | D home