まとめ
並行処理でwriteするときはthreading.Lock
を使おう!(どんな並行処理ライブラリを使っていてもthreadsをimportすればOK!)
経緯
Webスクレイピングで7つぐらいのスレッドから一つのファイルにスクレイピング結果を書き込んでいたとき、完成したスクレイピング結果ファイルをみると文字化けだらけだった!
ここでは、並行処理にfuturesを使っていました。
やっていたコード(悪い例)
悪い例
from concurrent import futures
def scrape(アクセス先):
global f
#(スクレイピング部分)
f.write(result)
f = open("ファイル", "w", encoding="utf-8")
future_list = []
with futures.ThreadPoolExecutor(max_workers=drivers_n) as executor:
#処理を登録
for link in アクセス先リスト:
future = executor.submit(scrape, url=link)
future_list.append(future)
#全ての処理が完了するまで待ち
_ = futures.as_completed(fs=future_list)
f.close()
そしてさっきの写真のような文字化けをおこしました。
複数のスレッドのwriteが同時発生して競合したせいでしょうね。
解決
一つのスレッドがwriteしている間は他のスレッドを待たせる必要があります。排他処理ってやつですね。
その「一つのスレッドがwriteしている間は他のスレッドを待たせる」ロジックを使うにはthreading.Lock
を使えばいいんです。
これはどの並行処理ライブラリ(futuresでもなんでも)にも有効です。
改善したコード(良い例)
良い例
```Python:悪い例
from concurrent import futures
from threading import Lock
def scrape(アクセス先):
#追加!
global f, write_lock
#(スクレイピング部分)
#追加!
with write_lock:
f.write(result)
f = open("ファイル", "w", encoding="utf-8")
future_list = []
#追加!
write_lock = Lock()
with futures.ThreadPoolExecutor(max_workers=drivers_n) as executor:
#処理を登録
for link in アクセス先リスト:
future = executor.submit(scrape, url=link)
future_list.append(future)
#全ての処理が完了するまで待ち
_ = futures.as_completed(fs=future_list)
f.close()
グローバル変数としてLockを一つ用意すれば、排他処理にしたいロジックを
with Lock:
のブロックに入れることで、「一つのスレッドがwriteしている間は他のスレッドを待たせる」処理ができます。