UNIX上でのC++ソフトウェア設計の定石 (4)

鉄則4: スレッドの「非同期キャンセル」を行わない設計にしよう

  • スレッドの非同期キャンセルとは: あるスレッドが別のスレッドを即座に強制終了すること
  • 単に「設計が楽だから」「シンプルになるから」という理由でスレッドの非同期キャンセルを使うのはやめよう

一見楽そうに、シンプルそうに見えるだけ。様々な問題を引き起こす可能性が。問題の詳細を把握しないまま、スレッドの非同期キャンセルを行う設計にしないこと!

pthread規格では、あるスレッドの処理を別のスレッドが強制的に中断することが許可されています。これを、スレッドのキャンセルと呼びます。


スレッドのキャンセルには次の二種類があります。

  • 方式1: 非同期キャンセル(PTHREAD_CANCEL_ASYNCHRONOUS)
    • キャンセルは即座に行われる
  • 方式2: 遅延キャンセル(PTHREAD_CANCEL_DEFERRED)
    • キャンセルは、スレッドの処理が「キャンセルポイント」に達するまで遅延される

また、どちらのキャンセル方式を利用するかは、キャンセルするがわではなく、キャンセルされる側が決定することができます*1。また、キャンセルされる側は、キャンセルを完全に禁止することもできます*2

何が起きるか

さて、今回はスレッドの「非同期キャンセル」の乱用が何を引き起こすかを見ていきましょう。定石(3)を見ていただいた方はわかるかもしれませんが、次のシナリオで、被キャンセルスレッド以外の任意のスレッドがデッドロックします。

  1. スレッド1がmalloc関数の処理を実行中に、スレッド2がスレッド1を非同期キャンセルしたとする
  2. スレッド1は即座にキャンセルされ、malloc関数のmutexをアンロックするスレッドがいなくなる
  3. 次に任意のスレッドがmalloc関数を呼ぶと、そのスレッドがデッドロックする

ここでは例によってmalloc関数を例に出しましたが、他にも危険な関数は沢山あります。


逆に、非同期キャンセルを行っても問題のない関数も少数ですが存在し、それらは「async-cancel safeな関数」とか「非同期キャンセルセーフな関数」と呼ばれます。一部の商用UNIX*3では、OSの提供する関数について、ドキュメントにasync-cancel safetyの記載がありますが、Linux(glibc)には残念ながら殆ど記載がありません。


ここで、規格(SUSv3)を参照してみましょう。すると、規格で非同期キャンセルセーフであると述べられているのは次の3つの関数のみだということがわかります。

  1. pthread_cancel
  2. pthread_setcancelstate
  3. pthread_setcanceltype

そして、"No other functions are required to be async-cancel-safe" と明記されています。ですから、Linuxの場合などでドキュメントにasync-cancel safetyについて記載のない関数は、安全ではないと仮定すれば良いでしょう!

災いをどう回避するか

マルチスレッドのプログラムでの非同期キャンセルを安全に行うための、デッドロック問題回避の方法はあるでしょうか?いくつか考えてみます。これは、鉄則(3)のスレッド+forkの場合の回避策に似ています。


回避方法1: 被キャンセルスレッドでは、非同期キャンセルセーフな関数のみを使用する


まず、被キャンセルスレッドでは、非同期キャンセルセーフな関数のみを使用するという方法があります。しかしこの方法は

  • 規格で非同期キャンセルセーフとされている関数は3つしかない
  • それ以外の関数は、非同期キャンセルセーフでないか(商用UNIX)、ドキュメントがないのでわからない(Linux)

の2点によりあまり現実的ではありません。


回避方法2: 被キャンセルスレッドで非同期キャンセルセーフでない処理を行っている最中は、キャンセル方式を「遅延」あるいは「禁止」に再設定しておく


2つめは、被キャンセルスレッドで非同期キャンセルセーフでない処理を行っている最中は、キャンセル方式を「遅延」あるいは「禁止」に再設定しておくという方法です。この方法には

  • 方法1で書いたとおり、どの関数が非同期キャンセルセーフであるか把握するだけでも一苦労
  • 任意の場所での即時のキャンセル動作が保証されなくなる
    • 例えば「遅延」に再設定した間にキャンセルが起こったとき、あるブロッキングI/O関数のブロックが解除されるかは微妙
    • キャンセル禁止中のキャンセル要求はペンディングされる

という問題があり、結局「キャンセル方式の切り替えに気を使うくらいなら、最初から遅延キャンセルのみ使用するほうがまだマシ」という事になってしまいがちです。あまりよい回避方法ではないでしょう。


回避方法3: pthread_cleanup_push関数を用いて、非同期キャンセルが発生した時のクリーンナップ用コールバック関数を登録する


3つめは、pthread_cleanup_push関数を用いて、非同期キャンセルが発生した時のクリーンナップ用コールバック関数を登録するという方法です。鉄則(3)で紹介したpthread_atfork関数に類似の関数です。この関数で登録したコールバック関数で、データやロックの掃除を行うことでデッドロックを回避するという寸法です。


...しかし、pthread_cleanup_push関数で登録したコールバック関数は、「遅延キャンセル」を行った場合にしか呼ばれません。したがって、この回避方法3は非同期キャンセルでは役に立ちません。


回避方法4: 非同期キャンセルは一切使用しない


最後は、非同期キャンセルは一切使用しないという方法です。非同期キャンセルを行わない代わりに、

  • スレッドのキャンセルに依存しない設計にする

あるいは

  • キャンセルの使用がやむを得ないのなら、非同期キャンセルではなく遅延キャンセルだけを使用する

ようにします。これは、現実的な方法であり、推奨できます。

以上.

*1:pthread_setcanceltype関数

*2:pthread_setcancelstate関数

*3:SolarisHP-UXなど