[dmd-concurrency] draft 6

Andrei Alexandrescu andrei at erdani.com
Thu Jan 28 08:18:48 PST 2010


Thanks for the comments! A few follow-ups below.

Michel Fortin wrote:
> I'm curious about something. You say this:
> 
>> When receive sees a message of an unexpected type, it doesn’t throw
>> an excep- tion (as receiveOnly does). The message passing subsystem
>> simply saves the non- matching messages in a queue, colloquially
>> known as the thread’s mailbox.
> 
>> [...]
>> 
>> Planting a Variant handler at the bottom of the message handling
>> food chain is a good method to dynamically check for protocol bugs.
>> 
> 
> So basically you should use receive with a variant at the end when
> you want to catch protocol bugs (and avoid crowding the queue). My
> question is this: when do you *not* want to catch protocol bugs?

Heh. Good point. I should rephrase that. The point is that sometimes you 
call receive() without a Variant handler because you want to leave 
non-matching messages in the mailbox, in knowledge that you'll handle 
them later. Here's the rephrase:

=========
Planting a\sbs @Variant@ handler at the bottom of the message handling
food chain  is a good method  to make sure that  stray messages aren't
left in your mailbox.
=========

>> Choosing a good buffer size and checking for errors completes a
>> useful (if unoriginal) program.
> 
> 
> Hum, choosing a good buffer size is a useful "program"?

=========
Adding appropriate  error handling completes a  useful (if unoriginal)
program.
=========

>> The exception is only thrown if receive has no more match- ing
>> messages and must wait for a new message; as long as receive has
>> something to fetch from the mailbox, it will not throw. In other
>> words, when the owner thread termi- nates, the owned threads’ calls
>> to receive (or receiveOnly for that matter) will throw
>> OwnerTerminated if and only if they would otherwise block waiting
>> for a new message
> 
> 
> Didn't we agree this should happen serially, in he same order the
> messages are added to the queue? Ah, but you're explaining it
> correctly two paragraphs below. :-)
> 
> 
>> The ownership relation is not necessarily unidirectional. In fact,
>> two threads may even own each other; in that case, whichever thread
>> finishes will notify the other.
> 
> 
> My idea with the thread termination protocol was that it would
> prevent circular references, following the owner chain would always
> lead you back to the main thread. It somewhat breaks the idea that
> when the main thread terminates all threads end up notified. I don't
> quite oppose this, but which use case do you have in mind for this?

I don't. I chose to do what Erlang does. It is quite clear on 
bidirectional linking and in fact makes it the default, so it must have 
a reason. Sean?

>> the OwnerTerminated exception
> 
> So it is going to inherit from Exception after all?

Yah, I think it should.

>> If a thread exits via an exception, the exception OwnerFailed
>> propagates to all of its owned threads by means of prioritySend.
> 
> 
> I though we were in agreement that it'd be better if this was handled
> serially by default to avoid races in the program's logic?

I think it's fine to propagate serially on _success_ but not on failure. 
Failure has priority.

> Also, no mention about what happens if the writer thread exits with
> an exception. You only explain what happens if it exits normally
> after catching the exception itself (sending a message to that thread
> throws).

I (only) explained what happens if the writer throws:

=========
In this  case, @fileWriter@ returns  peacefully when @main@  exits and
everyone's  happy.   But  what  happens  in  the  case  the  secondary
thread---the writer---throws  an exception?   The call to  the @write@
function may fail if there's a  problem writing data to @tgt at . In that
case, the call to @send@ from the primary thread will fail by throwing
an  exception, which  is exactly  what should  happen.
=========

I changed the paragraph to:

=========
In this  case, @fileWriter@ returns  peacefully when @main@  exits and
everyone's  happy.   But  what  happens  in  the  case  the  secondary
thread---the writer---throws  an exception?   The call to  the @write@
function may fail if there's a  problem writing data to @tgt at . In that
case, the call to @send@ from the primary thread will fail by throwing
an\sbs   @OwnedFailed@  exception,  which   is  exactly   what  should
happen. By the  way, if an owned thread exits  normally (as opposed to
throwing an exception), subsequent calls to @send@ to that thread also
fail, just with a different exception type:\sbs @OwnedTerminated at .
=========

By the way I added a joke that I don't want you guys to miss:

=========
As an  aside, if there exists a  ``Best Form-Follows-Function'' award,
then the  notation @qualifier(type)@ should snatch  it.  It's perfect.
You can't even syntactically create the wrong pointer type, because it
would look like this:

\begin{D-nocheck}
int shared(*) pInt;
\end{D-nocheck}

\noindent which  does not make  sense even at syntactic  level because
`@*@'  is not  a type  (granted, it  \emph{is} a  nice emoticon  for a
cyclops).
=========

> On to 'shared'...
> 
>> The annotation helps the compiler with much more than an indication
>> of where the variable should be allocated
> 
> 
> Strange. I though 'shared' has no relation to where the data is
> allocated. There is no way to implement thread-local memory pools in
> the D2, since you're allowed to cast non-shared to shared when you
> know that no one else has a reference to it, and also because of
> immutable. So what are you trying to say exactly?

For global data, shared indicates that data goes in the global memory 
segment, not in TLS. To avoid confusion, I just changed that to read: 
"The annotation  helps the compiler a  great deal: ..."

>> atomicOp!"+="(threadsCount, 1); // fine
> 
> 
> Oh, wow. Can't we have a better syntax? :-/

I know you'd rather define the Atomic type. I think it's better to have 
explicit operations, and atomicOp!"+=" is better than a million 
atomicBlahs. Let's collect some more opinions.

>> It’s like saying “I’ll share this wallet with everyone, just please
>> re- member that the money in it ain’t shared.” Claiming the pointer
>> is shared across threads but the pointed-to data is not takes us
>> back to the wonderful programming-by-honor- system paradigm that
>> has failed so successfully throughout history. It’s not the volun-
>> tary malicious uses, it’s the honest mistakes that form the bulk of
>> problems. Software is large, complex, and ever-changing, traits
>> that never go well with maintaining guarantees through convention.
> 
> I think you're going a little too far with this. Just saying "if many
> threads can read a pointer, all those threads can follow this pointer
> and access the data it points to, so that data cannot be shared"
> should be enough. It's pretty obvious.
> 
> And I'm not even sure how far the wallet analogy goes since
> const(shared(money*)) should be enough to not have anyone steal your
> money (you should keep the mutable pointer to yourself, of course).

I like that paragraph too much, so I'll exercise author's prerogative on 
that. Your combination of qualifiers does complete the joke very well, 
so I added this footnote:

========
Incidentally, you can share a wallet  with theft-protected money with 
the help of \cc{const}  by using the type \cc{shared(const(Money)*)}.
========

>> The shared constructor undergoes special typechecking, distinct
>> from that of reg- ular functions. The compiler makes sure that
>> during construction the address of the object or of a member of it
>> does not escape to the outside. Only at the end of the con-
>> structor, the object is “published” and ready to become visible to
>> multiple threads.
> 
> That sounds wrong. I mean, it's all fine to have a constructor that
> ensures that no reference escapes, but is 'shared' the right term for
> this? In all other cases, 'shared' means the 'this' pointer is
> shared, not that it can't escape.

I am not sure what the rules surrounding shared constructors should be. 
Inside the ctor, the object is not yet shared, so for example member 
arrays should be initializable, just not with aliased arrays.

> I think 'scope' would be a better term for 'no escape'.
> 
> Also, I'm a little surprised. I though the 'no escape' thing was
> deemed too difficult to implement a few months ago. Has something
> changed?

Nothing has changed. Inside immutable and shared constructors, we are 
limiting what the constructors can do so we enable analysis without 
requiring inter-procedural reach.


Andrei


More information about the dmd-concurrency mailing list