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

apple

鉄則6: マルチスレッドプログラミングの「常識」を守ろう

  1. POSIXの標準関数のうち、非スレッドセーフであるものの一覧を把握し、使わないようにせよ
  2. 自作の関数はスレッドセーフにせよ
    • 共有変数はロックして参照・更新せよ
    • C++を使っているなら、関数を同期化する方法に注意せよ

説明: (1) POSIXの標準関数のうち、非スレッドセーフであるものの一覧を把握し、使わないようにせよ


もしPOSIXプラットフォームでマルチスレッドのプログラミングを行うなら、いくらかの最低限の知識、つまり「常識」を知り、厳守する気持ちで望みましょう。


...まずは、「スレッドセーフ」の意味を理解しましょう。スレッドセーフな関数とは、「複数のスレッドが同時に呼び出しても問題ない関数のこと」です。こういう関数は次のどちらかの性質を満たしています。

  1. 局所的静的変数(関数内のstatic変数)や非局所的静的変数(大域変数)の操作をしない。かつ、他の非スレッドセーフな関数を呼んでいない
  2. そういう変数の操作をするが、その部分をmutexなどで同期化し、複数のスレッドが同時には操作しないように制限している

さて、POSIX標準の関数の中には、上記の条件を満たさなくて良いとされている関数があります。歴史的な事情で関数のシグネチャが酷いものになっており、どうやっても条件を満たせない関数があるからなんですね。例えば localtime関数 を見てみましょう。シグネチャは次の通りです:

struct tm *localtime(const time_t *timer);

localtime関数は、現在時刻を整数(1970/1/1からの経過秒数)で与えると、その時刻をtm構造体という年月日などが入ったわかりやすい形で戻してくれる関数です。規格によると、戻ってきたtm構造体はfree()する必要がありませんし、してはいけません。この関数の典型的な実装は次のようになります:

struct tm *localtime(const time_t *timer) {
  static struct tm t;
  
  /* ... timer引数から年月日などを算出 ... */

  t.tm_year = XXX;
  /* ...構造体を埋めていく... */
  t.tm_hour = XXX;
  t.tm_min  = XXX;
  t.tm_sec  = XXX;

  return &t;
}

この関数が次のように使われると処理が破綻します:

  1. スレッドAが ta = localtime(x); 呼び出し
  2. スレッドBが tb = localtime(y); 呼び出し
  3. スレッドAが ta構造体を使用 → おかしな値が入っている!

...長々と説明するようなことでも無かったですね。この破綻は、localtime関数内でmutexを使ったとしても回避できません。シグネチャがダメなんです。


というわけで、既に述べたとおりPOSIX規格(SUSv3)は「スレッドセーフでなくとも良い」関数をきちんと定義しています。"§2.9.1 Thread-Safety" のところに載っている関数一覧がソレです。

asctime, basename, catgets, crypt, ctime, dbm_clearerr, dbm_close, dbm_delete, dbm_error, dbm_fetch, dbm_firstkey, dbm_nextkey, dbm_open, dbm_store, dirname, dlerror, drand48, ecvt, encrypt, endgrent, endpwent, endutxent, fcvt, ftw, gcvt, getc_unlocked, getchar_unlocked, getdate, getenv, getgrent, getgrgid, getgrnam,
(略)

規格で非スレッドセーフとされている関数の使用は避けるようルール化し、またそのような関数が使われていないか自動でチェックできるように環境を整えておくと良いでしょう。


逆に、ここに掲載されていないPOSIX標準関数は "shall be thread-safe" とされていますので、(使用プラットフォームのマニュアルに特に非スレッドセーフと記載がなければ)マルチスレッド環境で問題なく使用できると期待されます。


さらに、いくつかの非スレッドセーフ関数には、関数のシグネチャを変更してスレッドセーフ化した代用関数が用意されています。そのような関数には _r というサフィックスが付いており、区別できるようになっています*1。例えば、asctime関数であればスレッドセーフ版はasctime_r関数といった調子です。代用関数が規格で定義されているかどうかは、さきほどのページで関数名をクリックしてみると良いです。rand関数をクリックすると、

[TSF] int rand_r(unsigned *seed);

のように、[TSF] とマークされた関数が併記されているでしょう。これが代用関数です。一覧は規格に載っていないようですが(追記: やや嘘でした。こちらを参照)、私の知る限り次の代用関数があります。

asctime_r, ctime_r, getgrgid_r, getgrnam_r, getpwnam_r, getpwuid_r, gmtime_r, localtime_r, rand_r, readdir_r, strerror_r, strtok_r

また、規格外ですが大抵 次の関数は用意されていることが多いようです。

gethostbyname_r, gethostbyname2_r

最近のOSであれば、getaddrinfoというIPv6対応の名前解決のAPIも使えます。gethostbyname系のAPIは古臭いので、こちらを使うほうがベターでしょう*2。SUSv3によるとgetaddrinfoはスレッドセーフです:

The freeaddrinfo() and getaddrinfo() functions shall be thread-safe.

非スレッドセーフ関数を使用しないとともに、これらの代用関数を積極的に使用していきましょう。


続き

*1:C言語にゃ関数のオーバーロードはありません

*2:ネットワーク関係のAPIの古い/新しいについては、IPv6ネットワークプログラミング (network technology series) という良書があります