asynchronous communication between threads

Charles Hixson charleshixsn at earthlink.net
Thu Jan 3 22:36:24 PST 2013


On 01/03/2013 11:34 AM, Dmitry Olshansky wrote:
> 03-Jan-2013 22:38, Charles Hixson пишет:
>> On 01/03/2013 08:40 AM, Dmitry Olshansky wrote:
>>> 02-Jan-2013 03:54, Charles Hixson пишет:
>>>> If I were to use the below as an asynchronous communication channel,
>>>> would it avoid deadlocks (presuming that only Cell called Msg) and that
>>>> when a thread activated Cell, the first thing it did was process it's
>>>> mailbox?
>>>> Also, if only around 7 cells were created in the main thread, would RAM
>>>> usage remain reasonable (i.e., when a thread was through with an MCell,
>>>> would only one copy continue to exist in RAM? Or would there be a copy
>>>> for each thread that had ever referenced it? Should MCell instances be
>>>> marked shared?
>>>
>>> That's a lot of questions to ask and currently I hardly can decipher the
>>> whole logic from this and the code below alone, too much is behind the
>>> scenes.
>>>
>>> I'm willing to help out if you could post a more complete example or
>>> more accurate questions as e.g. "created 7 cells in main thread" could
>>> be done very differently.
>>>
>>> Also a nit at this size of code (and for me it's unformatted) I'd
>>> recommend linking to the code on some paste service e.g. dpaste
>>> http://dpaste.dzfl.pl/.
>>>
>>> [snip]
>>>
>> I'm trying to design a program (I'm not writing it yet) that requires
>> asynchronous communication between LOTS of separate "cells". You can
>> think of it as vaguely like a neural network, but it doesn't fit the
>> definition, that's just an analogy. I thought that D would be a good
>> language to do this in, but all the built-in approaches seem to be
>> "overly protective", locking a lot more than I want and yielding
>> deadlocks, or copying more than I want, and ensuring that I end up with
>> stale data. The approach that I've come up with to avoiding this is to
>> attach a mailbox to each "cell". In an ideal world, each cell would be
>> a separate thread, but as there will be at least hundreds of thousands
>> of them, and likely millions, that's unreasonable. So I need the
>> threads to cycle through pools of "cells". Each cell may send messages
>> to any other cell (not really, but you don't know in advance what the
>> connections will be).
>
> So cell is in fact a task (more common name for it I think) with a
> mailbox and you want threads multiplexed across these tasks. The task is
> running some code/function/callback/whatever that periodically polls a
> mailbox & puts stuff in other task's mailboxes. So far good?
>
> Then definitely take a look at Fiber in druntime (it's core.Fiber AFAIK).

It's in core.Thread.  A fiber would work well for the cell, but it's 
been pointed out to me that with this design, an ordinary class instance 
would also work, so for that it's probably overkill.  Which leaves the 
mailbox unhandled.  "Periodic" polling, as in time based, is too 
expensive.  Periodic polling as in "once in each cycle" is what is 
planned, and that part doesn't require the overhead of threads, etc., 
except to keep martial messages from being read.  But the documentation 
of fiber *strongly* implies that the contents of the mailbox would be 
copied from it's normal location to the active thread, if it's not the 
same thread within which the cell executes.  So it looks to me as if the 
mailbox needs to be synchronized.  But I'm not really sure what 
synchronized means in D.  (N.B.:  The cell is logically a task, but if 
it were implemented as such, then the mailbox would be unnecessary.  But 
the overhead would be unbearable.)
>
>>
>> That skeleton of code was intended to show the idea of cells isolated
>> from outside attached to a mailbox, which has blocking access from the
>> outside. The cells should never block, but this is largely because they
>> are only directly accessed by the thread within which they run. The
>> mailboxes can receive messages from anyone, but their processes are so
>> short, that blocking will be extremely brief.
>
> I'm sure not so optimistic about locking. Even though it's brief there
> are many threads that may be putting stuff simultaneously into mailboxes
> thus contending on the lock and causing context switches.
> + the lock/unlock of a mutex is not free. The lock-based message queue
> is nice for a start (less _subtle_ bugs) but you sure got to look into
> lock-free alternatives later on.
The number of cells is huge when compared with the number of available 
threads, On the rough order of 1,000,000 to 6.  Because of this I don't 
expect contention to be a big problem.  OTOH, the number of 
cores/processor is going *up*, so a lock-free system would be very 
desireable.  But the only lock-free design I'm familiar with is CSMACD 
(ethernet).  And to implement that, I think the mailboxes would need to 
be shared.  And this requires some protocol such as CSMACD for each 
access.  It's not clear to me that this would have less overhead than 
would synchronized (which locks, if I understand correctly, the class 
instance when someone else has write access to it).  Certainly TDPL 
talks about them sequentially, and implies that there is a tight 
connection between shared variables and synchronized classes.  It is as 
if a synchronized class is the analog of an atomic operation acting on a 
larger set of data.  And when I look at implementing CSMACD it looks as 
if only one thread can detect that it's write was interrupted, and then 
only after it's complete.  Unless I implement a checksum scheme, in 
which case I guess both could detect an improperly completed write. But 
then independent readers couldn't detect the problem.  So readers would 
need to be locked out while writing was in process...and we're back to 
mutexes/spinlocks/etc.
So it looks to me as if a synchronized mailbox class eliminates a 
multitude of problems a the lowest reasonable cost.  If it works as I 
hope it does.  But if the mailbox ends up getting copied from thread 
address space to thread address space, then the overhead would be much 
higher than a naive estimate, and I can't guess how high.  It *could* 
scale so poorly that 90% of the computation consisted of mailboxes being 
copied from thread to thread.
>
>> I wanted to know if this proposed design would work, as in not getting
>> into deadlocks, not blocking excessively, and not giving me excessively
>> stale data.
>
> The the crucial part is missing - taking a message out of the mailbox ;)
The only entity permitted to take a message from the mailbox is the cell 
to which it is attached, and that happens at each initiation of 
activation.  But that's one reason that the mailbox should be synchronized.
>
> But anyway let's focus on the details. 2 classes and 2 functions. Cell's
> send & MBox's receive.
>
> Let's suppose we have 2 Cells A & B and their mboxes MA & MB.
>  From the code I see (that's full of typos? MCell & Cell are used
> interchangeably) the chain of event for full send from A --> B is:
>
> 1. A Cell's send locks cell A. (send is sync-ed)
Why does it lock the cell?  But perhaps this is because the proffered 
cell class was synchronized.
> 2. It locks target cell B.
Why does it lock cell B?  Cell A makes no access to cell B.  Only to the 
mailbox.  Which is why in that rough design cell and mailbox were 
separate (synchronized) classes at the same level.  It it locks Cell B, 
then the design won't work.  (OTOH, the Cell function doesn't need to be 
synchronized anyway, that's a remnant from a prior design.)
> 3. It then locks its mailbox MB.
Check.
> 4. undoes all the locks backwards.
Check.
>
> Then there is of course a deadlock bound to happen if B is sending
> message in opposite direction, e.g. :
> 1. A locks A (making its first step)
> 2. B lock B (ditto)
> 3. A locks B & blocks
> 3. B locks A & blocks
If the cells get locked, then the design will not work.  No argument.  I 
was attempting to avoid that by making the mailbox a separate class, and 
having each cell only contact the other cell's mailbox.  If that doesn't 
work, then the design is not going to work, and I'll need to create a 
new one.
>
> that if for instance there is step 2. So I guess you haven't meant the
> step 2.
>
> If there is no lock of the cell except before sending then it looks
> legit as there are 2 locks protecting separate entities:
>
> - one is to protect message queue on putting message into it
>
> - the other one is to protect ... what exactly? send is already
> implicitly guarded by target queue's lock.
Yeah.  Sorry, that was sloppy thinking on my part.  The cell doesn't 
need to be synchronized.  (But I don't understand why that would be 
destructive of anything except efficiency.)
>
> So I'd say you only need to guard the message queue and that's about it.
> The only other concern is properly scheduling the execution of tasks (or
> your cells) so that one runs on no more then one thread at any given time.
The idea here is that each cell has an id#, and there is a pool of 
threads.  Each cell is accessible by the thread whose sequence number is 
the same as id# mod # of threads.  Each thread loops through the cells 
accessible to it.  When a cell is activated by the thread, first it 
checks it's mailbox to update it's state.  Then it processes, possibly 
sending messages to the mailboxes of other cells in a task independent 
way.  (It won't know about threads, only about mailboxes.)  Execution 
then passes on the the next cell in the thread.
>
> In simplest case just make a locked (or lock-free) queue of these and
> let threads pick cells/tasks from it and put back when they are done.
A lock-free queue would do it, I think, but the only ones I know of are 
attached to the thread rather than to a data instance, and that WOULD 
lead to massive contention.  And increasing contention as the number of 
cores (threads) increased.
>
> Far better is a large bounded queue that you never ever remove/put stuff
> into. It's just a big array of pointers/references to task. A thread
> then just goes around it looking for valid/ready entries (they stay
> allocated so no nulls there) and executes them. That goes firmly into
> lock-free zone to make it correctly synchronized though but with a bit
> of care it should be doable.
It would be quite reasonable to make the queue bounded, and reject 
messages when the mailbox was full.  But, again, if it's attached to the 
thread rather than to the object there is going to be a massive problem 
with contention.
>
> The second one also can be done with locks. In this case a thread goes
> through all of tasks/cells and tries to lock them (That's where your
> lock around it comes in, is it?). If it locks - cool, work on it, if not
> - try the next one.
Good point.  If the cell is initiated and can't read it's mailbox (this 
action needs a write lock, as it removes messages), then going on to the 
next cell rather than blocking is better.  (Again, if the mailbox is 
attached to the thread, this won't work.  All mailboxes will be nearly 
constantly in contention.)
>
>
>  > More details aren't available, because I didn't want to
>> commit to this design if the basic design was wrong, so I haven't
>> written them. It has been suggested that since the cell will only be
>> accessed by the thread, it doesn't need to be synchronous.
>>
>
>> I'm really nervous about how many copies of the cell will exist,
>> however. Since there are going to be so many of them, if I ended up
>> with a cell/thread, the system would bog down in unused storage. But
>> the mailbox needs to be globally accessible for the scheme to work.
>
> Everything on the heap is accessible from other threads, provided they
> have the pointer to the object in question.
Good.  And since I'm working with classes, everything is really on the 
heap, which means that only pointers will get copied.  (Well, 
references.  I avoid using pointers in my code unless I really can't 
avoid them.)
>
>
>> N.B.: When a cell receives a message from another cell it's likely, but
>> not guaranteed, to send a response back. It may also send responses
>> onwards. And the number of cells isn't fixed, nor is the number of
>> their connections. (This is less important in D than in many other
>> languages, but even in D it affects serialization.)
>>
>> FWIW, I've been told that an approximately similar design has worked in
>> Ada, though the design was written in Ada-83. (In Ada the mailbox was
>> protected, which I take to be approximately the same as synchronized.)
>
> In general there are ways to make it fly. The tricks to use depend on
> the use case and what is bottleneck (I/O or the CPU time).
> The pain points is faster mailboxes and better scheduling (as in less
> context switches for nothing, faster turn-around time etc.).
>
The bottleneck will be CPU time (given sufficient RAM).  No way to avoid 
that.  Stuffing things into a mailbox is going to be basically copying a 
struct.  (That hasn't been designed yet, but it's going to include a ref 
to the sender, a requested action,  a numeric value,  and I'm not sure 
of what else.  The "requested action" will probably be represented as an 
enum.  I'm probably going to avoid strings, as they don't appear to be 
valuable, even though that's just a reference copy.  So say 128 bytes of 
parameter, or possibly 256.  And receiving a message is copying the 
parameters into a queue.  Perhaps I could remove the synchronization 
from the class, and just guard calculating the index position into the 
queue, as once the position in the queue was known, there wouldn't be 
any contention WRT where the message would be stored.  That should be a 
very fast design, but I worry about how much RAM space would be 
required.  With a pre-allocated queue, each mailbox would consume the 
maximal amount of space even if none were used.  So if there were space 
for 20 messages, then the mailbox would consume 5120 bytes + overhead 
for each cell.  Which means that if I have 500,000 cells (an extremely 
lowball figure) just the mailboxes would consume 1,024,000,000 bytes 
plus overhead.  True. most of that would never be accessed, but enough 
accesses would be randomly distributed throughout the system that I 
would expect thrashing...or even failure due to inability to allocate 
memory. This would be compressed significantly if I used a thread 
attached mailbox, at the cost of nearly guaranteed massive contention 
problems.  And 20 messages is an unreasonably low upper limit, even 
though it's too high for a mean value.  (I expect the mean value to be 
closer to 3-4.)  So I'd been planning on using variable length arrays 
for the mailbox, which would be deallocated every time the cell accessed 
them.  So messages could be attached by simply doing an append.  This 
would place the message queue on the heap, with an initially quite low 
value for maximal # of messages.  I'll admit that this may only look 
better because I can't estimate the amount of RAM consumed.

Perhaps I need to use some sort of disk cache of a data file, and only 
have the most active cells in memory at any particular time.  This would 
still result in a lot of thrashing, but in a more controlled manner. 
I've only around 8 GB of actual memory and it looks like 7.8 GB total 
memory, if one includes virtual memory.  Perhaps this needs to be 
upgraded...which would probably mean I upgraded the number of cores 
available, meaning increased contention.  But I could clearly upgrade 
the amount of virtual memory with just a repartitioning of the disk. 
It's not been needed so far, but then I've just been designing this 
system, not implementing it.  OTOH, virtual RAM is thrashing, so it's 
not clear how much that would help over, say, a BTree that rolled out 
relatively inactive cells, even though each cell would need to be RAM 
resident at least once per cycle,

That said, the application I'm designing would probably overstress any 
computer I could afford.  I'm almost resigned to that.  I just want to 
come as close to reasonable speed of execution as possible, and I 
clearly want to avoid deadlocks and data that doesn't get properly updated.

OTOH, rolling things out to a B+Tree means that I need to devise a way 
to access the mailbox based around the target's id# rather than around a 
reference to the item.  A hash table is the obvious solution, but the 
class managing the hash table would need to roll the cell+mailbox in if 
it isn't RAM resident.  Not something that's reasonable to do while the 
mailbox access is locked.  So the mailbox would need to queue the 
request for access to the cell's mailbox with a thread, stuff the 
message in a "to be acted upon" queue, and return.  And where that queue 
should be stored isn't clear to me.  Perhaps that's where the thread 
attached non-blocking queue should come into play.  Also, hash tables 
have enough overhead themselves that they would limit the number of RAM 
resident cells considerably over prior estimates, even while increasing 
the total number that could be dealt with.  Probably they would halve 
the number of RAM resident cells (a rough estimate, admittedly), while 
expanding the total number of cells that could be handled to be limited 
by available disk space.  They would also impose a severe performance 
penalty.  (In prior small scale tests, hash table access has been very 
roughly about 1/10 as fast as variable length array access.  Of course, 
this is still a lot faster than disk file access.)

Still, it's sounding like the basic design will work, unless a cell 
calling the mailbox of another cell locks that cell, and I don't see any 
reason why it should...but I've been repeatedly surprised by the 
requirements imposed on concurrently executing threads of execution.


More information about the Digitalmars-d-learn mailing list