如何在Python中公平地为工作人员分配任务? - 可迭代分割成类似大小的块

时间:2012-10-30 17:52:32

标签: python python-3.x split itertools iterable

我有工作人员和任务要做:

workers = ['peter', 'paul', 'mary']
tasks = range(13)

现在我想将任务拆分成块或批量工作,这样每个工作人员可以在一个批处理上工作,并且完成与其他人相同的工作量。在我的现实生活中,我想将批处理作业安排到计算场。批处理作业应该并行运行。实际的时间表和调度由商业级工具完成,例如lsf或grid。

我期望的一些例子:

>>> distribute_work(['peter', 'paul', 'mary'], range(3))
[('peter', [0]), ('paul', [1]), ('mary', [2])]
>>> distribute_work(['peter', 'paul', 'mary'], range(6))
[('peter', [0, 3]), ('paul', [1, 4]), ('mary', [2, 5])]
>>> distribute_work(['peter', 'paul', 'mary'], range(5))
[('peter', [0, 3]), ('paul', [1, 4]), ('mary', [2])]

此问题与问题hereherehere

非常相似

不同之处在于我想按顺序或优先顺序使用这些功能:

  1. 如果可能,不使用len内部没有长数据结构的构建
  2. 接受发电机
  3. 返回发电机
  4. 尽可能多地使用stdlib组件
  5. 关于要求的一些附注:

    • 没有任何意图:我有同名的工人可以做多批(unix主机名)。如果您的解决方案使用dicts,那很好,因为我们总是可以通过批处理枚举来查找工作程序。
    • 任意长度:工作人员和任务都可以是任意长度的可迭代的> = 1.并且它们不必均匀分割,如上面的示例所示,其中Mary只获得一个任务。
    • 订单:对我来说并不重要。我想其他人可能更喜欢像[0,1],[2,3],[5]那样的顺序,但我不在乎。如果您的解决方案可以保留或切换订单,那么可能值得指出其他人。

    我试图绕过itertools和这个特殊问题,并提出以下代码来说明问题:

    from itertools import *
    
    def distribute_work(workers, tasks):
        batches = range(len(workers))
        return [ ( workers[k],
                   [t[1] for t in i]
                   )   for (k,i) in groupby(sorted(zip(cycle(batches),
                                                       tasks),
                                                   key=lambda t: t[0]),
                                            lambda t: t[0]) ]
    

    这满足4.但是这种类型很可能违反了1 ..和2./3。甚至没想过。

    可能有一些简单的解决方案,以一种我没想过的方式组合一些stdlib组件。但也许不是。任何人?

5 个答案:

答案 0 :(得分:1)

你必须预先批处理吗?

为什么不只是拥有一个队列,并让每个工作人员在完成工作单元时弹出队列?

答案 1 :(得分:1)

关注Tyler's answer

def doleOut(queue, workers):
    for worker,task in itertools.izip(itertools.cycle(workers),queue):
        yield worker,task

只要有队列,这将继续返回(worker, task)个元组。因此,如果你有阻止waitForMoreWork,你可以这样做:

queue = []
doler = distribute_work(workers, queue)
while 1:
    queue.append(waitForMoreWork)
    currentqueuelen = len(queue)
    for i in range(0,queuelen):
        worker,item = doler.next()
        worker.passitem(item)

这样它会阻塞,直到有更多队列项,然后分发它们,然后再次阻止。您可以将waitForMoreWork表达式设置为一次分发尽可能多的项目。

答案 2 :(得分:1)

我认为您希望使用multiprocessing.Pool.imap来处理您的员工并分配他们的工作。我相信它能做你想做的一切。

jobs = (some generator)                   # can consume jobs from a generator
pool = multiprocessing.Pool(3)            # set number of workers here
results = pool.imap(process_job, jobs)    # returns a generator

for r in results:                         # loop will block until results arrive
    do_something(r)

如果结果的顺序与您的应用无关,您也可以使用imap_unordered

答案 3 :(得分:0)

好的,在说不可能之后,这是一个想法。也许这是我应该转向代码审查的东西 - 我非常感兴趣的是关于它在内存中产生多少开销的评论。换句话说,我不知道这是否真的解决了任务列表很长且未知大小的问题。 作为Blckknght mentioned multiprocessing might be the better alternative

代码:

import itertools

def distribute_work(workers, tasks):
    """Return one generator per worker with a fair share of tasks

    Task may be an arbitrary length generator.
    Workers should be an iterable.
    """
    worker_count = len(workers)
    worker_ids = range(worker_count)
    all_tasks_for_all_workers = itertools.tee(tasks, worker_count)
    assignments = [ (workers[id], itertools.islice(i, id, None, worker_count))
                    for (id,i) in enumerate(all_tasks_for_all_workers) ]    
    return(assignments)

算法是

  1. 为每个工作人员复制一次原始任务列表。由于这只是复制生成器对象,因此它应该与内存中任务列表的大小无关。即使这是一个相对昂贵的操作,它只是一次性的启动成本,对于非常大的任务列表而言内存微不足道。
  2. 要将任务分配给一个工作人员,每个工作人员必须抓住任务列表的一部分。如果#W是工作人员数量,则第一个工作人员会执行0#W2*#W3*#W等任务。第二个工作人员需要{{1} }},0+1#W+12*#W+1等。每个工作人员的拼接都可以使用3*#W+1
  3. 完成

    对于纯粹的拆分/分配任务,此功能并不真正需要工人的名称。但是工人的数量是多少。更改此功能将使该功能更加通用和有用,并使返回值更容易理解。要回答我自己的问题,我将按原样保留该功能。

    用法和结果:

    itertools.islice

    它还处理工人具有相同名称但不同实体的情况:

    >>> for (worker,tasks) in distribute_work(['peter', 'paul', 'mary'], range(5)):
    ...   print(worker, list(tasks))
    ... 
    peter [0, 3]
    paul [1, 4]
    mary [2]
    

答案 4 :(得分:0)

这是我喜欢的方法:

parallelism = os.cpu_count()
num_todos = len(todos)

# this zip fanciness makes each chunk stripe through the data sequentially overall so that the
# first items still get done first across all the workers
chunksize = math.ceil(num_todos / parallelism)
chunks = list(itertools.zip_longest(*[todos[i:i+chunksize] for i in range(0, num_todos, chunksize)]))
chunks = [[c for c in chunk if c is not None] for chunk in chunks]

with Pool(processes=parallelism) as pool:
    tasks = [pool.apply_async(my_function, args=(chunk)) for chunk in chunks]
    [task.get() for task in tasks]

根据您是否需要累积结果,您可以进行调整,但对我来说有趣的部分是让工作人员协作以全局顺序完成工作(在我的情况下,处理连续的图像帧以便我可以看到事情看起来像所有的cpus都在发动。)