複数のプロセスからの単一ファイルの処理


82

各行を処理して(いくつかの操作を実行して)データベースに保存したい単一の大きなテキストファイルがあります。単一の単純なプログラムには時間がかかりすぎるので、複数のプロセスまたはスレッドを介して実行する必要があります。各スレッド/プロセスは、その単一のファイルから異なるデータ(異なる行)を読み取り、それらのデータ(行)に対していくつかの操作を実行してデータベースに配置する必要があります。これにより、最終的にすべてのデータが処理され、データベースは必要なデータとともにダンプされます。

しかし、私はこれにどのようにアプローチするかを理解することができません。


3
いい質問です。私もこの疑問を持っていました。私はファイルをより小さなファイルに分割するオプションを選択しましたが:)
Sushant Gupta 2012

回答:


109

あなたが探しているのは生産者/消費者パターンです

基本的なスレッドの例

これは、(マルチプロセッシングの代わりに)スレッドモジュールを使用した基本的な例です。

import threading
import Queue
import sys

def do_work(in_queue, out_queue):
    while True:
        item = in_queue.get()
        # process
        result = item
        out_queue.put(result)
        in_queue.task_done()

if __name__ == "__main__":
    work = Queue.Queue()
    results = Queue.Queue()
    total = 20

    # start for workers
    for i in xrange(4):
        t = threading.Thread(target=do_work, args=(work, results))
        t.daemon = True
        t.start()

    # produce data
    for i in xrange(total):
        work.put(i)

    work.join()

    # get the results
    for i in xrange(total):
        print results.get()

    sys.exit()

ファイルオブジェクトをスレッドと共有することはありません。キューにデータ行を提供することで、彼らのために仕事を生み出します。次に、各スレッドは行を取得して処理し、キューに戻します。

リストや特別な種類のキューなど、データを共有するためにマルチプロセッシングモジュールに組み込まれているより高度な機能がいくつかあります。マルチプロセッシングとスレッドの使用にはトレードオフがあり、作業がCPUバウンドかIOバウンドかによって異なります。

基本的なマルチプロセッシング。プールの例

これがマルチプロセッシングプールの本当に基本的な例です

from multiprocessing import Pool

def process_line(line):
    return "FOO: %s" % line

if __name__ == "__main__":
    pool = Pool(4)
    with open('file.txt') as source_file:
        # chunk the work into batches of 4 lines at a time
        results = pool.map(process_line, source_file, 4)

    print results

プールは、独自のプロセスを管理する便利なオブジェクトです。開いているファイルはその行を反復処理できるため、ファイルをに渡すことができます。これにより、ファイルがpool.map()ループされ、ワー​​カー関数に行が配信されます。マップはブロックし、完了すると結果全体を返します。これは非常に単純化された例であり、pool.map()作業を行う前にファイル全体を一度にメモリに読み込むことに注意してください。大きなファイルが必要な場合は、このことに注意してください。プロデューサー/コンシューマーのセットアップを設計するためのより高度な方法があります。

制限と行の再ソートを伴う手動「プール」

これはPool.mapの手動の例ですが、反復可能ファイル全体を一度に消費する代わりに、キューサイズを設定して、処理可能な速度で1つずつフィードするようにすることができます。また、後で追跡して参照できるように、行番号を追加しました。

from multiprocessing import Process, Manager
import time
import itertools 

def do_work(in_queue, out_list):
    while True:
        item = in_queue.get()
        line_no, line = item

        # exit signal 
        if line == None:
            return

        # fake work
        time.sleep(.5)
        result = (line_no, line)

        out_list.append(result)


if __name__ == "__main__":
    num_workers = 4

    manager = Manager()
    results = manager.list()
    work = manager.Queue(num_workers)

    # start for workers    
    pool = []
    for i in xrange(num_workers):
        p = Process(target=do_work, args=(work, results))
        p.start()
        pool.append(p)

    # produce data
    with open("source.txt") as f:
        iters = itertools.chain(f, (None,)*num_workers)
        for num_and_line in enumerate(iters):
            work.put(num_and_line)

    for p in pool:
        p.join()

    # get the results
    # example:  [(1, "foo"), (10, "bar"), (0, "start")]
    print sorted(results)

1
これは良いことですが、処理がI / Oバウンドの場合はどうでしょうか。その場合、並列処理は速度を上げるのではなく、速度を落とす可能性があります。単一のディスクトラック内のシークは、トラック間シークよりもはるかに高速であり、I / Oを並行して実行すると、シーケンシャルI / Oロードとなるトラック間シークが発生する傾向があります。並列I / Oのメリットを享受するには、RAIDミラーを使用すると非常に役立つ場合があります。
user1277476 2012年

2
@ jwillis0720-もちろんです。 (None,) * num_workersワーカー数のサイズに等しいNone値のタプルを作成します。これらは、作業がなくなったために各スレッドに終了するように指示する番兵の値になります。このitertools.chain関数を使用すると、何もコピーせずに、複数のシーケンスを1つの仮想シーケンスにまとめることができます。したがって、最初にファイルの行をループし、次にNone値をループします。
jdi 2014

2
それは私の教授よりもよく説明されています、とても素敵な+1です。
lycuid 2016年

1
@ℕʘʘḆḽḘ、テキストを少し編集してわかりやすくしました。ここで、真ん中の例では、ファイルデータ全体を一度にメモリに丸呑みすることを説明します。これは、ファイルが現在使用可能なRAMの量よりも大きい場合に問題になる可能性があります。次に、3番目の例で、ファイル全体を一度に消費しないように、行ごとに移動する方法を示します。
jdi 2018年

1
@ℕʘʘḆḽḘpool.Map()のドキュメントを読んでください。iterableをチャンクに分割し、ワーカーに送信すると書かれています。したがって、メモリ内のすべての行を消費することになります。はい、一度に1行ずつ繰り返すとメモリ効率が高くなりますが、これらすべての行をメモリに保持することになった場合は、ファイル全体の読み取りに戻ります。
jdi 2018年

9

これが私が作った本当にばかげた例です:

import os.path
import multiprocessing

def newlinebefore(f,n):
    f.seek(n)
    c=f.read(1)
    while c!='\n' and n > 0:
        n-=1
        f.seek(n)
        c=f.read(1)

    f.seek(n)
    return n

filename='gpdata.dat'  #your filename goes here.
fsize=os.path.getsize(filename) #size of file (in bytes)

#break the file into 20 chunks for processing.
nchunks=20
initial_chunks=range(1,fsize,fsize/nchunks)

#You could also do something like:
#initial_chunks=range(1,fsize,max_chunk_size_in_bytes) #this should work too.


with open(filename,'r') as f:
    start_byte=sorted(set([newlinebefore(f,i) for i in initial_chunks]))

end_byte=[i-1 for i in start_byte] [1:] + [None]

def process_piece(filename,start,end):
    with open(filename,'r') as f:
        f.seek(start+1)
        if(end is None):
            text=f.read()
        else: 
            nbytes=end-start+1
            text=f.read(nbytes)

    # process text here. createing some object to be returned
    # You could wrap text into a StringIO object if you want to be able to
    # read from it the way you would a file.

    returnobj=text
    return returnobj

def wrapper(args):
    return process_piece(*args)

filename_repeated=[filename]*len(start_byte)
args=zip(filename_repeated,start_byte,end_byte)

pool=multiprocessing.Pool(4)
result=pool.map(wrapper,args)

#Now take your results and write them to the database.
print "".join(result)  #I just print it to make sure I get my file back ...

ここで注意が必要なのは、ファイルを改行文字で分割して、行を見逃さないようにする(または部分的な行だけを読み取る)ようにすることです。次に、各プロセスはファイルの一部を読み取り、メインスレッドによってデータベースに配置できるオブジェクトを返します。もちろん、すべての情報を一度にメモリに保持する必要がないように、この部分をチャンクで実行する必要がある場合もあります。(これは非常に簡単に実行できます-「args」リストをXチャンクに分割して呼び出すだけですpool.map(wrapper,chunk) -ここを参照してください


-3

1つの大きなファイルを複数の小さなファイルに分割し、それぞれを別々のスレッドで処理します。


これはOPが望んでいることではありません!! しかし、アイデアのためだけに...悪くはありません。
DRPK 2017年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.