On Tue Oct 23 15:29:40 2012, JDHEDDEN wrote:
Show quoted text> On 2012-10-22 15:44:26, MSCHWERN wrote:
> > Your solution only works if there's one thread. D'oh!
> > The first thread dequeues the magic value and the rest
> > hang.
> >
> > I couldn't figure out how to do it. You, the author, made
> > a mistake doing it. The original SYNOPSIS didn't think of
> > it. I think that's a pretty solid argument for providing
> > a built in, explicit, safe, no-edge case way to solve this
> > problem.
>
> No, I didn't make a mistake. My example was simplistic with
> only one thread. If you have more, then you send more
> 'undef's - one (or more as appropriate) per thread. For
> example:
>
> $_->enqueue(undef) foreach (threads->list(threads::running));
>
> Since each thread will only dequeue one 'undef' to stop
> work, this is the solution. I use a similar approach in
> examples/pool_reuse.pl in the 'threads' distribution.
...
Show quoted text> I like this concept of constructing a specific termination
> item. Quite clever. For more threads, you'd just use:
>
> $_->enqueue($stop_flag) foreach (threads->list(threads::running));
I hope you'll agree, it's not a very safe solution. You have to know A)
how many threads are running and B) what their behavior will be.
A has a race condition where if a thread starts in the middle of that
loop, you're hosed. You have to patch it up with something like...
$_->enqueue($stop_flag) for threads->list(threads::running)
while(threads->list(threads::running));
But I'm not even sure that's always reliable.
B relies on the queuer having intimate knowledge of how each worker
behaves. Each worker must always pull a fixed and known number of items
off the queue before exiting. Consider the following...
sub work {
my $item1 = $q->dequeue;
...do some work on $item1...
my $item2 = $q->dequeue;
...do some work on $item1 and $item2...
}
Is the thread blocked on the first one or the second one? How many do
you put on the queue? Sure, you might say "why would you do that when
you can my @items = $q->dequeue(2); but somebody will do it.
Here's maybe a more practical example. Two different sets of workers,
one dequeues 1 at a time, one dequeues 3 at a time. threads::running
only tells you how many workers you have, not which type they are.
Either you need to add bookkeeping or you need to feed threads::running
* 3 elements onto the queue. And hope the race condition doesn't start
again.
What if there's two sets of workers, each talking to a different queue?
Now you need lists of what workers are on what queue. What if there's
a third set not talking to any queue at all? What if there's a fourth
talking to two queues, one is exhausted and one which is not? It might
eat your stop flags before other threads get to see them, causing some
threads to never unblock.
If the queue doesn't have total control of which threads are working on
their queue I don't think there's ANY reliable way to guarantee your
workers will unblock. You wind up shovelling stop flags onto the queue
as fast as possible and hope you can outrun the potential race conditions.
More likely a user doesn't think about any of this. "Stop on a magic
flag off the queue" requires too much careful coordination between the
workers and the queue manager. It's too easy to get into a state where
workers exhaust the stop flag (or forget to look for it) and wind up
blocking. Doing it right involves a lot of careful consideration and a
lot of code, which means a lot of people are going to get it subtly wrong.
Show quoted text> As to your patch, I'm concerned about a few issues I noted.
>
> > I had done() issue a cond_broadcast(). I've never gotten
> > into the cond_blah stuff, so please triple check that
> > work.
>
> This is inappropriate. cond_broadcast() wakes up all
> threads which can then all attempt to dequeue items at the
> same time and hence cause data integrity problems (e.g.,
> more than one thread getting the same queue item).
>
> The fix
> is to use cond_signal() and to also put a test on
> should_block on the cond_signal call in dequeue():
>
> cond_signal(%$self) if ((@$queue > $count) || ! $self->should_block);
I'm a bit nervous about this. It seems to rely on done() issuing
cond_signal, getting picked up by one thread which then wakes up and
issues a cond_signal to the next thread which then wakes up and issues a
cond_signal to the next thread... a chain which could be broken. A
single "wake everybody up" call seems more reliable.
Shouldn't the lock on %$self in dequeue() protect against multiple
threads trying to pull data from it at once? If not, would a second
lock inside _dequeue_nb() cover that?
I wrote up some test code to try and make a collision but couldn't get
one with either my patch or 3.01.
https://gist.github.com/3942210
Show quoted text> In 'dequeue' you have:
>
> cond_wait(%$self) while ($self->should_block && @$queue < $count);
>
> If the queue is set to 'done()' before the $count
> requirement is met, then a blocked thread will not get the
> correct number of items it is expecting. This means the
> programmer needs to check for this, and take corrective
> action. This is conceptually similar to the objections you
> brought up in the first place. (I.e., if the programmer is
> not *careful*, the program will go awry.) This is not a bad
> thing per se, but it does illustrate that no matter what is
> conceived of, the programmer will still be responsible for
> programming things correctly.
You're right, and "bad data" is a more prosaic sort of failure already
present, anybody can put bad data onto the queue, and MUCH easier to
debug than concurrency bugs. One way to mitigate it would be to change
dequeue() so it throws an exception if not enough items are pulled off
the queue. Then the developer can't miss the mistake. Also it's
impossible to tell "bad data in the queue" from "the queue is exhausted"
otherwise. Checking $q->pending after $q->dequeue invites a race
condition. I like that idea.
dequeue() on an exhausted queue is always going to return a single
scalar undef or fixed size list possibly containing some undefs
(depending on how you asked for them). While dequeue_nb() is going to
return an empty list. I think this consistent with how these methods
work normally, dequeue() is always going to give you a fixed list while
dequeue_nb() gives you whatever's left.
Show quoted text> Then in your merged _dequeue_nb() you have:
>
> push(@items, shift(@$queue)) for (1..$count);
>
> If $count is greater than the size of the queue, then
> 'undef's will be added to the queue which again may (or may
> not) be problematic. That is why the two dequeue methods
> were separate.
Assuming the queue is locked during the operation (ie. your cond fixes
are applied) this is accounted for. dequeue() will always return a
fixed size list. dequeue_nb() will knock the count down to what's left
in the queue...
# If there's not enough left in the queue, return what's left.
$count = @$queue if @$queue < $count;
This is the same as their current behaviors which I just left alone.
If it makes you more comfortable to keep them separate but a bit
redundant that's fine.
Show quoted text> I think I will eliminate the should_block() call. I cannot
> conceive of a significant situation when a programmer would
> need to sent a queue to non-blocking and then back again.
I'm cool with eliminating it from the public API.
Show quoted text> All told, thanks for the ideas and the code. I will work it
> into an update and send it to you under separate cover for
> your further comment.
This reply is already too huge. Let me do that in a separate reply.