リエントラント関数とスレッドセーフ設計の基礎

組み込みシステムでは、割り込みやRTOS(Real-Time Operating System:リアルタイムOS)によるマルチタスク環境が広く利用されています。同じ関数が複数のタスクや割り込みハンドラ(ISR:Interrupt Service Routine)から呼び出されることもあるため、「リエントラント」と「スレッドセーフ」を正しく理解した設計が欠かせません。しかし、両者は混同されやすく、違いを理解しないまま設計・実装すると、不具合の原因になります。

この記事では、それぞれの定義や違いを整理し、組み込み開発で実践すべき設計のポイントを解説します。

リエントラントとは何か

リエントラントは、割り込みやRTOS環境で安全に動作するソフトウェアを設計する上で基本となる性質です。ここでは、定義や実装方法、リエントラント性を損ねる原因を解説します。

リエントラントの定義

リエントラント(Reentrant)とは、関数の実行中に同じ関数が再度呼び出されても、安全かつ正しく動作する性質です。「再入可能」とも呼ばれます。

たとえば、タスクの実行中に割り込みが発生し、同じ関数が割り込みハンドラから呼び出されても、各呼び出しの状態が衝突せず正しく処理されることが求められます。そのための基本は、関数内部に状態を持たないことです。状態が必要な場合は、呼び出し側がコンテキストを用意して引数で渡します。

リエントラント性を損ねる原因

リエントラント性を損ねる主な原因は、複数の呼び出しで同じ状態を共有することです。

代表例として、グローバル変数や静的変数への依存、共有バッファの書き換え、I/Oデバイスやハードウェアレジスタへの同時アクセスがあげられます。また、関数内部で状態を保持する設計も競合の原因となるため、呼び出し間で状態を保持しない設計が重要です。

リエントラント関数の実装例

リエントラント関数は、状態を関数内部に持たないことが基本です。

作業用データにはローカル変数を使用し、必要な情報は引数で受け取り、結果は戻り値や出力引数で返します。状態が必要な場合は、コンテキスト構造体を呼び出し側で管理し、呼び出しごとに独立したコンテキストを渡しましょう。これにより、複数の実行コンテキストがある場合でも安全に利用できます。

スレッドセーフとの違い

リエントラントとスレッドセーフの違いを示す図。リエントラントは、実行中に同じ関数が再度呼び出されても安全に動作することを指す。実行中のタスクが呼んだ関数Aに割り込みから再入しても、独立したローカル変数と引数だけを使うため問題ない。状態を持たず、呼び出しごとに独立して実行できることがポイントである。スレッドセーフは、複数スレッド(タスク)から同時に呼び出されても安全に動作することを指す。タスク1とタスク2が呼ぶ関数Bは、排他制御(ロック等)で共有データの整合性を保つ。共有データへのアクセスを同期機構で保護することがポイントである。

リエントラントとスレッドセーフは混同しがちですが、対象となる実行環境や安全性を実現する方法が異なります。相違点を理解することで、適切な同期機構やAPI設計を選択できるようになります。

スレッドセーフの定義

スレッドセーフとは、複数のスレッドが同じデータにアクセスしても、データの整合性を保ちながら正しく動作する性質です。

一般的には、ミューテックスやセマフォ、アトミック操作などの同期機構によって実現されます。RTOS環境では、共有資源を扱うAPIに不可欠な性質です。

  • スレッドセーフは主に「タスク(スレッド)間」を想定しています。ISRからは使えない同期機構(ミューテックスなど)があります。

リエントラントとの違い

リエントラントは「実行中に再度呼び出されても安全(各呼び出しの状態が衝突せず、データが破壊されない)」、スレッドセーフは「複数スレッドから同じデータにアクセスしても安全(状態やデータの整合性が保たれる)」であることを意味します。

リエントラントは、複数の呼び出し間で状態を共有しない設計で実現するのが基本です。一方でスレッドセーフは、排他制御で実現されることもあります。そのため、両者は一致する場合もあれば、一方だけを満たす場合もあります。

組み込み開発で混同しやすい例

printfやmalloc/freeなどの標準ライブラリ関数は、ライブラリの実装によってリエントラントであるか、スレッドセーフであるかが異なります。組み込みのCライブラリの実装によっては、リエントラント化のためにスレッドごとの再入保護構造体や、動的メモリ確保(malloc)時の排他制御フック実装が必要になることがあります。採用するツールチェーンやRTOSの設定とライブラリ仕様を確認してください。また、ドライバAPIが静的変数や共有レジスタを利用している場合は、複数タスクや割り込みハンドラからの同時利用で競合が発生することがあります。

そのため、「スレッドセーフだから割り込みハンドラでも安全」「リエントラントだから排他制御は不要」と早合点せず、ライブラリやドライバの仕様を確認して利用することが重要です。たとえば、ミューテックスでスレッドセーフ化された関数は、多くのRTOSでは割り込みハンドラからミューテックス取得API自体が使用できず、ロック取得待ちにもなり得るため不適切です(デッドロックや割り込み遅延の原因になります)。

組み込み開発で注意すべき設計ポイント

関数自体がリエントラントであっても、呼び出し側で共有資源を適切に管理・排他制御しなければ、システム全体の安全性は確保できません。ここでは、割り込みハンドラから呼び出す関数や排他制御、共有資源の扱い方を解説します。

割り込みハンドラから呼び出す関数

割り込み発生時は実行中のタスクが中断されて同じ関数が再入される可能性があります。リエントラント性の確保は、割り込みハンドラから呼び出す関数を設計する上で重要な条件です。

また、割り込みハンドラではミューテックスの取得や待ち時間を伴うブロッキング処理は使用できません。同期が必要な場合は、RTOSが提供する割り込みハンドラ専用APIを利用することが重要です。

RTOS環境での排他制御の選択と使い分け

RTOSでは用途に応じて同期機構を使い分けます。ミューテックスはタスク間の排他制御に使用します。

短時間の共有資源保護にはクリティカルセクション(割り込み禁止などによって排他的に実行される区間)やアトミック操作を利用し、処理時間を最小限に抑えることが重要です。

共有資源を安全に扱う方法

共有資源への競合を減らすには、リングバッファやメッセージキュー、ダブルバッファを活用します。これらを利用することで、タスクと割り込みハンドラ間でのデータ受け渡しの安全性が高まります。

また、設計段階でデータの所有権を明確にし、共有そのものを最小限にすることも重要です。

リエントラント性を高める設計とレビュー

リエントラント性は、実装後ではなく設計段階から意識することが重要です。API設計や状態管理を工夫し、レビューやテストで再入時の安全性を確認しましょう。

API設計の工夫

状態を持たない関数を基本とし、必要な情報はコンテキスト構造体を介して受け渡します。複数のタスクや割り込みハンドラから同じ関数を呼び出す場合でも、状態を分離しやすくなります。

グローバル変数を減らす方法

グローバル変数への依存を減らし、タスクごと(スレッドごと)に独立したデータを保持することで、共有データによる競合を防げます。

コードレビューでの確認ポイント

レビューでは、静的変数やグローバル変数の使用、排他制御の漏れ、割り込みハンドラから呼び出される可能性を確認します。さらに、割り込みハンドラや複数タスクから同一関数を同時に呼び出すテストを実施し、再入時の動作を検証することが重要です。

よくある誤解とトラブルの事例

リエントラント性については、「排他制御すれば安全」「スレッドセーフなら割り込みハンドラからも利用できる」といった誤解が少なくありません。最後に、代表的な誤解と確認ポイントを紹介します。

排他制御だけではリエントラントとはならない理由

排他制御は共有資源を保護する仕組みであり、リエントラント性を保証するものではありません。

たとえば、ミューテックスを保持したまま割り込みが発生すると、割り込みハンドラで同じ関数が再入した際にデッドロックや応答遅延を招くことがあります。リエントラント性と排他制御は、それぞれ別の観点で設計する必要があります。

リエントラント性を考慮しない場合の問題

リエントラント性を考慮していないと、ログ出力関数や通信ドライバ、メモリ管理機能、デバイスドライバなどでデータ破壊や予期しない動作が発生することがあります。

設計時のチェックリスト

関数を設計する際は、割り込みハンドラから呼び出される可能性、複数タスクからの同時利用、静的変数・グローバル変数・外部リソースへの依存、排他制御の方法を確認しましょう。

これらを設計・レビュー段階で確認することで、リエントラント性とスレッドセーフ性を両立した、信頼性の高い組み込みソフトウェアを実現できます。

組み込みソフトの世界 トップへ戻る