スレッドセーフ

スレッドセーフ: thread-safe)は、マルチスレッドプログラミングにおける概念である。あるプログラムコードがスレッドセーフであるという場合、そのコードを複数のスレッドが同時並行的に実行しても問題が発生しないことを意味する[1]。特に、ある共有データへの複数のスレッドによる読み書きアクセスがあるとき、一度に1つのスレッドのみがその共有データにアクセスするようにして安全性を確保しなければならない。スレッドセーフでないコードを同時並行的に実行すると、データ競合による未定義動作を引き起こしたり、競合状態(レースコンディション)による意図しない動作を引き起こしたりする。場合によっては深刻なセキュリティホール(脆弱性)が引き起こされることもある[2]

概要

スレッドセーフはマルチスレッドプログラミングにおける重要な要素である。それは従来、オペレーティングシステムの開発者だけが考慮しなければならない問題だったが、1990年代後半には一般的な問題となった[注釈 1]。マルチスレッドプログラムでは、複数のスレッドが同じアドレス空間内で同時に実行される。各スレッドのアクセスするメモリ領域が特に制限されることはなく、全スレッドが全アドレス空間にアクセスできる。異なるスレッドが同時に同じメモリ領域に読み取りアクセスする場合は問題にならないが、異なるスレッドが同時に同じメモリ領域に読み書きアクセスする場合(少なくともどちらか一方のスレッドが書き込みアクセスする場合)は競合の問題が発生しうる。また、スレッドがどのような順番で実行され、データアクセスがどのような順序で発生するかは、オペレーティングシステムのスケジューリング仕様や、実行時の計算資源の利用状況などといった外因次第であり、完全に予期・予測することはできず非決定論的な動作をする。従って、静的コード解析だけでスレッドに関するプログラム上の誤りを見つけるのは困難となる。マルチスレッド環境では、そのような理論上起こりうる状況を考慮し、調停のための排他制御をするなど、慎重なプログラミングが求められる。

データ競合のようなスレッドに関する誤りを含むプログラムは、前述のように異常動作や誤動作を引き起こしうる。スレッドセーフとは、そのような意図しない動作を発生させないことを保証するための、コード実行の安全性に関わる指標である。

しかし、ある操作をスレッドセーフにするためには相応の時間的・空間的オーバーヘッドを伴うため、プログラム上のありとあらゆる操作をスレッドセーフにしようとすることは現実的ではない。不必要な排他制御はプログラムのパフォーマンス低下を招く[4]。例えば単一のスレッドからしかアクセスされないことが分かりきっているデータ領域へのアクセスや、すべてのスレッドから読み取りアクセスのみされる完全定数データ領域へのアクセスを排他制御しようとするのは完全に無駄である。そのため、実際に複数のスレッドによって同時に読み書きアクセスが実行されうるコード領域(危険領域、クリティカルセクション)に関してのみスレッドセーフ化する。

スレッドセーフかどうかの判断基準

あるコードの断片がスレッドセーフかどうかを判断するのは簡単ではない。しかし、以下のような点に注意して調べることで問題が見つかることが多い。

  • グローバル変数(広域変数)などの静的記憶域期間を持つ変数[注釈 2]や、動的メモリ確保されたヒープ領域にアクセスしているかどうか
  • グローバルな制限のあるリソース(ファイル、プロセスなど)を確保・解放しているかどうか
  • 参照やポインタによる間接アクセスをしているかどうか
  • 明確な副作用があるかどうか(例えば、C言語で volatile 変数にアクセスするなど)

スタック上の変数のみを使用し、引数にのみ依存し、同様な特性のサブルーチンしか呼ばないならば、そのサブルーチンはリエントラントであり、スレッドセーフである。このようなサブルーチンは「純関数; pure function」などと呼ばれることもあり、数学の関数によく似ている。

静的コード解析ツールの中には、プログラムの並行性に関するバグをある程度検出してくれるものもあり、スレッドセーフでないコードに対して警告を出す[5]

スレッドセーフの実現手法

スレッドセーフを実現する方法として以下のようなものがある。

リエントラント
リエントラント化することでスレッドセーフを実現できるが、広域変数などを使った状態情報のセーブができない。
相互排他
共有データへのアクセスをシリアライズ(逐次化)することでスレッドセーフを実現する。ただし、複数の共有データにアクセスする際には十分に注意しなければならない(排他制御参照)。複数のスレッドがお互いに異なるリソースをロックし合うと、デッドロックが発生することがある。
スレッドローカルデータ
例えばスレッドの識別子(番号)をキーとして広域変数をスレッドごとに持たせることで、サブルーチンを超えた範囲で変数を保持できるようにする。各変数にアクセスするサブルーチン自体はリエントラントではないが、特定のスレッドだけが特定の広域変数にアクセスすることが保証できれば、スレッドセーフとなる。
アトミック操作
共有データを何らかのアトミック(不可分)な操作でアクセスすることで他のスレッドから同時アクセスされないことを保証する。これは一般に特別な命令を必要とするが、そのようなハードウェア的な支援を必要としない純粋なソフトウェア的な解としてランポートのパン屋のアルゴリズムのように、ライブラリがそのような機能をサポートしている場合がある。アトミック操作は多くの排他機構の基盤となっている。

一般的にはこれらの手法に以下の手法を結合して使用する。

  • 共有データのスレッド固有のコピーを使用し、そのコピーの値で共有データをアトミックにアップデートする。このようにすることでコードの大部分は並行して実行可能となり、必要最小限の部分だけがシリアライズされる。

なお、ファイルのようなシステム共有リソースに関しては、同じプロセス内で動作する他のスレッドだけでなく、別のプロセス内で動作する他のスレッドからもアクセスされる可能性がある。そのため、場合によっては単にスレッドセーフにするだけでは不十分であり、必要に応じてプロセス間で排他制御する(ファイルロックも参照)。

脚注

注釈

  1. ^ POSIXスレッド(Pthreads)が最初に標準化されたのはIEEE Std 1003.1c-1995である[3]。またMicrosoft WindowsではWindows 95およびWindows NTWin32 APIにおいてスレッドが導入された。
  2. ^ JavaC#にはグローバル変数は存在しないが、静的フィールドが該当する。

出典

  1. ^ スレッドセーフとは - 意味をわかりやすく - IT用語辞典 e-Words
  2. ^ CON00-C. 複数のスレッドによる競合状態を避ける
  3. ^ POSIX threads - Mastering C++ Multithreading [Book]
  4. ^ 第3回 マルチスレッドでデータの不整合を防ぐための排他制御 ― マルチスレッド・プログラミングにおける排他制御と同期制御(前編) ―:連載.NETマルチスレッド・プログラミング入門(2/3 ページ) - @IT
  5. ^ アプリケーション開発で質の高いコードレビューを実現するためのポイントとは ? ~ 後編~ - builders.flash☆ - 変化を求めるデベロッパーを応援するウェブマガジン | AWS

関連項目

外部リンク

  • スレッドセーフ情報[リンク切れ] HP-UX 11i v1.5 におけるスレッドセーフなAPIの一覧
  • .NET Framework - スレッドセーフ コンポーネント | MSDN, Internet Archive
  • C と Win32 を使用するマルチスレッド | Microsoft Learn
  • マネージド スレッド処理のベスト プラクティス | Microsoft Learn

以下、英文

  • Article "Thread-safe webapps using Spring" by Steven Devijver
  • Thread-safe design
  • Article "Design for thread safety" by Bill Venners
  • Article "Write thread-safe servlets" by Phillip Bridgham
  • Article "Smart Pointer Thread Safety" by Dejan Jelovic