C言語

pthread_cancel(3)を使うのは止めておけ

はじめに

マルチスレッドのプログラミングにおいて、スレッドの動作を途中でキャンセルさせたい場合があります。
そんなとき、pthread_cancel の使用を検討した経験があるのではないでしょうか。

インターネットで検索してみると、サンプルもたくさんあり「よし、使ってみよう!」と考えて
本記事にたどり着いた方も多いかと思います。

 

安易に pthread_cancel に手を出すと、いばらの道が待ってる可能性が高いです

 

なぜそのように考えるのか、順番に説明いたします。
この記事を読み終わるころには、pthread_cancel を使いたくなくなることでしょう(笑)

 

スレッドのキャンセル

関数の仕様

pthread_cancel は、引数の thread で指定したスレッドへキャンセル要求を発行します。

 

動作仕様

pthread_cancel は、指定されたスレッドに対して「キャンセル要求」を発行するだけです。
対象スレッドの終了を待つことはしません。

 

キャンセルを要求されたスレッドの動作は、キャンセル状態と種類によって決まります。
それぞれ、設定するための API がライブラリに実装されています。

  • キャンセル状態:pthread_setcancelstate(3)
    有効(PTHREAD_CANCEL_ENABLE)または
    無効(PTHREAD_CANCEL_DISABLE)を設定できます。
    デフォルトは「有効」です。
  • キャンセルの種類:pthread_setcanceltype(3)
    遅延(PTHREAD_CANCEL_DEFERRED)または
    非同期(PTHREAD_CANCEL_ASYNCHRONOUS)を設定できます。
    デフォルトは「遅延」です。

 

注意が必要なキャンセルの種類

キャンセル状態の設定は「有効」と「無効」となっており、直感的に動作を理解できると思いますが、
種類の「遅延」と「非同期」について、少し詳しく解説いたします。

  • 遅延キャンセル:PTHREAD_CANCEL_DEFERRED
    対象のスレッドが、後述する「キャンセルポイント」の関数を呼び出すまで、
    スレッドの処理を継続してキャンセルを保留します。
  • 非同期キャンセル:PTHREAD_CANCEL_ASYNCHRONOUS
    対象のスレッドは、任意の時点でキャンセル可能です。

 

実は「非同期キャンセル」がロジカルなプログラミングにとってなかなかの曲者になっています。
ここで、man page に記載のある説明を見てみましょう。

Asynchronous cancelability means that the thread can be canceled at any time (usually immediately, but the system does not guarantee this).

注意ポイント

和訳すると、「すぐにキャンセルされるとは限らない。」です。

SANACHAN
SANACHAN
曖昧な仕様のため、大半は遅延キャンセルを使うことになります。

 

キャンセルポイント

キャンセル状態、種類が PTHREAD_CANCEL_ENABLEPTHREAD_CANCEL_DEFERRED の場合、
要求されたスレッドが実際にキャンセルされるのは、次にキャンセルポイントに到達した場合です。

キャンセルポイントとは、pthread が「キャンセルポイント」と定義した関数の呼び出しを指します。
pthread(7) にその関数がまとめられていますので、参照してみてください。

 

いかがでしょうか?
キャンセルポイントに指定されている関数が意外と多いことに気づきましたでしょうか。
さらに、POSIX のバージョンによってもキャンセルポイントが異なると記されています。

SANACHAN
SANACHAN
そうです。キャンセルポイントを正確に把握することが困難なのです。

注意ポイント

キャンセルポイントとなる関数が多く、システムによっても異なる

 

なぜ pthread_cancel を使わない方が良いのか

ようやく本題です。
ここまでにいくつか「注意ポイント」を書きましたが、ここで深堀していきます。

 

非同期キャンセルの罠

非同期キャンセルをセットすると、任意の時点(マシン語レベル)でキャンセル要求を受け付けます。
man page の記載より、システムとして保証していないとしても、この点を考慮する必要があります。

 

例えば、malloc の呼び出し前、malloc 内部の mutex 獲得中、malloc 内部の mutex 解放後、
どの時点でもキャンセルされる可能性があります。

ところが、キャンセルされた場合に実行されるクリーンナップハンドラでは、
どの時点でキャンセルされたのか判断できず、どの処理から再開すべきかも正確には判断できません

 

先の例の malloc 実行中にキャンセルされるかもしれず、この場合は他のスレッドも巻き込んで
不正な状態に陥ります。そのため、非同期キャンセルを使用するスレッドは、
動的メモリ確保/解放や mutex などのロック資源を使用することはできません。

ポイント

malloc やロック資源が使えないことから、ほとんどの標準Cライブラリ関数を使えません。
すなわち、算術演算だけを行うようなスレッド以外に非同期キャンセルを行えないのです。

 

キャンセルポイントを考慮する難しさ

前述のとおり、pthread_cancel を使う場合は「遅延キャンセル」を選択することになるでしょう。
次に問題となるのが「キャンセルポイント」です。

pthread(7) で紹介した通り、キャンセルポイントとなる関数を覚えたり、
一覧を参照しながらプログラムを書くことは現実的ではありません。

また、

  1. 関数Aが関数Bを呼び出す
  2. 関数Bが関数Cを呼び出す
  3. 関数Cがキャンセルポイントとなる関数を呼び出す

のように、内部実装を全て把握してプログラミングを行うことは容易ではありません。

SANACHAN
SANACHAN
標準Cライブラリ関数を含めた内部実装を把握するのはさらに困難でしょう。
C++言語の std ネームスペースを含めると更にです。

 

全貌把握が困難なキャンセルポイントを意識して、どの時点でキャンセルされても
正しく後処理(資源の開放など)を行えるようにする必要があります。

ポイント

キャンセル安全(cancel-safe)なプログラミングは非常に難しい、ということです。

 

正常な後処理との競合

資源(メモリやロック)を使用するスレッドでは、何か異常を検知した場合、
自発的にスレッドを終了、または資源の開放を行うのが一般的です。

これらの準正常処理とキャンセル処理が競合した場合も、当然考えておく必要があります。

 

例えば、close システムコールはキャンセルポイントになっています。
以下の3つのタイミングでキャンセル要求を受け付けた場合、スレッドが終了した後に
ファイルディスクリプタの状態を正確に答えられるでしょうか?

SANACHAN
SANACHAN
誤って2重クローズなどをしようものなら、とんでもない不具合を生むことになります。
  1. close を呼び出す前
    一番正常な処理となりますが、fd はきちんと閉じられるのでしょうか?
  2. close を呼び出し中
    アプリが close を呼び出してからカーネルに処理が移り、
    どの時点でキャンセル処理が行われるのでしょうか?
  3. close を呼び出した後
    次のキャンセルポイントまで遅延されます。
    次のキャンセルポイントを正確に見つけ出すことができますか?

 

このようなことを使用している「キャンセルポイント」と定義されている関数全てにおいて
検討・検証していく必要があり、非常にめんどうで不具合を埋め込む温床となります。

そのため、冒頭で「いばらの道」と表現したわけです。

SANACHAN
SANACHAN
タイミングが関係するため、普通の試験や検証では除去できない不具合を作りこんでしまいます。

 

どのように回避すればよいのか

ほとんどの場合は遅延キャンセルを使用することになるため、
キャンセルを要求してから実際にスレッドが終了するまで、少しの間は動作継続しても許容されます。

 

以下のような考え方の元、実装することができれば pthread_cancel を使用しなくても
スレッドの処理の途中でキャンセルを実現できます。

  • pipe を使って、スレッドに終了を要求
  • スレッド内で使用するリソースは、スレッドを生成する側で確保/開放

 

サンプルプログラム

異常時の処理がかなり適当ですが、上記の考え方で実装すると以下のようになります。

readwrite のシステムコールもいい加減な実装となっていますので、
詳しくは「read 関数の使い方」と「write 関数の使い方」を参照ください。

 

こちらの記事もよく読まれています

  • この記事を書いた人
  • 最新記事
SANACHAN

SANACHAN

「生涯一エンジニア」を掲げ、大手グローバル企業でSE/PGとして8年勤め、キャリアアップ転職した現役のエンジニアです。世にあるメジャーな全プログラム言語(コボル除く)を自由に扱えます。一児の父。自分のため、家族のため、日々勉強してます。システムエンジニア、プログラミングに関する情報を蓄積している雑記帳です。

-C言語
-, ,