序文
Pythonは、プログラムの並列化でやや悪名高いです。スレッドの実装やGILなどの技術的な問題は別として、間違った指導ガイダンスが主な問題だと思います
質問。一般的な古典的なPythonマルチスレッドおよびマルチプロセスチュートリアルは、ほとんどが「重い」ものです。また、日常業務で最も役立つものを掘り下げずに、表面を傷つけることがよくあります。
伝統的な例
「Pythonマルチスレッドチュートリアル」を簡単に検索すると、ほとんどすべてのチュートリアルでクラスとキューに関する例が示されています。
Python学习交流Q群:906715085###
import os
import PIL
from multiprocessing import Pool
from PIL import Image
SIZE = (75,75)
SAVE_DIRECTORY = 'thumbs'
def get_image_paths(folder):
return (os.path.join(folder, f)
for f in os.listdir(folder)
if 'jpeg' in f)
def create_thumbnail(filename):
im = Image.open(filename)
im.thumbnail(SIZE, Image.ANTIALIAS)
base, fname = os.path.split(filename)
save_path = os.path.join(base, SAVE_DIRECTORY, fname)
im.save(save_path)
if __name__ == '__main__':
folder = os.path.abspath(
'11_18_2013_R000_IQM_Big_Sur_Mon__e10d1958e7b766c3e840')
os.mkdir(os.path.join(folder, SAVE_DIRECTORY))
images = get_image_paths(folder)
pool = Pool()
pool.map(creat_thumbnail, images)
pool.close()
pool.join()
はぁ、Javaみたいですね。
マルチスレッド/マルチプロセスタスクにプロデューサー/コンシューマーモデルを使用するのが間違っていると言っているわけではありません(実際、このモデルにはその場所があります)。ただ、毎日のスクリプトを処理する
タスクより効率的なモデルを使用できます。
問題は…
まず、定型文のクラスが必要です。
次に、オブジェクトを渡すためのキューが必要です。
さらに、チャネルの両端で対応するメソッドを構築して、その作業を支援する必要もあります(双方向で通信したり、結果を保存したりする場合は、別のキューを導入する必要があります)。
より多くの労働者、より多くの問題
この考え方に従って、ワーカースレッドのスレッドプールが必要になります。以下は、IBMクラシックチュートリアルの例です-マルチスレッドによるWebページの取得
ラインアクセラレーション。
Python学习交流Q群:906715085###
#Example2.py
'''
A more realistic thread pool example
'''
import time
import threading
import Queue
import urllib2
class Consumer(threading.Thread):
def __init__(self, queue):
threading.Thread.__init__(self)
self._queue = queue
def run(self):
while True:
content = self._queue.get()
if isinstance(content, str) and content == 'quit':
break
response = urllib2.urlopen(content)
print 'Bye byes!'
def Producer():
urls = [
'http://www.python.org', 'http://www.yahoo.com'
'http://www.scala.org', 'http://www.google.com'
# etc..
]
queue = Queue.Queue()
worker_threads = build_worker_pool(queue, 4)
start_time = time.time()
# Add the urls to process
for url in urls:
queue.put(url)
# Add the poison pillv
for worker in worker_threads:
queue.put('quit')
for worker in worker_threads:
worker.join()
print 'Done! Time taken: {}'.format(time.time() - start_time)
def build_worker_pool(queue, size):
workers = []
for _ in range(size):
worker = Consumer(queue)
worker.start()
workers.append(worker)
return workers
if __name__ == '__main__':
Producer()
このコードは正しく機能しますが、実行する必要があることを詳しく見てください。さまざまなメソッドを作成し、一連のスレッドをトレースして、厄介なデッドロックの問題を解決します。
一連の結合操作を実行する必要があります。これは始まりにすぎない...
これまで、古典的なマルチスレッドチュートリアルを確認しましたが、これはやや中空ですよね。ボイラープレートとエラーが発生しやすい、少ないリソースでより多くのことを行うこのスタイルは、明らかに日常の使用にはあまり適していません。
幸いなことに、私たちにはもっと良い方法があります。
地図を試してみませんか
小さくて繊細な関数マップは、Pythonプログラムを簡単に並列化するための鍵です。マップは、Lispのような関数型プログラミング言語から派生しています。それはシーケンスによって達成することができます
2つの関数間のマッピング。
urls = ['http://www.yahoo.com'、'http://www.reddit.com']
結果=map(urllib2.urlopen、urls)
上記の2行のコードは、シーケンスurlの各要素をパラメーターとしてurlopenメソッドに渡し、すべての結果を結果リストに保存します。その結び目
結果はおおよそ次のようになります。
results = []
for url in urls:
results.append(urllib2.urlopen(url))
map関数は、シーケンス操作、パラメーターの受け渡し、結果の保存などの一連の操作を処理します。
何でこれが大切ですか?これは、適切なライブラリを使用すると、マップで操作を簡単に並列化できるためです。
Pythonには、map関数を含む2つのライブラリがあります。multiprocessingとそのあまり知られていないサブライブラリmultiprocessing.dummyです。
ここにさらに2つの文があります:multiprocessing.dummy?mltiprocessingライブラリのスレッドクローン?これはエビですか?マルチプロセッシングライブラリの公式ドキュメントでも
このサブライブラリに関連する説明は1つだけです。そして、人間の言葉に翻訳されたこの説明は、基本的に次のことを意味します。
!
ダミーはマルチプロセッシングモジュールの完全なクローンです。唯一の違いは、マルチプロセッシングがプロセスで機能するのに対し、ダミーモジュールはスレッドで機能することです(したがって、
Pythonの一般的なマルチスレッドの制限がすべて含まれています)。
したがって、これら2つのライブラリを同じように使用するのは驚くほど簡単です。IO集約型タスクとCPU集約型タスク用に異なるライブラリを選択できます。
やってみよう
次の2行のコードを使用して、並列化されたマップ関数を含むライブラリを参照します。
from multiprocessing import Pool
from multiprocessing.dummy import Pool as ThreadPool
プールオブジェクトをインスタンス化します。
pool = ThreadPool()
この単純なステートメントは、example2.pyのbuildworkerpool関数の7行のコードの作業を置き換えます。一連のワーカースレッドを生成し、初期化を完了します
、簡単にアクセスできるように変数に保存します。
Poolオブジェクトにはいくつかのパラメーターがありますが、ここで注意する必要があるのは、最初のパラメーターであるプロセスだけです。このパラメーターは、スレッドプール内のスレッド数を設定するために使用されます。デフォルト値は
現在のマシンCPUのコア数。
一般に、CPUを集中的に使用するタスクを実行する場合、呼び出されるコアの数が多いほど、高速になります。しかし、ネットワークを多用するタスクを処理する場合、実際には、物事は少し予測不可能になる可能性があります
スレッドプールのサイズを決定するために実験することをお勧めします。
pool = ThreadPool(4) # Sets the pool size to 4
スレッド数が多すぎると、スレッドの切り替えにかかる時間が実際の作業時間を超える場合があります。さまざまなジョブの場合、スレッドプールサイズの最適値を見つけるのは簡単ではありません。
間違った考え。
Poolオブジェクトが作成されると、並列化されたプログラムを実行する準備が整います。書き直されたexample2.pyを見てみましょう
import urllib2
from multiprocessing.dummy import Pool as ThreadPool
urls = [
'http://www.python.org',
'http://www.python.org/about/',
'http://www.onlamp.com/pub/a/python/2003/04/17/metaclasses.html',
'http://www.python.org/doc/',
'http://www.python.org/download/',
'http://www.python.org/getit/',
'http://www.python.org/community/',
'https://wiki.python.org/moin/',
'http://planet.python.org/',
'https://wiki.python.org/moin/LocalUserGroups',
'http://www.python.org/psf/',
'http://docs.python.org/devguide/',
'http://www.python.org/community/awards/'
# etc..
]
# Make the Pool of workers
pool = ThreadPool(4)
# Open the urls in their own threads
# and return the results
results = pool.map(urllib2.urlopen, urls)
#close the pool and wait for the work to finish
pool.close()
pool.join()
実際に機能するコードは4行だけで、そのうち1行だけが重要です。map関数は、上記の40行以上の例を簡単に置き換えることができます。もっと楽しくするには、
さまざまなメソッドとさまざまなスレッドプールサイズの消費時間をカウントしました。
# results = []
# for url in urls:
# result = urllib2.urlopen(url)
# results.append(result)
# # ------- VERSUS ------- #
# # ------- 4 Pool ------- #
# pool = ThreadPool(4)
# results = pool.map(urllib2.urlopen, urls)
# # ------- 8 Pool ------- #
# pool = ThreadPool(8)
# results = pool.map(urllib2.urlopen, urls)
# # ------- 13 Pool ------- #
# pool = ThreadPool(13)
# results = pool.map(urllib2.urlopen, urls)
结果:
# Single thread: 14.4 Seconds
# 4 Pool: 3.1 Seconds
# 8 Pool: 1.4 Seconds
# 13 Pool: 1.3 Seconds
素晴らしい結果ですね。この結果は、スレッドプールのサイズが実験的に決定される理由も説明しています。私のマシンでは、スレッドプールのサイズが9より大きい場合、利点は次のとおりです。
非常に限られています。
別の実際の例
何千もの画像のサムネイルを生成する
これはCPUを集中的に使用するタスクであり、並列化に適しています。
基本的なシングルプロセスバージョン
Python学习交流Q群:906715085####
import os
import PIL
from multiprocessing import Pool
from PIL import Image
SIZE = (75,75)
SAVE_DIRECTORY = 'thumbs'
def get_image_paths(folder):
return (os.path.join(folder, f)
for f in os.listdir(folder)
if 'jpeg' in f)
def create_thumbnail(filename):
im = Image.open(filename)
im.thumbnail(SIZE, Image.ANTIALIAS)
base, fname = os.path.split(filename)
save_path = os.path.join(base, SAVE_DIRECTORY, fname)
im.save(save_path)
if __name__ == '__main__':
folder = os.path.abspath(
'11_18_2013_R000_IQM_Big_Sur_Mon__e10d1958e7b766c3e840')
os.mkdir(os.path.join(folder, SAVE_DIRECTORY))
images = get_image_paths(folder)
for image in images:
create_thumbnail(Image)
上記のコードの主な仕事は、受信フォルダー内の画像ファイルをトラバースし、サムネイルを1つずつ生成し、これらのサムネイルを特定のフォルダーに保存することです。
私のマシンでは、このプログラムで6000枚の画像を処理するのに27.9秒かかりました。
forループの代わりにmap関数を使用する場合:
import os
import PIL
from multiprocessing import Pool
from PIL import Image
SIZE = (75,75)
SAVE_DIRECTORY = 'thumbs'
def get_image_paths(folder):
return (os.path.join(folder, f)
for f in os.listdir(folder)
if 'jpeg' in f)
def create_thumbnail(filename):
im = Image.open(filename)
im.thumbnail(SIZE, Image.ANTIALIAS)
base, fname = os.path.split(filename)
save_path = os.path.join(base, SAVE_DIRECTORY, fname)
im.save(save_path)
if __name__ == '__main__':
folder = os.path.abspath(
'11_18_2013_R000_IQM_Big_Sur_Mon__e10d1958e7b766c3e840')
os.mkdir(os.path.join(folder, SAVE_DIRECTORY))
images = get_image_paths(folder)
pool = Pool()
pool.map(creat_thumbnail, images)
pool.close()
pool.join()
5.6秒!
変更されたコードは数行だけですが、プログラムの実行速度が大幅に向上しました。実稼働環境では、CPUを集中的に使用するタスクとIOを集中的に使用するタスクを分離できます
マルチプロセッシングおよびマルチスレッドライブラリを選択して、実行速度をさらに向上させます。これは、デッドロックの問題を解決するための優れた方法でもあります。また、マップ機能は手動スレッド管理に対応していないため、その逆
また、関連するデバッグ作業が非常に簡単になります。
この時点で、Pythonの1行で(基本的に)並列化を実現しました。