Celery任务设置,使用视频帧的内存缓存作为python中的循环缓冲区策略

时间:2018-10-30 15:05:15

标签: python caching celery shared-memory video-processing

我想在Celery上建立一个多任务处理管道,并希望多个任务处理同一个视频文件。 这些任务需要共享视频数据。因此,并非每个任务都必须解码并从视频文件中提取帧。视频数据将是提取的帧的列表(不需要视频的每个帧)。

有没有有效共享这些帧的解决方案?可以在不同的节点上处理任务。但是我不想像Memcached或Redis这样通过网络共享数据。 一个任务应该在内存/缓存中查找视频数据,如果不存在,则应该发出另一个任务来加载视频并提取要缓存的帧。

(每个视频文件的制作者和多个使用者)

因此,同一节点/计算机上的任务能够共享缓存的数据。不同节点上的两个任务通过缓存没有好处。

我不想缓存整个提取的视频,必须有一些循环缓冲区缓存。每个视频的缓存具有固定大小,可以说100帧。最快和最慢任务之间的间隔不能超过100帧。内存/缓存中总共只有100帧。

出现两个主要问题:

  1. 任务设置

    任务A:从视频中提取帧(生产者到内存/缓存)

    任务B1:消耗帧并进行实际工作(处理帧)

    任务Bn:消耗帧并做实际工作(处理帧)

    A,B1-Bn并行运行。 但是,这些任务必须在同一节点上运行。如果B taks分布在不同的节点上,则必须生成另一个A任务(每个节点上一个任务来解码和提取帧)。 您在这里推荐什么方法?最好的选择是什么?

  2. Python缓存

    有没有最适合我的用例的缓存库/实现/解决方案,可以通过一些循环缓冲区实现在本地计算机上缓存大数据? 类似于DiskCache,但只能通过环形缓冲来缓存100帧。

您建议采用什么方法和设计来实现我的用例?我想坚持用Celery进行任务分配。

1 个答案:

答案 0 :(得分:1)

这可能是我的强表现,但是我总是发现像芹菜这样的项目在多处理(已经很复杂)的基础上增加了很多复杂性,这比它们值得的麻烦更多。从速度和简便性的角度来看,没有比使用stdlib共享内存和互斥更好的imo了。

对于您而言,一个简单的解决方案是仅对每个进程使用fifo队列,然后将框架从生产者处放入每个进程。如果您要为n个使用者制作每个帧的n个副本,那么自然会产生大量的内存使用,但是,您可能很容易想出一种将帧本身放入multiprocessing.sharedctypes.Array并仅传递索引的机制通过队列代替。只要限制队列的长度短于缓冲区的长度,就应限制覆盖缓冲区中的帧,直到所有使用者都将其使用为止。没有任何同步,这将在您的裤子旁边飞来飞去,但是一点点的互斥量魔术肯定可以使它成为一个非常强大的解决方案。

例如:

import numpy as np
from time import sleep
from multiprocessing import Process, freeze_support, Queue
from multiprocessing.sharedctypes import Array
from ctypes import c_uint8
from functools import reduce

BUFSHAPE = (10,10,10) #10 10x10 images in buffer

class Worker(Process):
    def __init__(self, q_size, buffer, name=''):
        super().__init__()
        self.queue = Queue(q_size)
        self.buffer = buffer
        self.name = name

    def run(self,): #do work here
        #I hardcoded datatype here. you might need to communicate it to the child process
        buf_arr = np.frombuffer(self.buffer.get_obj(), dtype=c_uint8)
        buf_arr.shape = BUFSHAPE
        while True:
            item = self.queue.get()
            if item == 'done':
                print('child process: {} completed all frames'.format(self.name))
                return
            with self.buffer.get_lock(): #prevent writing while we're reading
                #slice the frame from the array uning the index that was sent
                frame = buf_arr[item%BUFSHAPE[0]] #depending on your use, you may want to make a copy here
            #do some intense processing on `frame`
            sleep(np.random.rand())
            print('child process: {} completed frame: {}'.format(self.name, item))

def main():
    #creating shared array
    buffer = Array(c_uint8, reduce(lambda a,b: a*b, BUFSHAPE))
    #make a numpy.array using that memory location to make it easy to stuff data into it
    buf_arr = np.frombuffer(buffer.get_obj(), dtype=c_uint8)
    buf_arr.shape = BUFSHAPE
    #create a list of workers
    workers = [Worker(BUFSHAPE[0]-2, #smaller queue than buffer to prevent overwriting frames not yet consumed
                      buffer, #pass in shared buffer array
                      str(i)) #numbered child processes
                      for i in range(5)] #5 workers

    for worker in workers: #start the workers
        worker.start()
    for i in range(100): #generate 100 random frames to send to workers
        #insert a frame into the buffer
        with buffer.get_lock(): #prevent reading while we're writing
            buf_arr[i%BUFSHAPE[0]] = np.random.randint(0,255, size=(10,10), dtype=c_uint8)
        #send the frame number to each worker for processing. If the input queue is full, this will block until there's space
        # this is what prevents `buf_arr[i%BUFSHAPE[0]] = np...` from overwriting a frame that hasn't been processed yet
        for worker in workers:
            worker.queue.put(i)
    #when we're done send the 'done' signal so the child processes exit gracefully (or you could make them daemons)
    for worker in workers:
        worker.queue.put('done')
        worker.join()


if __name__ == "__main__":
    freeze_support()
    main()

编辑

某种偏离一格的错误要求队列比缓冲区小2帧,而不是小1帧,以防止在时间之前覆盖帧。

EDIT2-首次编辑的说明:

len(q) = len(buf)-2的原因似乎是q.get()在我们从缓冲区中获取帧之前被调用,而帧本身是在我们尝试将索引推送到队列之前被写入的。如果长度差只有1,则工作人员可能会从队列中拉出一个帧索引,那么生产者可能会看到它可以立即推送到队列并移至下一帧,然后工作人员才有机会读取该帧本身。您可以采用多种方法来处理不同的问题,这可能使更少的进程始终保持等待状态,也许使用mp.Event