z/OSマルチスレッドPL/IにおけるSTATIC変数の落とし穴 ~THREAD属性と競合回避、そしてアベンドの深淵~
基幹システムの世界に身を置く者ならば、PL/Iという言語の持つ「底知れなさ」に、一度は触れたことがあるはずだ。特にz/OS環境で、しかもマルチスレッドという、ある種の「禁断の領域」に踏み込むとなると、その複雑さは指数関数的に増していく。今回は、PL/Iプログラムにおける `STATIC` 変数と `THREAD` 属性、そしてそれらがマルチスレッド環境で引き起こす潜在的な問題、さらにはアベンド(ABEND)発生時の解析といった、現場のテックリードやマイグレーション担当者が避けては通れないテーマに、深く切り込んでいきたい。
JavaやC#へのマイグレーションを検討する際、多くのアーキテクトは、PL/Iの `STATIC` 変数をグローバル変数のように捉え、その挙動を現代的な言語のグローバル変数やクラス変数に単純にマッピングしようとする。しかし、z/OSのPL/Iコンパイラ、特にマルチスレッド環境を意識した設計においては、その「静的な」変数が、我々が想像する以上にデリケートな存在になり得るのだ。
1. PL/Iプログラムの基本構造とSTATIC変数の「静けさ」
まず、PL/Iプログラムの基本的な構造を再確認しておこう。
/ プログラムの開始点 /
MYPROG: PROCEDURE OPTIONS(MAIN);
/ 宣言部 /
DECLARE
MY_STATIC_VAR FIXED BIN(31) STATIC INITIAL(0), / 静的変数 /
MY_DYNAMIC_PTR POINTER; / ポインタ変数 /
/ 処理部 /
MY_STATIC_VAR = MY_STATIC_VAR + 1;
PUT SKIP LIST(‘MY_STATIC_VAR:’, MY_STATIC_VAR);
/ 動的メモリ確保とポインタ操作の例 /
CALL ALLOCATE_MEMORY;
RETURN;
/ サブルーチン /
ALLOCATE_MEMORY: PROCEDURE;
DECLARE
ALLOCATED_AREA AREA,
ALLOC_SIZE FIXED BIN(31) INITIAL(100);
ALLOCATE ALLOCATED_AREA; / 動的メモリ領域の確保 /
MY_DYNAMIC_PTR = ADDR(ALLOCATED_AREA); / 確保した領域のアドレスをポインタに格納 /
PUT SKIP LIST(‘Allocated memory at:’, MY_DYNAMIC_PTR);
/ ここで確保したメモリ領域にアクセスする処理… /
/ メモリ解放は通常、プログラム終了時や明示的に行われる /
/ RETURN; /
END ALLOCATE_MEMORY;
END MYPROG;
この例のように、`STATIC` 属性を持つ変数は、プログラムの実行開始時にメモリ領域が確保され、プログラムの終了までその領域が保持される。初期値が指定されていれば、それが代入され、そうでなければゼロ(数値型)、スペース(文字型)などが初期値となる。
Javaで言えば、`static` キーワードを付けたクラス変数に相当する。C#であれば、`static` 修飾子を付けたフィールドだ。一見、どこからでもアクセスできて便利だが、これがマルチスレッド環境になると、その「静けさ」が牙を剥くことになる。
2. マルチスレッド環境とSTATIC変数の「競合」
z/OSでPL/Iプログラムをマルチスレッドで実行する場合、通常は `CICS` 環境や、`BPXPRV`/`BPXPOST` といったUNIX System Services (USS) のAPIを利用してスレッドを生成・管理することが多い。あるいは、PL/Iコンパイラの `THREAD` オプションを利用して、コンパイル時にスレッドセーフなコードを生成させる場合もある。
ここで重要なのが、`THREAD` 属性である。PL/Iコンパイラには、`OPTIONS(THREAD)` というコンパイルオプションが存在する。これを指定すると、コンパイラはプログラム内の `STATIC` 変数や `BASED` 変数、`CONTROLLED` 変数などのメモリ管理を、スレッドごとに独立した領域を割り当てるように調整する。
2.1 THREAD属性の「恩恵」と「落とし穴」
`OPTIONS(THREAD)` を指定してコンパイルされたプログラムは、各スレッドが `STATIC` 変数にアクセスする際に、それぞれ独自のスレッドローカルなコピーにアクセスすることになる。これにより、あるスレッドが `STATIC` 変数を更新しても、他のスレッドには影響しない。これは、まさにJavaの `ThreadLocal` のような振る舞いだ。
しかし、ここで油断は禁物だ。`OPTIONS(THREAD)` を指定せずに、複数のスレッドから同じPL/Iプログラム(またはその一部)を呼び出した場合、どうなるか?
問題発生シナリオ:
1. スレッドA が `MY_STATIC_VAR` にアクセスし、値を `10` に更新する。
2. スレッドB が `MY_STATIC_VAR` にアクセスしようとするが、その直前に スレッドA が更新した値 (`10`) を参照してしまう。
3. スレッドB が `MY_STATIC_VAR` の値を `20` に更新しようとする。
4. スレッドA が再度 `MY_STATIC_VAR` にアクセスすると、意図せず `20` に更新されている(または、さらに別のスレッドの操作が混在している)。
これは、典型的な競合状態(Race Condition)だ。複数のスレッドが共有リソース(この場合は `STATIC` 変数)に同時にアクセスし、その結果がアクセスするタイミングによって変わってしまう現象である。
2.2 「THREAD」属性の指定方法とコンパイラ挙動
`OPTIONS(THREAD)` は、プログラムの最上位 `PROCEDURE` 文に指定する。
/ THREADオプションを指定した例 /
MYPROG: PROCEDURE OPTIONS(MAIN, THREAD);
DECLARE
MY_STATIC_VAR FIXED BIN(31) STATIC INITIAL(0);
/ … 処理 … /
END MYPROG;
このオプションが指定されている場合、コンパイラは `STATIC` 変数に対して、スレッドごとに個別のストレージを確保するようにコードを生成する。これは、コンパイラが内部的に生成するストレージ構造や、各スレッドの実行コンテキストと連携して実現される。
3. 動的メモリ操作とポインタの「危険な関係」
`STATIC` 変数だけでなく、PL/Iのポインタ (`POINTER`) や `BASED` 変数、`CONTROLLED` 変数といった動的メモリ操作も、マルチスレッド環境では注意が必要だ。
3.1 BASED変数とポインタの「共有」問題
`BASED` 変数は、`POINTER` 変数によって指し示されるメモリ領域にアクセスするための宣言である。
MYPROG: PROCEDURE OPTIONS(MAIN, THREAD);
DECLARE
GLOBAL_BUFFER CHAR(100) BASED(GLOBAL_PTR), / グローバルなバッファ /
GLOBAL_PTR POINTER;
DECLARE
THREAD_SPECIFIC_BUFFER CHAR(100) BASED(THREAD_PTR), / スレッド固有のバッファ /
THREAD_PTR POINTER;
/ … 処理 … /
/ GLOBAL_PTR は、どこからか不定のメモリ領域を指している可能性がある /
/ THREAD_PTR は、スレッドローカルな領域を指すように管理する必要がある /
END MYPROG;
`GLOBAL_PTR` のように、複数のスレッドから共有されるポインタ変数が、ある `BASED` 変数(例えば、ヒープ領域に確保されたメモリ)を指している場合、それは競合状態の温床となる。
- スレッドA が `GLOBAL_PTR` を介して `GLOBAL_BUFFER` にデータを書き込む。
- スレッドB が `GLOBAL_PTR` を介して `GLOBAL_BUFFER` を読み込もうとするが、その直前に スレッドA が書き込み途中の、不完全なデータを読んでしまう可能性がある。
- あるいは、スレッドA が `GLOBAL_PTR` が指すメモリ領域を解放(`FREE`)した後、スレッドB がその解放済みのメモリ領域にアクセスしようとして、アベンド(ABEND)が発生する。
3.2 動的メモリ確保 (`ALLOCATE`) と解放 (`FREE`) の注意点
`ALLOCATE` および `FREE` ステートメントは、動的にメモリを確保・解放する。`CONTROLLED` 変数や `BASED` 変数と組み合わせて使われることが多い。
/ 例: CONTROLLED変数の使用 /
MYPROG: PROCEDURE OPTIONS(MAIN, THREAD);
DECLARE
MY_CONTROLLED_VAR CHAR(100) CONTROLLED,
MY_PTR POINTER;
/ … /
ALLOCATE MY_CONTROLLED_VAR; / メモリ確保 /
MY_PTR = ADDR(MY_CONTROLLED_VAR); / 確保した領域のアドレスを取得 /
MY_CONTROLLED_VAR = ‘Data from Thread A’;
/ … 他のスレッドからのアクセス … /
FREE MY_CONTROLLED_VAR; / メモリ解放 /
END MYPROG;
`OPTIONS(THREAD)` が指定されていても、`ALLOCATE` で確保されたメモリ領域自体は、デフォルトではスレッド間で共有される。もし、スレッドごとに独立したメモリ領域を確保したい場合は、`ALLOCATE` の呼び出しをスレッドのコンテキスト内で行い、そのポインタをスレッドローカルに管理する必要がある。
重要なポイント:
- 共有ポインタ: 複数のスレッドで共有されるポインタ変数は、非常に注意深く管理する必要がある。ポインタが指すメモリ領域が、あるスレッドによって解放されたり、再割り当てされたりする可能性があるためだ。
- スレッドローカルなポインタ: `OPTIONS(THREAD)` が指定されていても、ポインタ変数の宣言自体はスレッドローカルなストレージに配置されるとは限らない。ポインタ変数が指す先のメモリ領域が、スレッドローカルであるか共有であるかを明確に定義し、適切に管理する必要がある。
- NULLポインタチェック: ポインタが有効なメモリ領域を指しているかどうかのチェックは、アベンドを防ぐために極めて重要である。
4. アベンド(ABEND)発生時のダンプ解析 ~闇を照らす光~
マルチスレッド環境での `STATIC` 変数やポインタの誤用は、しばしばアベンドを引き起こす。特に、`0C4` (Segmentation Fault)、`0C7` (Data Exception)、`0A9` (Address Exception) といったアベンドは、メモリ関連の不具合を示唆していることが多い。
ダンプ解析は、これらのアベンドの原因を特定するための生命線だ。z/OSのダンプ(特に `Language Environment (LE)` のダンプ)は、プログラムの実行状態、レジスタ、ストレージの内容、変数、コールスタックなどを詳細に記録している。
4.1 ダンプ解析の「勘所」
1. アベンドコードの特定: まず、どのコードでアベンドが発生したかを確認する。IPLログやSYSMDUMP/SDUMPなどを確認する。
2. プログラムカウンタ (PC) の確認: アベンド発生時のプログラムカウンタ(レジスタ15)は、実行されていた命令のアドレスを示す。このアドレスをソースコードと照合する。
3. レジスタの確認: 各レジスタの内容は、プログラムの実行状態を物語る。特に、ポインタ、ベースレジスタ、インデックスレジスタなどは、メモリ参照の妥当性を判断する上で重要だ。
4. ストレージダンプの確認: アベンド発生時のストレージの内容をダンプで確認する。変数、配列、動的に確保されたメモリ領域などが、意図した値になっているか、あるいは破壊されていないかを確認する。
5. Language Environment (LE) の情報: LEは、PL/Iプログラムの実行環境を提供しており、LEのデバッグ情報(変数名、型、アドレスなど)は、ダンプ解析において非常に強力な武器となる。`CEEDUMP` などで詳細なLEトレースを取得できる。
4.2 STATIC変数とアベンドの関連
`STATIC` 変数自体が直接アベンドを引き起こすことは少ないが、その値が不正なポインタの計算に使われたり、配列のインデックスとして誤って使用されたりすることで、間接的にアベンドを誘発することがある。
例えば、`STATIC` 変数で配列のサイズを管理しており、そのサイズが不正な値(負の値や、確保されているメモリ領域を超える値)になっていた場合、その配列へのアクセスで `0C4` アベンドが発生する可能性がある。
/ 例: 不正な配列アクセスによるアベンド /
MYPROG: PROCEDURE OPTIONS(MAIN, THREAD);
DECLARE
ARRAY_SIZE FIXED BIN(31) STATIC INITIAL(10), / サイズ /
MY_ARRAY CHAR(100) DIMENSION(ARRAY_SIZE); / 配列 /
/ 別のスレッドが ARRAY_SIZE を変更したと仮定 /
/ 例: ARRAY_SIZE = -5; (もし可能なら、これは致命的) /
PUT SKIP LIST(MY_ARRAY(1)); / この時点で、配列サイズが不正だとアベンドの可能性 /
END MYPROG;
このような場合、ダンプのPCとレジスタを確認し、`MY_ARRAY` へのアクセス命令を特定。さらに、その命令で使用されているベースレジスタやインデックスレジスタが参照している `ARRAY_SIZE` の値を確認することで、原因にたどり着ける。
5. 信頼性向上のための「実践的」アプローチ
5.1 コンパイラオプションの「賢い」使い方
- `OPTIONS(THREAD)`: マルチスレッド環境で `STATIC` 変数や共有リソースの競合を避けたい場合に有効。ただし、すべての `STATIC` 変数がスレッドローカルになるわけではない(後述)。
- `OPTIMIZE(FULL)` / `OPTIMIZE(LEVEL(3))`: コンパイラによる最適化は、パフォーマンス向上に不可欠だが、時にコードの可読性を損ない、デバッグを難しくすることがある。移行調査の段階では、最適化レベルを下げて(例: `OPTIMIZE(LEVEL(1))`)、挙動を確認することも有効。
- `SOURCE`/`LIST`: ソースコードとコンパイルリストは、アベンド解析やコードの挙動理解の基本。常に生成し、参照できるようにしておく。
5.2 ベース変数とポインタを用いた動的メモリ操作の「鉄則」
- ポインタの初期化: ポインタ変数は、使用前に必ず初期化(NULLポインタにするか、有効なアドレスを代入する)する。
- NULLポインタチェック: ポインタを経由したアクセス前に、必ずNULLチェックを行う。
IF MY_POINTER ¬= NULL THEN
/ アクセス処理 /
ELSE
/ エラー処理 or 何もしない /
ENDIF;
- メモリ解放の管理: `FREE` したメモリ領域へのアクセスは絶対に避ける。メモリ管理の責任範囲を明確にする。
- BASED変数とスレッド: `BASED` 変数が指すメモリ領域が共有されるのか、スレッドローカルなのかを意識する。共有メモリ領域を `BASED` 変数でアクセスする場合は、排他制御(セマフォなど)を検討する。
5.3 パックデシマル (Packed Decimal) の「潜む」バグ
パックデシマルは、z/OSで一般的に使われる数値データ形式だが、その内部表現(符号位置など)は、コンパイラやライブラリによって微妙に異なることがある。特に、外部システムとのデータ交換や、異なるコンパイラバージョン間での互換性において、符号の反転バグ(例: 正の値なのに負の値として扱われる、あるいはその逆)が発生することがある。
/ 例: パックデシマルと数値演算 /
MYPROG: PROCEDURE OPTIONS(MAIN);
DECLARE
PACKED_NUM PIC'(9)9′ USAGE IS PACKED-DECIMAL, / 例: 123 /
OTHER_NUM FIXED BIN(31) INITIAL(0);
PACKED_NUM = 123;
OTHER_NUM = PACKED_NUM; / ここで、もしPACKED_NUMの内部表現がおかしければ… /
PUT SKIP LIST(‘PACKED_NUM:’, PACKED_NUM, ‘OTHER_NUM:’, OTHER_NUM);
END MYPROG;
この種のバグは、コンパイラの内部処理や、数値変換ルーチンに起因することが多い。ダンプ解析では、パックデシマル変数のメモリダンプを直接確認し、そのビットパターンが期待通りか検証する必要がある。
5.4 埋め込みSQL (DB2) と CICS オンライン処理のエッジケース
- DB2: 埋め込みSQLを使用する場合、カーソル管理、トランザクション境界、ロック競合などがマルチスレッド環境で複雑化する。特に、`STATIC` 変数やポインタがSQLバインド変数として使われる場合、その値の整合性に注意が必要。
- CICS: CICS環境では、タスク(スレッド)管理、共有リソース(ファイル、キュー、DB2コネクションなど)へのアクセス制御が重要になる。`OPTIONS(THREAD)` を使用しても、CICSのタスクレベルでのリソース競合は別途考慮する必要がある。例えば、共有ファイルへの書き込みは、ロック機構(`ENQ`/`DEQ` など)を使って保護する必要がある。
6. マイグレーション設計における「STATIC」変数の処遇
Java/C#へのマイグレーションを検討する際、PL/Iの `STATIC` 変数をどのように扱うかは、設計上の重要なポイントとなる。
- `OPTIONS(THREAD)` の活用: もし、PL/Iプログラムが `OPTIONS(THREAD)` でコンパイルされている、あるいはそれに準ずるスレッドセーフな設計になっている場合、`STATIC` 変数はJavaの `ThreadLocal` や、C#の `ThreadStatic` 属性を持つフィールドにマッピングできる可能性がある。
- 共有リソースとしての管理: `OPTIONS(THREAD)` が指定されていない、あるいは `STATIC` 変数が意図的に共有される設計になっている場合は、Javaの `synchronized` メソッド/ブロック、`Atomic` クラス、あるいは `ConcurrentHashMap` などのスレッドセーフなコレクションクラスにマッピングする必要がある。C#であれば、`lock` ステートメントや `Interlocked` クラスなどが該当する。
- クラス変数としての再設計: 多くの `STATIC` 変数は、論理的に特定のオブジェクト(クラス)に属するべきデータであることが多い。マイグレーションの際には、これらの `STATIC` 変数を、対応するクラスのインスタンス変数(フィールド)として再設計するのが最も自然なアプローチとなる。
- グローバル状態の排除: 可能な限り、プログラム全体で共有されるグローバルな状態(`STATIC` 変数に代表される)を減らす設計を目指す。これにより、コードのテスト容易性、保守性、そしてスレッドセーフ性が向上する。
終わりに ~「静か」ではない、PL/Iの「深淵」~
PL/Iの `STATIC` 変数、特にz/OSのマルチスレッド環境における挙動は、一見単純に見えて、その実、多くの落とし穴を抱えている。`OPTIONS(THREAD)` は強力な味方だが、万能ではない。動的メモリ管理、パックデシマル、DB2やCICSとの連携といった要素が絡み合うと、その複雑さは増すばかりだ。
基幹システムの安定稼働と、レガシー移行の成功は、こうした「見えない」部分への深い理解にかかっている。ダンプ解析のスキルは、単なるデバッグツールではなく、プログラムの「魂」を読み解くための羅針盤だ。
今回解説した内容が、皆様の現場におけるトラブルシューティングや、将来のシステム設計の一助となれば幸いである。PL/Iの深淵は、まだまだ我々を飽きさせない。
