ねぇうしくんうしくん

今週のまとめ (一週間で自分が見た技術系サイトのログ)が今のところメインです。プログラミング言語、人工知能、セキュリティ 等

🎸 ライブロック 🎸について

この記事は rogy Advent Calendar 2018 - Adventar の 9日目の記事です。

みなさんはデッドロックについてはご存知かと思われます。

// thread A
void doA() {
   lock1.lock(); // (A1)
   lock2.lock(); // (A2)
     // クリティカルセクション
   lock2.unlock();
   lock1.unlock();
}

// thread B
void doB() {
   lock2.lock(); // (B1)
   lock1.lock(); // (B2)
     // クリティカルセクション
   lock1.unlock();
   lock2.unlock();
}

2つのスレッドの処理が A1 → B1 のように進んだ場合、ロック解放待ちのブロックが発生し処理が停止してしまうという現象です。

ところで、デッドロックの親戚の概念としてライブロック(livelock)というものがあります。

A thread often acts in response to the action of another thread. If the other thread's action is also a response to the action of another thread, then livelock may result. As with deadlock, livelocked threads are unable to make further progress. However, the threads are not blocked — they are simply too busy responding to each other to resume work. Starvation and Livelock (The Java™ Tutorials > Essential Classes > Concurrency)

(訳) スレッドは時に別のスレッドの挙動に反応することがあります。一方で、そのスレッドがまた元のスレッドに影響を及ぼしあうとき、ライブロックが起こるかもしれません。デッドロックと同様に、ライブロックしたスレッドは処理を進めることができません。しかし、スレッドは実際にはブロックされていません ――― お互いの処理を再開するために永遠に反応しあうビジー状態に陥っています。

これを説明する比喩として面白い例がよく参照されます。

あなたが狭い道を歩いてるとき、向こうから人がやってきました。あなたはその人を見て右によけようとしますが、相手も同じ側によけられました。今度は左側によけようとしますが、あいてもまた左側によけようとします。結果としてお互いがまるで反復横跳びをしあうように運動し続け、前に進むことはありません。

こういった行動は日常生活でよくあると思いますので、その際はライブロックのことを思い出してください。*1

ライブロックするコードの例についてみてみましょう。

// thread A
void doA() {
  lock1.lock(); // (A1)
  while (lock2.locked()) // (A2)
  {
    lock1.unlock(); // (A3)
    sleep(); // (A4)
    lock1.lock(); // (A5)
  }
  lock2.lock(); // (A6)
  // クリティカルセクション
  lock2.unlock();
  lock1.unlock();
}

// thread B
void doB() {
  lock2.lock(); // (B1)
  while (lock1.locked()) // (B2)
  {
    lock2.unlock(); // (B3)
    sleep(); // (B4)
    lock2.lock(); // (B5)
  }
  lock1.lock(); // (B6)
  // クリティカルセクション
  lock1.unlock();
  lock2.unlock();
}

スレッドAで、A2 ~ A5 でlock2がロックされている場合、lock1 のロックを解除して「もう一方のスレッドに譲る(一旦ロックを解除し(A3)、少し待ってから(A4) 再ロックを行う (A5) )」行為を行っています。しかしながらスレッドBも同じことをしているため、最悪の場合お互い譲り合いをしあう状態が続き、whileループを抜け出せなくなってしまいます。

では、実際にこのようなバグが発生するのでしょうか?オープンソースの並行バグを調べた研究によると、ライブロックの割合は少ないものの Hadoop といった有名なフレームワークにも存在したようです。*2

f:id:z_kro:20181208164744g:plain

ライブロックを起こさない解決方法として以下のものがあげられます。

  • 譲るときの待ち時間をランダムにする
    • 現実世界の例で言っても譲るタイミングはいつかはずれてお互い通れるようになります。先ほどの例でも sleep の待ち時間をランダムにするという修正が考えられます。これは非常にアドホックでナイーブな修正ですが、ネットワークプロトコルの世界では「ランダムな待ち時間で衝突回避をする」といったことはよく行われています。
  • ロックの順番を適切に行う
    • デッドロックの解決策としても挙げられるやつです。lock1 の後に lock2 をする、というルールを定めればそのようなバグが起きることはありません。ルールに完璧に従ってプログラムを書ければ、ですが。
  • ロックを使わない。
    • ロックを使わなければデッドロック・ライブロックが起きることはありません。例えば、アトミック変数、ノンブロッキングデータ構造を使う、イベントドリブン(アクターモデル*3)なプログラムにすることなどが挙げられます。

他にも、ライブロック検出のためのアルゴリズムや手法がいくつかの論文で提唱されています。

*1:これを表現するスラングとしてSchlumperdinkというものがあるらしいです https://www.urbandictionary.com/define.php?term=schlumperdink

*2:Concurrency bugs in open source software: a case study https://jisajournal.springeropen.com/articles/10.1186/s13174-017-0055-2

*3:これでも暗黙のデッドロックみたいなものが生じるんですが、これについてはここでは述べません