web-dev-qa-db-ja.com

Pythonジェネレータによるマルチプロセッシング

ファイルを処理しようとしています(すべての行がjsonドキュメントです)。ファイルのサイズは、最大で数百MBからGBになります。そのため、ファイルから1行ずつ各ドキュメントを取得するジェネレータコードを作成しました。

def jl_file_iterator(file):
    with codecs.open(file, 'r', 'utf-8') as f:
        for line in f:
            document = json.loads(line)
            yield document

私のシステムには4つのコアがあるので、ファイルの4行を並行して処理したいと思います。現在、私はこのコードを一度に4行取り、並列処理のためにコードを呼び出します

threads = 4
files, i = [], 1
for jl in jl_file_iterator(input_path):
    files.append(jl)
    if i % (threads) == 0:
        # pool.map(processFile, files)
        parallelProcess(files, o)
        files = []
    i += 1

if files:
    parallelProcess(files, o)
    files = []

これは実際の処理が行われる私のコードです

def parallelProcess(files, outfile):
    processes = []
    for i in range(len(files)):
        p = Process(target=processFile, args=(files[i],))
        processes.append(p)
        p.start()
    for i in range(len(files)):
        processes[i].join()

def processFile(doc):
    extractors = {}
    ... do some processing on doc
    o.write(json.dumps(doc) + '\n')

ご覧のとおり、次の4つのファイルを送信する前に、4行すべての処理が完了するのを待ちます。しかし、私がしたいことは、1つのプロセスがファイルの処理を終えたらすぐに、次の行を開始して、実際のプロセッサに割り当てたいと思います。それ、どうやったら出来るの?

PS:問題は、そのジェネレーターなので、すべてのファイルをロードできず、マップのようなものを使用してプロセスを実行できないことです。

ご協力いただきありがとうございます

17
Muthu Rg

@pvgがコメントで述べたように、(制限付き)キューは、プロデューサーとコンシューマーの間で異なる速度で仲介する自然な方法であり、プロデューサーを先に進めずに、すべてを可能な限りビジー状態に保ちます。

次に、自己完結型の実行可能な例を示します。キューは、ワーカープロセスの数に等しい最大サイズに制限されます。コンシューマーがプロデューサーよりもはるかに速く実行する場合、キューをそれより大きくすることは理にかなっています。

特定のケースでは、コンシューマに行を渡して、document = json.loads(line)の部分を並行して実行させることはおそらく意味があります。

import multiprocessing as mp

NCORE = 4

def process(q, iolock):
    from time import sleep
    while True:
        stuff = q.get()
        if stuff is None:
            break
        with iolock:
            print("processing", stuff)
        sleep(stuff)

if __name__ == '__main__':
    q = mp.Queue(maxsize=NCORE)
    iolock = mp.Lock()
    pool = mp.Pool(NCORE, initializer=process, initargs=(q, iolock))
    for stuff in range(20):
        q.put(stuff)  # blocks until q below its max size
        with iolock:
            print("queued", stuff)
    for _ in range(NCORE):  # tell workers we're done
        q.put(None)
    pool.close()
    pool.join()
18
Tim Peters

だから私はこれをうまく実行することになりました。私のファイルから行のチャンクを作成し、それらの行を並行して実行します。それをここに投稿して、将来誰かに役立つようにします。

def run_parallel(self, processes=4):
    processes = int(processes)
    pool = mp.Pool(processes)
    try:
        pool = mp.Pool(processes)
        jobs = []
        # run for chunks of files
        for chunkStart,chunkSize in self.chunkify(input_path):
            jobs.append(pool.apply_async(self.process_wrapper,(chunkStart,chunkSize)))
        for job in jobs:
            job.get()
        pool.close()
    except Exception as e:
        print e

def process_wrapper(self, chunkStart, chunkSize):
    with open(self.input_file) as f:
        f.seek(chunkStart)
        lines = f.read(chunkSize).splitlines()
        for line in lines:
            document = json.loads(line)
            self.process_file(document)

# Splitting data into chunks for parallel processing
def chunkify(self, filename, size=1024*1024):
    fileEnd = os.path.getsize(filename)
    with open(filename,'r') as f:
        chunkEnd = f.tell()
        while True:
            chunkStart = chunkEnd
            f.seek(size,1)
            f.readline()
            chunkEnd = f.tell()
            yield chunkStart, chunkEnd - chunkStart
            if chunkEnd > fileEnd:
                break
8
Muthu Rg

Tim Petersの答え は素晴らしいです。
しかし、私の具体的なケースは少し異なり、私のニーズに合うように彼の答えを変更する必要がありました。ここを参照。
これはコメント内の@CpILLの質問に答えます。


私の場合、ジェネレータのチェーンを使用しました(パイプラインを作成するため)。
このジェネレータのチェーンの中で、そのうちの1つは重い計算を行っていて、パイプライン全体を遅くしていました。

このようなもの :

def fast_generator1():
    for line in file:
        yield line

def slow_generator(lines):
    for line in lines:
        yield heavy_processing(line)

def fast_generator2():
    for line in lines:
        yield fast_func(line)

if __name__ == "__main__":
    lines = fast_generator1()
    lines = slow_generator(lines)
    lines = fast_generator2(lines)
    for line in lines:
        print(line)

高速化するには、複数のプロセスでスロージェネレーターを実行する必要があります。
変更されたコードは次のようになります。

import multiprocessing as mp

NCORE = 4

def fast_generator1():
    for line in file:
        yield line

def slow_generator(lines):
    def gen_to_queue(input_q, lines):
        # This function simply consume our generator and write it to the input queue
        for line in lines:
            input_q.put(line)
        for _ in range(NCORE):    # Once generator is consumed, send end-signal
            input_q.put(None)

    def process(input_q, output_q):
        while True:
            line = input_q.get()
            if line is None:
                output_q.put(None)
                break
            output_q.put(heavy_processing(line))


    input_q = mp.Queue(maxsize=NCORE * 2)
    output_q = mp.Queue(maxsize=NCORE * 2)

    # Here we need 3 groups of worker :
    # * One that will consume the input generator and put it into a queue. It will be `gen_pool`. It's ok to have only 1 process doing this, since this is a very light task
    # * One that do the main processing. It will be `pool`.
    # * One that read the results and yield it back, to keep it as a generator. The main thread will do it.
    gen_pool = mp.Pool(1, initializer=gen_to_queue, initargs=(input_q, lines))
    pool = mp.Pool(NCORE, initializer=process, initargs=(input_q, output_q))

    finished_workers = 0
    while True:
        line = output_q.get()
        if line is None:
            finished_workers += 1
            if finished_workers == NCORE:
                break
        else:
            yield line

def fast_generator2():
    for line in lines:
        yield fast_func(line)

if __name__ == "__main__":
    lines = fast_generator1()
    lines = slow_generator(lines)
    lines = fast_generator2(lines)
    for line in lines:
        print(line)

この実装では、マルチプロセスジェネレーターがあります。他のジェネレーターとまったく同じように使用されますが(この回答の最初の例のように)、重い処理はすべてマルチプロセッシングを使用して行われ、高速化されます。

0
Astariul