web-dev-qa-db-ja.com

スレッドセーフなスクリプトに関するガイダンスを求める

さまざまなサーバー上のファイルへの複数のパスを取り、それらすべてを同時に検索し、結果の単一のリストをユーザーに返すスクリプトを作成しようとしています。最初は、Pythonスレッドを使用してこれを行っていましたが、すぐにいくつかの問題に遭遇しました:

  1. キックオフできるスレッドの数を制御していませんでした。したがって、誰かが1つのサーバーに対してクエリを実行するために100個のファイルを送信した場合、そのマシンでは100個のスレッドが開始されます。これは悪いニュースでした。

  2. 私が取り戻した結果は不完全で、劇的に変化しました。検索を直線的に(スレッドなしで)実行すると、完全な結果が得られますが、時間がかかります。私はこれといくつかの個人的な調査に基づいて、スレッドセーフなアプローチをとっていないと結論付け、キューモジュールを調べ始めました。

私はこのようなものになってしまいました...

def worker():
   while q.qsize != 0:
      cmd = q.get()
      # kick off a bash command that zgreps files from different servers
      p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, Shell=True)
      results.extend(''.join(p.stdout.readlines()).split('\n')[:-1])
      q.task_done()

NUM_WORKER_THREADS = 10
results = []

q = Queue.Queue()
for i in range(NUM_WORKER_THREADS):
   t = threading.Thread(target=worker)
   t.daemon = True
   t.start()

""" Code to generate input commands needed here """

for c in commands:
   q.put(c)

q.join()

""" Post processing of collected *results* array"""

プログラムの周りにいくつかのスレッドプール制約を設定し、キューにまだ何かがあるかどうかを各スレッドでチェックした後、私の結果は私が期待するものと一致しています。テスト後、結果はシングルスレッドアプローチの出力と一致します(ただし、はるかに高速です)。

私の質問は次のとおりです。

  1. 私のアプローチはスレッドセーフですか? 10個のワーカースレッドのいずれかが結果配列を拡張しようとする別のワーカースレッドを上書きする可能性はありますか?入力を処理するためにより小さいスレッドプールを割り当てることにより、上書きが発生する可能性を減らしたばかりであると心配していますが、実際には問題を解決していません。

  2. キューをスレッドセーフにすることになっていることを読んで理解しました。ただし、スレッドプールを削除し、スレッドのキューサイズを確認しないと、大量の入力で以前と同じ問題を再現できます。誰かがそれがなぜであるか説明できますか?

4
Elias51

1a。私のアプローチはスレッドセーフですか?

結果はスレッドセーフではありませんが、多くのスレッドによって読み書きされているためです。これをキューに変えることも検討してください。

1b。 10個のワーカースレッドの1つが別のスレッドを上書きして結果配列を拡張しようとする可能性はありますか?

はい、それはまさに起こり得ることです。そのため、スレッド化するときにそのような方法で配列を使用することは避けてください。

2。キューをスレッドセーフにすることになっていることを読んで理解しました。ただし、スレッドプールを削除し、スレッドのキューサイズを確認しないと、大量の入力で以前と同じ問題を再現できます。誰かがそれがなぜであるか説明できますか?

あなたは本当にあなたの仕事をするためにプリベイク Threading Pool を使うべきです。それはあなたの人生をずっと単純にするはずです。呼び出す関数と、呼び出す必要のあるすべての変数を含むリストを受け取り、作業のリストをスレッド間で分散します。唯一の落とし穴は、呼び出している関数に渡すことができるパラメーターは1つだけですが、使用しているのは1つだけなので、コードをかなり簡単に適応させることができます。

例えば:

from multiprocessing import Pool

def do_work(cmd):
  # kick off a bash command that zgreps files from different server
  # Not sure if this can be done better. Not clear what command you're running
  p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, Shell=True)
  return ''.join(p.stdout.readlines()).split('\n')[:-1]

NUM_WORKER_THREADS = 10

p = Pool(NUM_WORKER_THREADS)
results = p.map(do_work, commands)

補足として、必要なすべての引数をタプルにパックし、メソッドの開始時にそれらをアンパックするだけで、常に1つの引数の制限を回避できます。タプルはPythonでは安価です。

たとえば、関数do_work次のように2つのコマンドを取りました:

def do_work(cmd_one, cmd_two):
  p = subprocess.Popen(cmd_one, stdout=subprocess.PIPE, stderr=subprocess.PIPE, Shell=True)
  p2 = subprocess.Popen(cmd_two, stdout=subprocess.PIPE, stderr=subprocess.PIPE, Shell=True)
  return ''.join(p.stdout.readlines()) + ''.join(p2.stdout.readlines())

次のように書き直すことができます。

def do_work(commands):
  cmd_one, cmd_two= commands
  p = subprocess.Popen(cmd_one, stdout=subprocess.PIPE, stderr=subprocess.PIPE, Shell=True)
  p2 = subprocess.Popen(cmd_two, stdout=subprocess.PIPE, stderr=subprocess.PIPE, Shell=True)
  return ''.join(p.stdout.readlines()) + ''.join(p2.stdout.readlines())

そして、メインメソッドを次のように変更します。

from multiprocessing import Pool

NUM_WORKER_THREADS = 10

p = Pool(NUM_WORKER_THREADS)
results = p.map(do_work, [(x, y) for x in command1_list for y in command2_list])

このようにして、両方の変数を呼び出し用のタプルにパッケージ化し、必要なときにすぐにアンパックします。

3
Ampt