ku-nlp / pyknp

A Python Module for JUMAN++/KNP
Other
89 stars 23 forks source link

複数スレッド間で `Juman` インスタンスを共有し analysis を実行すると確率的に処理が止まってしまう #46

Closed euglena1215 closed 3 years ago

euglena1215 commented 3 years ago

複数スレッド間で Juman インスタンスを共有し analysis を実行すると確率的に処理が止まってしまう事象を観測したので報告します。 私はマルチスレッドで web サーバを起動していたときにこの事象に遭遇しました。

再現コード

from concurrent.futures import ThreadPoolExecutor
from pyknp import Juman

def main():
    jumanpp = Juman()

    # これは動く
    # for _ in range(10):
    #     print([m.midasi for m in jumanpp.analysis("こんにちは!").mrph_list()])

    # 途中で止まる
    with ThreadPoolExecutor(max_workers=10) as executor:
        futures = []
        for _ in range(10):
            futures.append(
                executor.submit(jumanpp.analysis, "こんにちは!")
            )

        for f in futures:
            print([m.midasi for m in f.result()])

if __name__ == "__main__":
    main()
foo@bar:/home# python reproduce.py
['こんにちは', '!']
['こんにちは', '!']
['こんにちは', '!'] ← ここで出力が止まってしまう

^C^CTraceback (most recent call last): ← Ctrl+C で中断しないとずっと何も返さない
  File "reproduce.py", line 60, in main
    print([m.midasi for m in f.result()])
  File "/usr/local/lib/python3.7/concurrent/futures/_base.py", line 430, in result
    self._condition.wait(timeout)
  File "/usr/local/lib/python3.7/threading.py", line 296, in wait
    waiter.acquire()
KeyboardInterrupt

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "reproduce.py", line 64, in <module>
    main()
  File "reproduce.py", line 60, in main
    print([m.midasi for m in f.result()])
  File "/usr/local/lib/python3.7/concurrent/futures/_base.py", line 623, in __exit__
    self.shutdown(wait=True)
  File "/usr/local/lib/python3.7/concurrent/futures/thread.py", line 216, in shutdown
    t.join()
  File "/usr/local/lib/python3.7/threading.py", line 1044, in join
    self._wait_for_tstate_lock()
  File "/usr/local/lib/python3.7/threading.py", line 1060, in _wait_for_tstate_lock
    elif lock.acquire(block, timeout):
^CKeyboardInterrupt
^CError in atexit._run_exitfuncs:
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/concurrent/futures/thread.py", line 40, in _python_exit
    t.join()
  File "/usr/local/lib/python3.7/threading.py", line 1044, in join
    self._wait_for_tstate_lock()
  File "/usr/local/lib/python3.7/threading.py", line 1060, in _wait_for_tstate_lock
    elif lock.acquire(block, timeout):
KeyboardInterrupt

考えられる原因

内部で実行している jumanpp コマンドの標準入力、標準出力の取り扱いで競合が発生し dead lock のような状態になっている。

考えられる解決策

  1. 利用者が Juman インスタンスをスレッド間で共有せず、スレッドごとで Juman インスタンスを利用する
  2. Juman#analysis を thread safe な実装に変更する 2-1. 別スレッドからの参照時は subprocess を作り直す実装に変更する 2-2. そもそも jumanpp を使い回さず Juman#analysis の実行毎に jumanpp コマンドを実行する related https://github.com/ku-nlp/pyknp/pull/28
euglena1215 commented 3 years ago
  1. 利用者が Juman インスタンスをスレッド間で共有せず、スレッドごとで Juman インスタンスを利用する

私はこの方法を使ってこの問題を回避しました。

参考実装

from pyknp import Juman

class JumanThreadSafe:
    def __init__(self, option='') -> None:
        self.juman_by_thread = {}
        self.option = option

    def analysis(self, text, **kwargs):
        now_thread_id = threading.get_ident()
        if now_thread_id not in self.juman_by_thread:
            self.juman_by_thread[now_thread_id] = Juman(option=self.option)

        return self.juman_by_thread[now_thread_id].analysis(text, **kwargs)
nobu-g commented 3 years ago

28 の方法をオプションで部分的に適用するように変更しました.

サブスレッドから解析が実行される可能性がある場合,KNP あるいは Juman インスタンス作成の際に,multithreading=True を指定してください.