2012-01-15

C++11ですみやかにプログラムの実行を終了する方法

結論:C++11で新しく追加されたstd::quick_exitを使え。

プログラムの終了は、すみやかに行われるべきである。なにしろ、終了なのだ。終了にもたついていてはストレスがたまる。とくに、多くの実行環境では、プログラムの外部から、プログラムを強制終了させる方法がある。強制終了は大抵、プログラムの意志を無視して、強制的に一瞬で行われる。外部からできるのであれば、内部からできてしかるべきである。

なぜプログラムは終了時にもたつくのか。それは、終了時に特別な処理を必要とする場合もあろう。たとえば、数GBものデータを遅いHDDに書きださねばならない場合もあるだろう。これは妥当な理由である。では、確保したメモリやその他のリソースの解放処理はどうか。これは、疑問である。というのも、多くの近代的なOSでは、プログラムは個々に独立している。プログラムには独自の仮想メモリ空間が与えられ、必要に応じて物理メモリが割り当てられ、その他のリソースも独自に確保される。他のプログラムとは区別される。もし他のプログラムとリソースを共有したい場合は、なにか特別な方法を使わなければならない。そして、あるプログラムが確保したリソースは、そのプログラムの終了時に、OSが自動的に解放するようになっている。

プログラムの実行中に、必要のないメモリその他のリソースを解放せずに、延々と確保し続けるのは、これをリソースリークと呼ぶ。しかし、終了時には、どうせOSが解放してくれるのだから、自分でやるのは無駄である。

ここで未熟なプログラマー読者は思うであろう。解放するのがプログラムにしろ、OSにしろ、どちらかがやらなければならないのだから、どちらがやっても同じではないか。もし、プログラムによる解放が、OSに比べて極端に遅いとすれば、それは欠陥ではないのかと。問題は、ひとつのリソースを解放するというほど単純ではない。

例えばメモリだ。近代的なプラットフォームでは、仮想メモリ空間に割り当てる物理メモリはページ単位で確保する。これはたいてい、数KB単位である。しかし、現実のプログラムでは、もっと少ない単位(数十バイト)のメモリを大量に必要とする。これに対し、数KB単位のメモリを割り当てていたのでは、無駄である。そこで、現実には、まず一括してある程度の量のメモリを確保しておき、メモリ上に管理のためのデータ構造を構築し、そのメモリを必要とする小さな単位に切り分けて使う手法がとられる。これをヒープと呼ぶ。プログラムの実行中は、このヒープ内のメモリの確保と解放の処理は意味がある。しかし、プログラムが終了するのであれば、そんな処理は必要ない。ある仮想メモリ空間に割り当てられている物理メモリをすべて解放し、その仮想メモリ空間も破棄すればよい。これはすぐ終わる。ところで、近代的なプログラムであれば、マルチスレッドを使うのは当然である。ヒープに対する操作は、スレッドセーフではない。よって、ヒープの操作には排他的なロックをかける必要がある。プログラムの終了時に、すべてのスレッドが確保していたヒープ上のメモリを一斉に解放しようとしても、ヒープに対する操作は排他的であるので、いかに多数のCPUを搭載していようと、パフォーマンスは全くスケールしない。ましてや、ヒープ上のデータ構造の状態は、ヒープごとすべて破棄する場合には、全く関係ないのだ。結果として、遅くなる。

近代的なOSでは、ファイルやネットワークソケットなどといったその他のリソースには、リソースに対するハンドルが与えられる。リソースへの操作は、このハンドルを介して行われる。実行時に動的な数のリソースを確保するプログラムは、動的な数のハンドルを格納できるデータ構造を使う。一方、OS側でも、ハンドルと実リソースと紐付けるために、何らかの動的なデータ構造で情報を保持して置かなければならない。プログラムが明示的にすべてのリソースを解放する場合、プログラム側のデータ構造を巡り巡って、すべてのハンドルに解放処理を行う。ハンドルに対する解放処理を要求されたOSは、これまたOS側のデータ構造からハンドルに該当する実リソースを探し出し、解放処理を行う。しかし、最初から全てを解放するのだと分かっているならば、プログラム側の処理は二度手間である。

そう、ただちに終了するのであれば、プログラム側は何もしないほうがいいのだ。ところが、近代的なプログラミング言語では、「何もしない」というのは難しい。たとえば、C++にはクラスという機能がある。クラスにはコンストラクターとデストラクターがあり、これは、クラスの構築時、破棄時に、自動的に実行される。

C++においてプログラムを終了させる一般的な方法とは、main関数からのreturnである。しかし、main関数からreturnするためには、ネストして呼び出しているすべての関数からreturnしなければならない。

class X
{
    X() { /* リソースの確保 */ }
    ~X() { /* リソースの解放 */ }
} ;

void f1()
{
    X x ;
    // さあ、終了しよう
    return ;
}

void f2()
{
    X x ;
    f1() ;
    return ;
}

int main()
{
    X x ;
    f2() ;
    return ;
}

この例で、関数f1の中身を実行中に、プログラムを終了させたい場合、ネストされた関数を延々とreturnしなければならない。そればかりではなく、スコープの離脱に伴い、クラスXのローカル変数のオブジェクトxのデストラクターを走らせなければならない。たとえ、デストラクターの処理が、単なるリソースの解放であったとしても、デストラクターの実行を止めることはできない。

C++には、Cから受け継いだstd::exitがある。これを使えば、その場で終了できる。exitを呼び出した後に実行が続くことはない。ところが、このexitにも問題がある。グローバル変数とスレッド変数の破棄が実行されてしまうのだ。

X x1, x2, x3 ;

void deep()
{
    std::exit(0) ; // x1, x2, x3, x4のデストラクターが実行される
}

void f()
{
    thread_local X x4 ;
    deep() ;
}

int main()
{
    f() ;
}

では、終了中かどうかを判定するフラグを作って、すべてのグローバル変数とスレッド変数のデストラクターにチェックさせるべきなのだろうか。それでは、自動的なデストラクターの実行の意味がない。

他にも、C++11には採用されなかったが、他の言語では、ガーベージコレクションなどがある。これも速やかな終了の妨げになる。

多くのプラットフォームでは、プログラム自身の終了を止める独自APIが提供されている。プログラムのすべてのスレッドの実行を問答無用で停止して、終了処理を行うようなAPIだ。特定のプラットフォームだけを考えればいいのならば、そういうAPIを使えばいい。しかし、ポータブルなC++のコードを書く場合、できるだけそのような独自APIには頼りたくない。何故ならば、サポートするプラットフォームの数だけ、別々の実装が必要になるからだ。

C++11では、この状況に鑑み、std::quick_exitを用意した。使い方は、std::exitを全く同じだ。違いは、グローバル変数やスレッド変数の破棄が実行されないこと、Cのファイルストリームのフラッシュが行われないこと、終了前にstd::at_quick_exitで登録した関数が呼ばれることだ。

したがって、近代的なプログラムの終了手順は以下のようになる。

  1. 必要であれば、std::at_quick_exitでプログラム全体の終了処理を行う関数を登録する(プログラムの内部バッファーの書き出しなど)
  2. その場で終わらせるべき処理を速やかに終わらせる
  3. Cファイルストリームを使っていて書き込み結果が反映されて欲しいのであれば、フラッシュする
  4. std::quick_exitを呼び出す

No comments: