A question about move() and a rant about shared
Stanislav Blinov
stanislav.blinov at gmail.com
Fri Jan 24 14:13:54 PST 2014
On Friday, 24 January 2014 at 19:54:31 UTC, Dmitry Olshansky
wrote:
> 2 unrelated questions would be better as 2 nice smaller posts.
> Just saying.
I know. Sorry if it got you irritated. I have a feeling I'll be
coming back with questions that are based on both move() and
"shared" tied up together, hence I posted these together too.
> Consider that std.container is an incomplete piece of work...
Understood. Thanks.
> moveFront/moveBack/moveAt is a shameful and unknown thing about
> ranges usually kept in a basement during public talks. Ask
> Andrei of where these things go.
Now you've got me intrigued :)
> If I understand the current direction is to go with ref T and
> make some language
> level/compiler level changes to the escaping reference.
Escaping... where? By "go with ref T" you mean return value of
e.g. front()?
>> struct Packet {
>> ulong ID;
>> ubyte[32] header;
>> ubyte[64] data;
>> }
>>
>> Note that I do have arrays in there, but they cannot possibly
>> introduce any aliasing, since they're static.
>
> They can. To copy the thing out of somewhere you need to reach
> into memory area that is shared between threads and then all
> bets are off.
> //Example:
> shared Packet[4] buffer;
Aha, I see you responded as you went through my post :) I've got
this case covered further down. Of course they can this way. But
instantiating them as "shared" doesn't make sense anyway, exactly
because they are not "shared"-aware.
> High level invariants that deal with ownership are not
> represented in D typesystem and hence exist only in the head of
> people writing code/comments and better be encapsulated in
> something manageable.
But they actually are. immutable === doesn't care: share or own,
you only ever can read it. Value type === single owner. Because
you can only ever give away the value entirely (move() it) or a
copy of it (which is coincidentally both shallow and deep copy).
References (any kind thereof) === sharing. Granted, this system
is lacking (i.e. it'd be nice to have unique references). But
that's at least a bare base.
>> It wouldn't work, because queue expects "shared" type.
>
> Depends on how you've defined push/pop.
I'm losing you here. Without any __gshared "tricks" we have this:
shared class Queue(T) {
private Container!T q;
void push(T);
T pop();
}
Note that the class Queue is "shared"-qualified, meaning that "q"
is also "shared", meaning that (due to transitive nature of
"shared"), the container ultimately stores shared(T). In fact,
the definition above would even fail to compile in this case,
because push should take shared(T) and pop should return
shared(T) (since Queue will be dealing with a container of shared
Ts, right?)
> The thing is that both operation either accept local Packet or
> produce local Packet.
> void push(ref Packet p); //copy from local memory to the queue
> Packet pop(); //pop to the local memory
> But it is intrinsic to the way queue which is local (T1) -->
> shared --> local (T2) bridge of sorts T1, T2 being some threads.
Umm... I did post an interface of Queue, did I not? Where'd that
"ref Packet" come from? :) push() should take an rvalue, pop()
should return an rvalue. The fact that pushing thread managed to
create an rvalue to push means precisely that it no longer has
any aliases to pushed data (remember, we're talking value types
here). Same with popping thread: as soon as it gets the value,
Queue no longer stores it.
If Packet weren't value type (e.g. contained references), it'd
break the first invariant I imposed in my original post and would
fall into "must be "shared"" category.
>> One solution would be to use a cast...
>> This is ugly...
>> Convention is not a reasonable justification for overlooking
>> type system.
>
> To simplify you put shared qualifier on type and then suddenly
> it doesn't do magic.
:D Was I that bad at explaining? Shared qualifier doesn't do any
magic by itself anyway, it's our task as programmers to make it
do what it means. I.e. put synchronization where it belongs.
> Sad but you have to think at what happens where with sharing.
I do. Lately, probably too much :) That's why I posted this.
> Casts or no casts is an internal thing and sadly at the time
> pretty much required because compiler can't grasp ownership for
> you.
It can't, and therefore it's my task to solve. I mean, in all of
my example the only rock-solid shared thing is the queue. The
queue would be the object all threads access simultaneously, the
queue would be declared somewhere as "shared(Queue) theQueue;",
the queue would be pushed and popped and fed and milked dry of
its contents. But the contents, each individual packet, would
*never* be accessed by more than one thread at a time, this is by
design in this particular case. Generalizing that design to
"always support shared" just doesn't make sense: why would I need
to implement shared interface (and thus, shared access and all
that comes with it) for those Packets if they're never used
concurrently?
>> Another solution would be to instantiate those structs as
>> "shared" in
>> the first place.
>
> And it doesn't work nicely because they are in fact not, only
> inside of the queue they are shared.
Eggz-actly. And even this "sharing" is purely incidental, due to
the very nature of both such queues and this particular type
Packet. Consider an already existing example of all that jazz
with producer-consumer queue: std.concurrency. Only in that case
that would be multi producer single consumer. It is perfectly
safe to send() value types all day. You don't have to design your
structs (or your ints :) ) as shared in order to pass them from
one thread to another in a message. That is as long as your types
don't have any references or pointers, which would *have* to be
"shared".
> There you are horribly wrong. All containers need to be aware
> of the fact that they are shared between threads else some ugly
> things are going to happen. (Simultaneous insert and removal
> from an shared array?)
Umm... I don't follow the logic. As you suggest further below,
all I'm doing is designing a container type instances of which
can be safely shared between threads. I can't build that
container using only other "shared"-enabled types, and why should
I? The underlying "storage" container is forever encapsulated in
my shared one, the shared one deals with synchronization.
> In essence all you need is a locking wrapper container that
> gives you shared interface around a mutable container or a
> container that is shared-aware by its nature (lock-free or
> whatever it may be inside).
Not exactly. In essence what I need is a "shared" wrapper that
deals with all synchronization issues (not necessarily locking).
Why does whatever it uses for *storage* have to know anything
about sharing? Classical implementations of such queues on top of
dlists, ring buffers, other queues...
I know that there are shared structures that can be implemented
more efficiently entirely from scratch (Andrei proposes a couple
as examples in TDPL, the latter being, in his own words, somewhat
barbaric), i.e. that maintain their own storage. But that's not
always required nor needed. If I'd reduce my Queue to single
producer single consumer (a pipe) it can be happily built on top
of a plain array plus a couple of pointers.
Do I understand you correctly that *everything* down to the
lowest level should be shared-enabled, i.e. thread-safe, i.e.
synchronized? Should Phobos just have e.g. two versions each of
Array, DList, RBTree, you-name-it, one shared, one - not?
Following that logic, how would you build some shared conatiner
on top of D arrays which are by their nature not synchronized?
> The need is well recognized and you are not alone in this.
That's good to hear. That is, if I didn't lose your meaning
completely :)
> It is. The problem with shared and immutable is that we humans
> by our very nature expect:
>
> immutable(SomeContainer!T)
>
> to mean the same as
> ImmutableSomeContainer!T
>
> Where ImmutableSomeContainer is almost the same thing but in
> fact is a different type that has immutable interface and
> handles immutable data well.
I'd say that's more of a way of thinking imposed by some other
language(s) rather than our human nature. Then again, the
boundaries do blur sometimes :)
> Example - ImmutableVector vs Vector.
> ImmutableVector need not to known about things such as
> capacity, has different kind of range and is a different type
> in general not just Vector with some auto-magic restrictions
> bolted on top of it.
>
> The same could be said about shared.
ImmutableVector is a strange concept altogether, but I understand
what you mean here. What I still don't completely understand is
the general message of "with "shared" everything should be
"shared"". How can it be?
More information about the Digitalmars-d
mailing list