web-dev-qa-db-ja.com

Python threading.Condition()notify()がロックを必要とするのはなぜですか?

私の質問は、不必要なパフォーマンスの影響により、なぜそれがそのように設計されたのかについて具体的に言及しています。

スレッドT1にこのコードがある場合:

_cv.acquire()
cv.wait()
cv.release()
_

スレッドT2には次のコードがあります。

_cv.acquire()
cv.notify()  # requires that lock be held
cv.release()
_

何が起こるかというと、T1がロックを待って解放し、次にT2がそれを獲得し、T [1]を起こすcvに通知します。現在、wait()から戻った後、T2のリリースとT1の再取得の間に競合状態があります。 T1が最初に再取得しようとすると、T2のrelease()が完了するまで、不必要に再一時停止されます。

注:明示的な呼び出しとの競合をわかりやすく示すために、意図的にwithステートメントを使用していません。

これは設計上の欠陥のようです。これについて知られている根拠はありますか、それとも何か不足していますか?

26
Yam Marcovic

これは決定的な答えではありませんが、この問題について私が収集した適切な詳細をカバーすることになっています。

まず、Pythonの スレッドの実装はJavaに基づいています です。 JavaのCondition.signal()ドキュメントには次のように書かれています。

実装では、このメソッドが呼び出されたときに、現在のスレッドがこのConditionに関連付けられたロックを保持する必要がある場合があります(通常は必要です)。

さて、問題はなぜPython)でこの動作をenforceするのかです。しかし、最初にプロと各アプローチの短所。

多くの場合、ロックを保持する方がよいと考える理由について、私は2つの主要な議論を見つけました。

  1. その分から、ウェイターがacquire()でロックを解除します。つまり、wait()でロックを解放する前に、シグナルが通知されることが保証されます。対応するrelease()がシグナリングの前に発生した場合、これによりシーケンス(ここでP = ProducerおよびC = ConsumerP: release(); C: acquire(); P: notify(); C: wait()この場合、同じフローのwait()に対応するacquire()は信号を見逃します。これが問題ではない場合もあります(より正確であると見なすこともできます)が、それが望ましくない場合もあります。これは1つの議論です。

  2. ロックの外でnotify()を実行すると、スケジューリング優先順位が逆転する可能性があります。つまり、優先度の低いスレッドが優先度の高いスレッドよりも優先される可能性があります。 1つのプロデューサーと2つのコンシューマー(LC =低優先度コンシューマーおよびHC =高優先度)を持つワークキューを考えてみます。 consumer)、ここで[〜#〜] lc [〜#〜]は現在作業項目を実行しており、[〜#〜] hc [〜#〜]wait()でブロックされています。

次のシーケンスが発生する可能性があります。

P                    LC                    HC
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                     execute(item)                   (in wait())
lock()                                  
wq.Push(item)
release()
                     acquire()
                     item = wq.pop()
                     release();
notify()
                                                     (wake-up)
                                                     while (wq.empty())
                                                       wait();

notify()release()の前に発生した場合、[〜#〜] lc [〜#〜]は発生しません[〜#〜] hc [〜#〜]が起こされる前にacquire()を実行できました。ここで優先順位の逆転が起こりました。これは2番目の引数です。

ロックの外側に通知することを支持する主張は、スレッドがスリープ状態に戻って、次に取得するタイムスライスを再びウェイクアップするためだけにスリープ状態に戻る必要がない場合です。私の質問。

Pythonのthreadingモジュール

Pythonでは、私が言ったように、通知している間はロックを保持する必要があります。皮肉なことに、内部実装では、基礎となるOSが優先度の逆転を回避できないため、ウェイターにFIFO順序が強制されます。もちろん、ウェイターの順序が確定的であるという事実は、便利になりますが、ロックと条件変数を区別する方がより正確であると主張できるときに最適化された同時実行性と最小限のブロッキングを必要とする一部のフローでは、そのようなことを強制する理由は疑問ですacquire()自体が先行する待機状態を登録するのではなく、wait()呼び出し自体を登録する必要があります。

間違いなく、Pythonプログラマーはとにかくこの点ではパフォーマンスに関心がないでしょう。ただし、標準ライブラリを実装するときに、いくつかの標準動作を許可しないでください。可能。

言わなければならないことの1つは、threadingモジュールの開発者が何らかの理由で特にFIFO注文を望んでいる可能性があり、これが何らかの方法で最良の方法であることが判明したことですそれを達成し、他の(おそらくより一般的な)アプローチを犠牲にしてConditionとしてそれを確立したかったのです。このため、彼らは自分で説明するまで疑いの恩恵を受けるに値します。

5
Yam Marcovic

説得力のある理由がいくつかあります(まとめると)。

1.通知機能はロックを取得する必要があります

Condition.notifyUnlocked()が存在するふりをします。

標準的な生産者/消費者の取り決めでは、両側でロックを取得する必要があります。

_def unlocked(qu,cv):  # qu is a thread-safe queue
  qu.Push(make_stuff())
  cv.notifyUnlocked()
def consume(qu,cv):
  with cv:
    while True:       # vs. other consumers or spurious wakeups
      if qu: break
      cv.wait()
    x=qu.pop()
  use_stuff(x)
_

Push()notifyUnlocked()の両方が_if qu:_とwait()の間に介入できるため、これは失敗します。

のどちらかを書く

_def lockedNotify(qu,cv):
  qu.Push(make_stuff())
  with cv: cv.notify()
def lockedPush(qu,cv):
  x=make_stuff()      # don't hold the lock here
  with cv: qu.Push(x)
  cv.notifyUnlocked()
_

動作します(これは興味深い演習です)。 2番目の形式には、quがスレッドセーフであるという要件が削除されるという利点がありますが、notify()の呼び出しを回避するためにロックする必要はありません。上手

特に (ご存じのとおり) CPythonは通知されたスレッドをウェイクアップして待機状態に切り替えさせるため、設定を説明する必要があります。ミューテックス上(単に 待機キューに移動する ではありません)。

2.条件変数自体にロックが必要です

Conditionには、同時待機/通知の場合に保護する必要がある内部データがあります。 ( [CPythonの実装 をちらりと見ると、2つの非同期のnotify() sが誤って同じ待機中のスレッドをターゲットにし、スループットの低下やデッドロックを引き起こす可能性がある)可能性があります。もちろん、専用ロック付きのデータ。すでにユーザーに見えるロックが必要なので、それを使用すると追加の同期コストを回避できます。

3.複数のスリープ状態がロックを必要とする場合がある

(下にリンクされているブログ投稿のコメントから転載。)

_def setSignal(box,cv):
  signal=False
  with cv:
    if not box.val:
      box.val=True
      signal=True
  if signal: cv.notifyUnlocked()
def waitFor(box,v,cv):
  v=bool(v)   # to use ==
  while True:
    with cv:
      if box.val==v: break
      cv.wait()
_

_box.val_がFalseで、スレッド#1がwaitFor(box,True,cv)で待機しているとします。スレッド#2はsetSignalを呼び出します。 cvが解放されても、#1はその状態でまだブロックされています。次に、スレッド#3がwaitFor(box,False,cv)を呼び出し、_box.val_がTrueであることを確認して待機します。次に、#2がnotify()を呼び出し、#3を起動します。これはまだ満足できず、再びブロックされます。現在、#1と#3はどちらか一方が条件を満たしている必要があるにもかかわらず、どちらも待機しています。

_def setTrue(box,cv):
  with cv:
    if not box.val:
      box.val=True
      cv.notify()
_

状況が発生しないようになりました:#3が更新前に到着して待機しないか、更新中または更新後に到着してまだ待機していないため、通知はwaitForから戻る#1に送信されます。

4.ハードウェアにはロックが必要な場合があります

待機モーフィングを使用し、GILを使用しない場合(Pythonのいくつかの代替または将来の実装)、メモリの順序付け(cf。Javaのルール )はロックによって課されます- notify()後のリリースとwait()からの戻り時のロック取得が、通知スレッドの更新が待機スレッドに可視であることの唯一の保証になる場合があります。

5.リアルタイムシステムはそれを必要とするかもしれません

POSIXテキストの直後 引用したもの 我々 find

ただし、予測可能なスケジューリング動作が必要な場合、そのミューテックスはpthread_cond_broadcast()またはpthread_cond_signal()を呼び出すスレッドによってロックされます。

1つのブログ投稿 には、この推奨事項の理論的根拠と歴史(および、ここにある他の問題のいくつか)のさらなる議論が含まれています。

2
Davis Herring

数ヶ月前、まったく同じ質問が私に起こりました。しかし、私はipythonを開いていたので、threading.Condition.wait??結果(メソッドの ソース )は、自分で答えるのに時間がかかりませんでした。

要するに、waitメソッドは、ウェイターと呼ばれる別のロックを作成し、それを取得し、リストに追加してから、驚いたことに、それ自体のロックを解放します。その後、再びウェイターを取得します。つまり、誰かがウェイターを解放するまで待機を開始します。次に、自身のロックを再度取得して戻ります。

notifyメソッドは、ウェイターリストからウェイターをポップし(覚えているように、ウェイターはロックです)、対応するwaitメソッドの続行を許可します。

つまり、waitメソッドがウェイターを解放するのを待つ間、notifyメソッドは条件自体にロックを保持していません。

UPD1:質問を誤解しているようです。 T2がロックを解放する前に、T1が自身のロックを再取得しようとする可能性があることを気にかけているのは正しいことですか?

しかし、PythonのGILのコンテキストでは可能ですか?または、条件を解放する前にIO呼び出しを挿入できると考えます。これにより、T1がウェイクアップして永久に待機できるようになりますか?

0
newtover

何が起こるかというと、T1がロックを待って解放し、次にT2がそれを取得し、T1をウェイクアップするcvに通知します。

結構です。 cv.notify()呼び出しはwake T1スレッドを実行しません。それは別のキューに移動するだけです。 notify()の前は、T1は条件が真になるのを待っていました。 notify()の後、T1はロックの取得を待機しています。 T2はcv.release()を明示的に呼び出すまで、T2はロックを解放せず、T1は「ウェイクアップ」しません。

0
Solomon Slow