Jump to page: 1 2
Thread overview
[dmd-concurrency] draft 6
Jan 28, 2010
Kevin Bealer
Jan 28, 2010
Michel Fortin
Jan 28, 2010
Michel Fortin
Jan 28, 2010
Michel Fortin
Jan 29, 2010
Sean Kelly
Jan 30, 2010
Robert Jacques
Jan 31, 2010
Walter Bright
Jan 31, 2010
Walter Bright
January 27, 2010
I'm done with basic messaging and entering the shared qualifier. I've reshuffled a fair amount of text, so you can expect new/improved material starting around section 4.5, "Exchanging Messages Between Threads".

I'm looking for a good example of a class for explaining shared and synchronized. Right now I use Date, but I'm hoping for something more challenging that contains an indirection. Any ideas, please let me know. Thanks!

The file is here:

http://erdani.com/d/fragment.preview.pdf


Andrei
January 28, 2010
On Thu, Jan 28, 2010 at 12:46 AM, Andrei Alexandrescu <andrei at erdani.com>wrote:

> I'm done with basic messaging and entering the shared qualifier. I've reshuffled a fair amount of text, so you can expect new/improved material starting around section 4.5, "Exchanging Messages Between Threads".
>
> I'm looking for a good example of a class for explaining shared and synchronized. Right now I use Date, but I'm hoping for something more challenging that contains an indirection. Any ideas, please let me know. Thanks!
>
> The file is here:
>
> http://erdani.com/d/fragment.preview.pdf
>
>
> Andrei
>

Maybe an "environment" object that contains handles to system resources like database handles, streams for writing to a printer, open files (like log files) or sockets to other processes.  The idea that a handle to a database or printer is something you want to share should make sense to people but leave you room to build something instructive.

Kevin
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.puremagic.com/pipermail/dmd-concurrency/attachments/20100128/fb4d37de/attachment.htm>
January 28, 2010
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?


> 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"?


> 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?


> the OwnerTerminated exception

So it is going to inherit from Exception after all?


> 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?

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).


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?


> atomicOp!"+="(threadsCount, 1); // fine


Oh, wow. Can't we have a better syntax? :-/


> 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).


> 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 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?


-- 
Michel Fortin
michel.fortin at michelf.com
http://michelf.com/



January 28, 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
January 28, 2010
Le 2010-01-28 ? 11:18, Andrei Alexandrescu a ?crit :

> 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.
> =========

Better.

But then I'll ask another question (I suspect the answer might expose a problem): when do you want to handle messages immediately?



>>> 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?

Keep in mind that Erlang doesn't have a thread termination protocol. I was mostly worried about the effects on the termination protocol.


>>> the OwnerTerminated exception
>> So it is going to inherit from Exception after all?
> 
> Yah, I think it should.

Ok, fine.


>>> 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.

I argued previously that this is what you want sometime, but not always. If you were "copying" your file by sending its content to a browser and a failure happens while reading the end of the file, you'd want to continue sending the file up to the error point. On the other side if you're copying to another file, you might want to delete the partial copy in case of failure. In the later case, sending the event faster is an optimization, in the first case, it is not even desirable.

So my point is that sending the event through the fast track is only an optimization and that it'll sometime get in the way. Also, it introduces a risk of race in  communication protocols not expecting it.


>> 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 .
> =========

That only partially address my point. In the thread termination protocol I wrote, if a thread terminates with an uncaught exception, that exception is sent back to the owner thread (and you'd get it on a call to receive). Have you decided not to do any of this?


>> 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: ..."

Ah, I see. The original text was just fine then. I just forgot about that.


>>> 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.

Personally, I'd rather type atomicAdd. Try typing !"+="( a couple of time for fun... Also, atomicAdd is more readable. How do you pronounce atomicOp!"+="?


>>> 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.

I'm doubtful of how far this can go without being able to apply 'scope' to other function's arguments. But if you're trying to avoid the reference from escaping, I'd definitely go with the word 'scope', or 'lent'. Not 'shared'.

-- 
Michel Fortin
michel.fortin at michelf.com
http://michelf.com/



January 28, 2010
Michel Fortin wrote:
> Le 2010-01-28 ? 11:18, Andrei Alexandrescu a ?crit :
> 
>> 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. =========
> 
> Better.
> 
> But then I'll ask another question (I suspect the answer might expose
>  a problem): when do you want to handle messages immediately?

In many situations, e.g. when you want to dispatch them to other threads. But really the fact of the matter is, any given paragraph of the book cannot explain every single implication and every single ramification immediately. If I mention an explanation, then I'd most likely need to qualify it and further exemplify it etc.

>>>> 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.
> 
> I argued previously that this is what you want sometime, but not
> always. If you were "copying" your file by sending its content to a
> browser and a failure happens while reading the end of the file,
> you'd want to continue sending the file up to the error point. On the
>  other side if you're copying to another file, you might want to
> delete the partial copy in case of failure. In the later case,
> sending the event faster is an optimization, in the first case, it is
>  not even desirable.

It's all a matter of defaults. You can choose to catch the exception and send it without priority.

>> ========= 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 .
>> =========
> 
> That only partially address my point. In the thread termination protocol I wrote, if a thread terminates with an uncaught exception, that exception is sent back to the owner thread (and you'd get it on a call to receive). Have you decided not to do any of this?

I don't know. Erlang offers a different primitive spawn_link for explicitly making the parent receive exceptions from the child. The default is the parent doesn't care.


Andrei
January 28, 2010
Le 2010-01-28 ? 13:48, Andrei Alexandrescu a ?crit :

> Michel Fortin wrote:
>> Le 2010-01-28 ? 11:18, Andrei Alexandrescu a ?crit :
>>>>> 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.
>> I argued previously that this is what you want sometime, but not always. If you were "copying" your file by sending its content to a browser and a failure happens while reading the end of the file, you'd want to continue sending the file up to the error point. On the
>> other side if you're copying to another file, you might want to delete the partial copy in case of failure. In the later case, sending the event faster is an optimization, in the first case, it is
>> not even desirable.
> 
> It's all a matter of defaults. You can choose to catch the exception and send it without priority.

It's just that in my view, defaults should be the safer choice. It's much easier to add an optimization (an asynchronous message) where you know need it than explicitly remove it everywhere where it could be a problem.


>> That only partially address my point. In the thread termination protocol I wrote, if a thread terminates with an uncaught exception, that exception is sent back to the owner thread (and you'd get it on a call to receive). Have you decided not to do any of this?
> 
> I don't know. Erlang offers a different primitive spawn_link for explicitly making the parent receive exceptions from the child. The default is the parent doesn't care.

It's again a problem of choosing the right defaults. :-)

My only fear is that if you only rarely send messages to a thread, and that thread crashes, it could take a long time before the program takes notice (probably the next time someone attempts to send a message). I don't think errors should be ignored by default.

I remember working with a heavily multithreaded C++ program where we sometime had threads crashing from unhanded exceptions. In those cases the program was often continuing as usual, only with unintended behaviours or with some things becoming non-functionnal, or crashing a little while later. In any case, it is not fun to debug.

That's why I believe such exceptions should be propagated back to the owner thread. The effect would be mostly like what exceptions do with the stack: progressively "unwind" the thread ownership hierarchy until one thread handles the exception

-- 
Michel Fortin
michel.fortin at michelf.com
http://michelf.com/



January 29, 2010
On Jan 28, 2010, at 8:18 AM, Andrei Alexandrescu wrote:

> Thanks for the comments! A few follow-ups below.
> 
> Michel Fortin wrote:
>> 
>>> 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?

Originally, I didn't understand the reason for this either.  But thinking about the termination process generated a bunch of examples where bidirectional linking is desired, and few where unidirectional linking is desired.  For example, consider the app that spawns two processes to perform a file copy.  If the writer fails, the reader should terminate as well, and vice-versa.
January 29, 2010
Page 63:"Software is large, complex, and ever-changing, traits that never
go well with maintaining guarantees through convention."
There needs to be either a period or semi-colon between "ever-changing"
and "traits".
January 29, 2010
Thanks!

Andrei

Robert Jacques wrote:
> 
> Page 63:"Software is large, complex, and ever-changing, traits that
> never go well with maintaining guarantees through convention."
> There needs to be either a period or semi-colon between "ever-changing"
> and "traits".
> _______________________________________________
> dmd-concurrency mailing list
> dmd-concurrency at puremagic.com
> http://lists.puremagic.com/mailman/listinfo/dmd-concurrency

« First   ‹ Prev
1 2