3

I have a server and several clients. They all share a task and results multiprocessing.Queue. However whenever a client finishes a task and puts result on results queue, I want the server to look at the results, and based on that, re-order the tasks queue.

This means of course popping everything off the tasks queue and re-adding. During this re-ordering process, I want the clients to block touching the tasks queue. My question is how I get the server to recognize when a task is added to the results queue and react by locking the tasks queue and reordering while protecting the queue. The invariant is that the server must re-order after every result returned before clients get a new task.

I suppose a simple (but wrong) way would be to have a multiprocessing.Value act as a boolean and whenever a result is added the client flips that to True, meaning a result has been added. The server could poll to get this value but ultimately it could miss another client coming in between polls and adding another result.

Any thoughts appreciated.

** The 'multithreading' tag is just because its very similar thought as in threading, I don't think the process/thread distinction here matters much.

3
  • I am not sure what part you are having a problem with. Is the problem how to protect the queue or how to notify the server when a new result arrives? What does the server need to be doing the rest of the time? I assume it can't just block in result_queue.get(). Commented Oct 10, 2013 at 4:00
  • the server is what listens for clients to connect and also is where the stored memory is located Commented Oct 10, 2013 at 14:40
  • a process (could be server) could listen on the results queue, but it would have to react and immediately attempt to lock the tasks queue on success, which reduces to the same as the polling case, effectively Commented Oct 10, 2013 at 14:56

1 Answer 1

1

Let's try some code - some progress is better than none ;-) Part of the problem is to ensure that nothing gets taken from the task queue if the result queue has something in it, right? So the queues are intimately connected. This approach puts both queues under the protection of a lock, and uses Conditions to avoid any need for polling:

Setup, done in server. taskQ, resultQ, taskCond and resultCond must be passed to the client processes (lock need not be explicitly passed - it's contained in the Conditions):

import multiprocessing as mp
taskQ = mp.Queue()
resultQ = mp.Queue()
lock = mp.Lock()
# both conditions share lock
taskCond = mp.Condition(lock)
resultCond = mp.Condition(lock)

Client gets task; all clients use this function. Note that a task won't be consumed so long as the result queue has something in it:

def get_task():
    taskCond.acquire()
    while taskQ.qsize() == 0 or resultQ.qsize():
        taskCond.wait()
    # resultQ is empty and taskQ has something
    task = taskQ.get()
    taskCond.release()
    return task

Client has result:

with resultCond:
    resultQ.put(result)
    # only the server waits on resultCond
    resultCond.notify()

Server loop:

resultCond.acquire()
while True:
    while resultQ.qsize() == 0:
        resultCond.wait()
    # operations on both queues in all clients are blocked now
    # ... drain resultQ, reorder taskQ ...
    taskCond.notify_all()

Notes:

  1. qsize() is usually probabilistic, but because all queue operations are done while the lock is held, it's reliable in this context.

  2. In fact, because all queue operations are protected by our own lock here, there's really no need to use mp.Queues. For example, an mp.Manager().list() would work too (any shared structure). Perhaps a list would be easier to work with when you're rearranging tasks?

  3. One part I don't like much: when the server does taskCond.notify_all(), some clients may be waiting to get a new task, while others may be waiting to return a new result. They may run in any order. As soon as any client waiting to return a result gets a chance, all clients waiting to get a task will block, but before then tasks will be consumed. "The problem" here, of course, is that we have no idea a new result is waiting before something is actually added to the result queue.

For the last one, perhaps changing the "client has result" code to:

resultQ.put(result)
with resultCond:
    resultCond.notify()

would be better. Unsure. It does make it significantly harder to reason about, because it's then no longer true that all queue operations are done under the protection of our lock.

Sign up to request clarification or add additional context in comments.

3 Comments

I came up with something very similar. The key insight you mention is that becase of the invariant, the queues are tied together in a sense. So once locked, they need not be special thread-safe data structures. I ended up having the server listener threads (the ones dispached to listen on sockets for clients after clients connect) actually do the re-ordering. This is best since these listeners actually respond to completed tasks. Their blocking re-ordering and result dispatching truly go together.
Though I admit I don't quite understand multiprocessing.Condition class usage - why not use a regular lock?
Cool! Conditions have a learning curve, but once learned Conditions greatly ease writing correct parallel-safe code. That's why, e.g., Python's threading.py builds Events and Barriers and Semaphores on top of Conditions. A key benefit is that - as above - the underlying lock in a Condition is never held for a long time. Code either releases the lock very soon, or soon does a .wait() (which releases the lock, and (re)acquires the lock when a .notify() ends the wait). No polling, no guesswork. But Conditions are built on locks, so - no - they're not truly essential.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.