On Saturday, 25 January 2014 at 10:03:55 UTC, Dmitry Olshansky
wrote:
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?)
No Queue in your definition of Queue it may take local T no
problem at all. It all depends on the contents of pop/push to
understand if it compiles.
It can already be inferred from the interface. The Queue uses
some Container as storage. Obviously push()/pop() would actually
access that storage to store/remove elements. If Container does
not support "shared", we're in trouble.
Let's get a little more concrete:
http://dpaste.dzfl.pl/5bd15697
A dastardly simple implementation of Queue. Note that Queue is
being designed as shared type, what it uses for storage is its
business. However, due to transitivity of "shared", it cannot
really use anything *but* other "shared" containers. Therefore,
we either need casts or __gshared :(
Note that "T" for Queue is "Packet", bur for Container it's
"shared(Packet)".
Well, ok. Let's take a step back and forget about existing
containers. I could implement the storage myself, inside the
Queue type, right? Something like that:
class Queue(T) {
struct Node {
T value;
Node* prev, next;
}
private Node* head, tail;
// Implementation follows...
}
I'd implement the whole business with doubly-linked list, maybe
even employ some interesting algorithm to make the whole thing
lock-free... But again, transitivity of "shared" now dictates
that Queue can only store shared(T).
Now about this Queue type, it has a very interesting property:
it's a shared data structure that never allows more than one
thread access to any single stored item at a time. It allows to
push() and pop() concurrently (well, ok, this one is blocking).
But it does not have random access, in fact, in its public
interface it doesn't have anything *but* push() and pop()!
Therefore, even though the Queue is a shared type (and instances
of it will always be shared), it can *safely* store non-shared
data. This does not mean that it *shouldn't* store shared data,
it just means that the transitivity of "shared" can be lifted in
one particular case: iff !hasUnsharedAliasing!(T). That's the
same basis upon which std.concurrency is built. And that's why I
went through that whole business with __gshared in my original
post :)
Where'd that "ref Packet" come from? :)
Well seeing that Packet is such a fat type, I instinctively
went for 'ref' :)
Aww, 'tis not fat, just a little overweight :D
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).
If the queue ultimately copies the data to its store it doesn't
matter if there are aliases to the original thread's local
packet.
Eggz-actly. But this is only true for data types such as that
Packet. If it contained pointers or references... no go.
From the type system point of view there already can't be
aliases in the queue as it's shared an the argument/return type
is not :)
You don't know that beforehand. Unlike its guts, which dictate
that what it stores is in fact shared(T), it's interface does not
say anything about T. T is T. Queue could be instantiated as
Queue!(shared(FancyType)) too :)
Given the interface alone it doesn't break anything. The whole
"as soon as it gets value" is the only interesting part. There
are different hack/tricks that could be used to make the queue
go shared --copy-> local by hand.
What I can deduce is that shared containers in general that
return a copy may do a shallow unqual, i.e. mark one "level" of
a indirections as not shared since it is a copy.
Almost my thinking. In fact, there's no sense in *copying* shared
data anyway. Moving - yes. OTOH, copying *references* to shared
data is perfectly fine.
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.
Mmm as I've seen above there is no problem modeling it - that
is taking local/mutable argument and put it into shared
container.
Yes, but either by using casts, or __gshared :)
You don't need shared interface for Packets to store them in a
shared container. In this case only because of no indirections
property of Packets.
That would depend on a container. As you've demostrated earlier,
if a container did allow concurrent access to its items, the
Packets could provide a world of hurt. The Queue does not allow
such things, therefore it's indeed safe.
Yes, by locking wrapper I meant exactly this. I just have no
idea how you'd generally wrap unknown beforehand container
otherwise (well there are spinlocks and whatnot, but still
locks).
Of course it's not unknown beforehand. I've already mentioned
that such queues could be built on top of dlists, other queues,
ring buffers, you-name-it. But if those are not "shared", we
again come back to all these shennanigans with casts or __gshared.
Do I understand you correctly that *everything* down to the
lowest level
should be shared-enabled?..
No, for many wrapper is enough or even all the boundary of what
we could do. What's needed is more containers that are (by
their nature) shared (thread-safe). See Java's
ConcurrentHashMap.
Whew :)
Following that logic, how would you
build some shared conatiner on top of D arrays which are by
their nature
not synchronized?
New type with a shared interface, lock and hacks with casts
inside.
Just like the Queue from my original post :D
What I still don't completely understand is the general
message of "with "shared" everything should be "shared"". How
can it be?
For _data_ it must else you allow low-level races.
"Everything" is bit too general a word.
Hmm... I'll take that into my reply to your next message ;)