📖

Python thread.Condition のマニュアル解説

2022/10/10に公開約4,000字

「わからないことがあればマニュアルを読めばいい」と先輩に言われたが、そもそもマニュアルに書いてある意味がわからない、、、なんて経験ないでしょうか。先輩にありがたい教えを頂いたにも関わらず、何年経っても成長しないのか、私はいまだにマニュアルを読むのに苦労しています。自分の理解も含めて解説文を書いてみることにしました。

なお、作者や和訳されてる方に説明が足りないというようなことを言うつもりはなく、いわゆるマニュアル的な説明を補うことが目的であり、作者様および訳者様には大変感謝しております。間違い等ありましたら遠慮なくコメントお願いします。

なお、このマニュアルを読むには、マルチスレッドについてと、ロック処理について理解している必要があります。

thread.Condition とは

マニュアルの解説を始める前に、thread.Condition が何かという概要です。thread と書いてあるとおり、スレッドを作成したときに使うツールになります。仮に2つスレッドを作ったとして、お互いに同じファイルを更新しあって中身をぐちゃぐちゃにすることが無いよう同期する必要がありますが、そういったときに使えます。特にお互いの処理が依存していて、依存もとのスレッドが依存先のスレッドに「もう処理していいよ」というときに使います。原文には producer/consumer を典型的な例として書かれていますが、もうちょっと具体的な例を挙げると consumer がキューを持っていて、producer がそのキューにタスクを突っ込んだあと、consumer に処理を始めるよう wakeup させるようなパターンに使えます。

解説

2022年10月現在の日本語訳になります。https://docs.python.org/ja/3/library/threading.html

thread.Condition とロック

原文:

条件変数 (condition variable) は、常にある種のロックに関連付けられています; このロックは明示的に渡すことも、デフォルトで生成させることもできます。複数の条件変数で同じロックを共有しなければならない場合には、引渡しによる関連付けが便利です。ロックは条件オブジェクトの一部です: それを別々に扱う必要はありません。

解説:
タイトルは原文にはありませんので勝手につけています。私は、この文章の冒頭で戸惑ってしまいました。条件変数ということば・・・概要に書いた機能を提供するものなのですがなんら繋がらない言葉ですね。こういうときは専門用語ということで割り切るしか無いです。thread.Condition = 条件変数という呼び名なのだということにしましょう。ハッシュを連想配列と呼ぶようなものですね。ここでは thread.Condition に書き換えて、もうちょっと情報量を増やした文章に書き換えます。

thread.Condition は、内部にロック変数を持っていて、そのロック変数を利用して動作します。このロック変数は、自分の好きなものを thread.Condition に持たせることもできますが、デフォルトでは thread.Condition オブジェクト作成と同時に自動で作ってくれます(なので引数が lock=None)。

複数の thread.Condition オブジェクトで、それぞれが同じロックを共有しなければならない場合には、デフォルトの自動生成ではなく、別途ロック変数を自分で作成し、thread.Condition オブジェクトに渡してあげましょう。最初に説明ししたとおり、ロック変数は thread.Condition オブジェクトの一部として保持しています。それを切り離して別々に扱う必要はないのです。

プログラムから意味を理解するタイプの人には、文章だとよくわからないと思います。実際の関数は、thread.Condition(lock=None) という書式になっていて、lock は好きなものを渡すことができるし、渡さなければ自動で作るよと言っています。thread.Condition はスリープしてるスレッドを他のスレッドから wake up させるための機能になります。それを実現するためにはロック機構が必要で、そのためのロック変数を内部に持ってますよということです。「常にある種のロックに関連付けられています」は、これを意味します。

使い方の概要

原文:

条件変数は コンテキスト管理プロトコル に従います: with 文を使って囲まれたブロックの間だけ関連付けられたロックを獲得することができます。 acquire() メソッドと release() メソッドは、さらに関連付けられたロックの対応するメソッドを呼び出します。

解説:
このテキストに含まれる「コンテキスト管理プロトコル」は、リンクになっていて同じページの最後にある with の使い方の説明に飛びます。thread.Condition のロックは、acquire() と release() によって同期を行います。with を使うと、ブロックの先頭で acquire() を呼び、最後で release() を呼んでくれます。リンク先は、thread パッケージ内の acquire/release を使うもの(Condition, Semaphor など)はすべて with が使えますよという説明になっています。

wait と notify

以降、原文は acquire/relase を意識して書かれていて複雑なので、with ブロックを使う前提で解説を続けます。

原文:

他のメソッドは、関連付けられたロックを保持した状態で呼び出さなければなりません。 wait() メソッドはロックを解放します。そして別のスレッドが notify() または notify_all() を呼ぶことによってスレッドを起こすまでブロックします。一旦起こされたなら、 wait() は再びロックを得て戻ります。タイムアウトを指定することも可能です。
notify() メソッドは条件変数待ちのスレッドを1つ起こします。 notify_all() メソッドは条件変数待ちの全てのスレッドを起こします。

解説:
先にサンプルコードをどうぞ。これは原文にあるサンプルに少し手を加えたものです。

def consumer(cv):
    with cv:
        while not an_item_is_available():
            cv.wait()
    print("consumer: woke up")
    item = get_an_available_item()
    print(f"consumer: item is {item}")

def producer(cv):
    with cv:
        make_an_item_available()
        cv.notify()

def main():
    cv = threading.Condition()
    consumer1 = threading.Thread(target=consumer, args=(cv,))
    consumer1.start()
    producer(cv)
    consumer1.join()

thread.Condition には、acquire/release 以外のメソッドがいくつかありがますが、これらのメソッドは、with cv: ブロックの中(つまりロックを保持した状態)で使わなければなりません。
その中で基本的なメソッドの一つ、cv.wait() メソッドはロックを開放して待ち状態(スリープ)になります。

待っているスレッドを起こすには notify か notify_all メソッドを使います。これも with ブロック内で使います。起こす役のスレッドは with cv (この cv は wait してるスレッドと同じもの)にるロックでその他すべてのスレッドをブロックした上で cv.notify() または cv.notify_all() で待ち状態のスレッドを起こします。

notify() を呼んだスレッドは、with ブロックを抜けるまでロックを保持し続けます。なので、wait() は notify() が呼ばれてもすぐには戻ってきません。notify() を呼んだ with ブロックを抜けたあと(ロックを開放したあと)、wait() がロックを保持して戻ってきます。wait() は、タイムアウトを指定することもできます。このことは原文に注意が書かれています。

注意: notify() と notify_all() はロックを解放しません; 従って、スレッドが起こされたとき、 wait() の呼び出しは即座に処理を戻すわけではなく、 notify() または notify_all() を呼び出したスレッドが最終的にロックの所有権を放棄したときに初めて処理を返すのです。

notify/notify_all の違いは原文のままで大丈夫かと思います。無理やり補足すると、notify は起こすスレッドの数を渡すことができるので、必ずしも1つだけを起こすということはありません。

長くなったので今日はこのへんで。

Discussion

ログインするとコメントできます