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) という良書があります

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

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

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

説明: (2) 自作の関数はスレッドセーフにせよ


マルチスレッドのアプリケーションを書くときは、複数のスレッドで共有する変数はロックしてから参照・更新するようにしましょう。複数スレッドで共有する変数として主なものは、大域変数と関数内のstatic変数が挙げられます。また、short型やint型の変数であってもロックして更新した方が安全な場合が多いです。


※ 詳細は id:yupo5656:20040618 "[C++] マルチスレッドと共有変数" を参照してください。


また、C++を使っている場合は関数の同期化方法に注意です。一般的に言って次はNGです。Mutexを関数内static変数として宣言するのはやめましょう

int incr_counter(void) {
  static Mutex m;  // ここがダメ
  m.Lock();

  static int counter = 0;
  int ret = ++counter;

  m.Unlock();
  return ret;
}

かわりに、

Mutex m;

int incr_counter(void) {
  m.Lock();
  // ...

と、Mutexをグローバル変数に追いやるほうが良い(すこしはマシ)です。


※ 詳細は id:yupo5656:20040713 "[C++] C++でsynchronized methodを書くのは難しい" を参照してください。


続き

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

スレッドセーフな関数を、

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

と定義しましたが、

  • 前者を特に、リエントラント(reentrant)な関数と呼んで区別するケース
  • 逆に、後者を特に"Serializable"とか(MT-Safeでなく単に)"Safe"な関数と呼んで区別するケース

もあります。Solarisのmanだと、後者の方式で区別していますね。マルチスレッドのプログラムから安全に呼べるなら"Safe"と呼び、さらにその関数の処理を複数スレッドで並列に行える(concurrencyがある)なら"MT-Safe"と呼んでいるようです


まぁ、ちょっと細かいので、シビアに実行速度を求められる環境でコードを書いているのでなければ単に「スレッドセーフかどうか」を意識するだけで良いのではないでしょうか。

晩御飯

急に必要になったJIS和文冊子・JISハンドブックの類を買いに八重洲ブックセンター本店へ。昼頃に電話で在庫を聞いたところ、必要な5冊中2冊は取り寄せということだったのだが、閉店間際に店舗に行ったら取り寄せ済みだった。偶然かもしれないが対応◎。日本規格協会Web Storeで買うより手軽感高し。八重洲ブックセンター本店は、理工書の品揃えは(わたしの)ツボを付いているし、沢山買えばお食事券も頂けるしで大好き。


その後、マカーの端くれの私は銀座まで歩いてAppleStore, Ginzaへ。冒頭の写真はAppleStoreのエレベータの中です。綺麗な建物の4Fに鏡餅iMacを自由に使えるコーナーがあり、Webぐるぐるで暇を潰せるようになっている。ウチのMacはやや旧式なので、速いMacに触っているだけでも結構楽しい(信者)。◎。暇つぶしさせてもらったお礼に帰宅してからAirMac Expressをポチっと購入させていただきました(献金)。ウチはベースステーションだけ11gで、繋ぐ側は11b機器しか存在していなくて寂しかったんですが、ようやく11gを生かせそうで嬉しいですね。


食事は、六本木の旬の味 いちか、恵比寿の酒亭 和(なごみ)を考えていたのだけど諸事情あり、相方と合流後そのまま銀座で。店名は失念したけどお料理もお酒も美味しかったのでこれも◎。

C++ライブラリクイックリファレンス, オライリー

八重洲ブックセンターで見かけた本。2004年7月発売となっているので出たばかりですねぇ。

C++ライブラリクイックリファレンス

本書は、C++のすべてのランタイムライブラリを詳しく解説する。ライブラリのヘッダごとに、関数、マクロ、クラスなど、宣言ならびに定義されたエンティティを詳述している。ヘッダとエンティティはアルファベット順に解説しているので、必要な情報をすぐに見つけることができる。毎日のプログラム作業に活用するのに最適な一冊。ISO/IECの標準規格に対応。

私は常々、C++を勉強するなら規格書そのもの、"JIS X 3014:2003 プログラム言語C++" を読むのがてっとり早いと考えているんですが、こういうリファレンスなら手元に一冊置いといても良いかなと思います。


ぱらぱら眺めた感じでは良さそうな本でした。規格書からC++言語そのものの解説を省き、かわりに標準ライブラリの解説を充実させた感じでしょうか。昔、国際規格(ISO)C++ライブラリハンドブック という似たような本がありましたが、残念ながらもう絶版ということで、類似のこういう本を待っていた人もいるんじゃないでしょうか。


とりあえず、プログラミング言語C++ (アスキーアジソンウェスレイシリーズ―Ascii Addison Wesley programming series) を買うくらいなら、規格書+コレかなと個人的には思います。