0

I'm looking a way to dynamically add arguments to a Pool of workers during the same iteration. So, in the case some of these fails I'm able to promptly re-process it.

from numpy import random
from multiprocessing import Pool
from time import sleep

def foo(x):
    sleep(0.1)
    # 50% chance to have a fault
    return x, x if random.rand() > 0.5 else -1

random.seed(3)      # seed
pool = Pool(2)      # process
args = range(5)     # arguments to process

for i,(id,x) in enumerate(pool.imap(foo, args)):
    print i,x
    if x != -1:
        args.remove(id)

print args

The output is

0 0
1 1
2 2
3 3
4 -1
[4]

but I'd like it to be

0 0
1 1
2 2
3 3
4 -1
5, 4
[]

within the same iteration. I meant, I don't want to create a new map to the same Pool of workers once the iteration is complete. I'd like to push new argument directly, so that it fails at the first iteration I don't have to wait till the end before using the available process! I hope it make sense...

Update: My problem above simplified, the "foo" function takes about 20 minutes to complete and it's spread across 24 process which run concurrently. As soon one process fails I need to re-processing as soon as possible, as I don't want to wait 20 minutes when I have available resources.

Alessandro Mariani
  • 1,181
  • 2
  • 10
  • 26
  • FIY: The `<>` operator should *not* be used. It's a wart of python2 which was *deprecated* since python 2.0 and *was removed* in python3+. Using it has the *only* effect of: 1) making code less readable for others since nobody uses it 2) making the code less portable. I really see no reason to use it instead of the usual `!=`. – Bakuriu Oct 09 '14 at 14:54
  • changed - I don't like either! – Alessandro Mariani Oct 09 '14 at 16:03
  • As far as I'm aware, you *can't* add new tasks to a running `pool`. I can't tell from the question what exactly you're trying to do--if the process fails for some argument, do you want to retry that argument or just note the failure and move on to the next one? Based on your intended output, I'm guessing that you want to retry with the same argument, but clarification would help. – Henry Keiter Oct 09 '14 at 16:10
  • if the process fails, I'd like to retry that argument straight away! – Alessandro Mariani Oct 09 '14 at 16:16

2 Answers2

1

As far as I know, you can't add a task to a currently-running Pool (without creating a race condition or undefined behavior, as you're currently seeing). Luckily, since all you need to do is retry any failed tasks until successful completion, you don't actually need to add anything to the Pool. All you need to do is modify the mapped function to behave the way you want.

def foo(x):
    sleep(0.1)
    # 50% chance to have a fault
    return x, x if random.rand() > 0.5 else -1

def successful_foo(x):
    '''Version of the foo(x) function that never fails.'''

    result = -1
    while result == -1:
        result = foo(x)
    return result

Now you can pool.imap(successful_foo, args), and be assured that every process will complete successfully (or run forever). If it's possible that it could run forever and you want an option to abort after some number of tries or some amount of time, just replace the while loop with an appropriate counter or timer.


Of course, in many non-demo cases, having a special return value to indicate failure is impractical. In that situation, I prefer to use a specialized Exception to handle the sorts of predictable failures you might encounter:

class FooError(BaseException):
    pass

def foo(x):
    sleep(0.1)
    # 50% chance to have a fault
    if random.rand() > 0.5:  # fault condition
        raise FooError('foo had an error!')
    return x, x

def successful_foo(x):
    '''Version of the foo(x) function that never fails.'''

    while True:
        try:
            return foo(x)
        except FooError as e:
            pass  # Log appropriately here; etc.
Henry Keiter
  • 16,863
  • 7
  • 51
  • 80
0

You can't. You want to modify a mutable list during iteration, this is known to not work. The output you get is due to the fact that when you remove an item form the list, the list reduces its length by 1 and all items after the one you removed are moved one index before. Which means that the item that followed is skipped.

The problem has nothing to do with multiprocessing per se, but with plain lists:

In [1]: def f(x):
   ...:     print(x)
   ...:     

In [2]: args = [0, 1, 2, 3, 4, 5]

In [3]: for i, x in enumerate(args):
   ...:     print(i, x)
   ...:     if x % 2 == 0:
   ...:         args.remove(x)
   ...:         
0 0
1 2
2 4

In [4]: args
Out[4]: [1, 3, 5]

note how the loop iterated only over the even values and never saw the odd values.

You want to keep track of which items to remove and only do that at the end of the loop:

to_be_removed = []
for i, (ident, x) in enumerate(pool.imap(foo, args)):
    print(i, x)
    if x != -1:
        to_be_removed.append(ident)

for ident in to_be_removed:
    args.remove(ident)

Or, probably more efficient, you can use a set and re-build the args list:

to_be_removed = set()
for i, (ident, x) in enumerate(pool.imap(foo, args)):
    print(i, x)
    if x != -1:
        to_be_removed.add(ident)

args = [el for el in args if el not in to_be_removed]

This takes linear time instead of, possibly, quadratic time of the previous solutions.


You can create also create a custom iterator that can do arbitrarily complex decisions as to which elements to produce for every iteration, however I'm not sure this would really work with multiprocessing since I believe it does not consume items one by one (otherwise it wouldn't be able to parallelize), and hence you wouldn't be able to provide any guarantee that the modifications will actually be seen when you expect them.

Moreover such a thing is only asking for bugs.

Bakuriu
  • 98,325
  • 22
  • 197
  • 231
  • I've tried "args.append(id)" instead of "args.remove(id)", which given what you're saying should increase the iteration by one all the time I'm appending something to the list, but it doesn't work either. My problem above is really oversimplify, the "foo" function takes about 20 minutes to complete and it's spread across 24 process which ran concurrently, and as soon one process fails I need to re-processing as soon as possible. – Alessandro Mariani Oct 09 '14 at 11:11
  • @AlessandroMariani I did **not** say that using `args.append` whould increase the iteration by one. What I wanted to say is that modifying the size of a list during iteration you get *undefined behaviour*. In fact, if you try to do that to a `dict` you will get an exception instead of silently skipping elements. You simply *cannot* rely on the list iteration. If you want some guarantees you *must* build them yourself using a completely customized iterator. – Bakuriu Oct 09 '14 at 14:52
  • @AlessandroMariani Also, when it comes to `Pool`, the pool does *not* iterate one element at a time over the list. It divides the lists in chunks of *k* jobs and dispatch those to different processes. In other words the modifications you are doing have a race condition with the workers. To make that thing work as you intend you'd have to use a process lock, which would decrease parallelization and defeat the purpose of multiprocessing. Anyway, I believe my answer answered your original question. If you have a *new* question about complex job dispatching using a Pool you should open a new one. – Bakuriu Oct 09 '14 at 14:57
  • I see - thanks for the clarification and replies bakuriu! I haven't asked how can I extend my list, I'm asking how can I solve my problem. If this mean I need to use a shared Queue or other more complex structure to do this I'll go that path. Since I'm neither a Python expert and multiprocess expert, I'm asking if my problem could be easily solved. Given what you're saying, do you have any suggestions? – Alessandro Mariani Oct 09 '14 at 16:11