実行の同期と競合の解決

マルチスレッドの実行がアプリケーション内で制御されない場合は、予期しない結果が生じます。 アプリケーションを実行して目的の結果を得るには、スレッドの実行の同期を取り、スレッド間のデータ競合を解決する必要があります。この問題を解決するには、さまざまな方法があります。 使用する方法は、アプリケーションのデータアクセス特性や、アプリケーションを構成する個々の COBOL プログラム内で完全なマルチスレッドを実現するための要件によって異なります。 使用できる方法は次のとおりです。

マルチスレッドプログラム属性

COBOL プログラム内のマルチスレッドに影響を与える主なプログラム属性は 3 つあります。 これらの属性は、プログラムのコンパイル時にマルチスレッドのコンパイラ指令を取り込んだり、取り外したりすることで割り当てられます。 使用する指令はプログラムのシステム作業領域の割り当てに影響を与え、コンパイルされたプログラムはシステムによって自動的にロックされることがあります。 プログラムは、次のように指定できます。

非マルチスレッド指定

プログラムのコンパイル時にマルチスレッドコンパイラ指令を指定しないと、非マルチスレッドプログラムになります。 この場合は、システム作業領域は静的に割り当てられ、競合の対象となります。 この方法には、呼び出しの高速化、マルチスレッド間でのスタックの有効利用など、いくつかの利点がありますが、非マルチスレッドプログラムで、確実に一度に 1 つのスレッドのみが実行されるかどうかはアプリケーションによって異なります。 ただし、呼び出し側プログラムの暗黙的なロジックにより、非マルチスレッドプログラムで、一度に 1 つのスレッドのみ実行するようにできます。たとえば、1 つのスレッドが非マルチスレッドプログラムを呼び出すようにアプリケーションを設計します。 または、呼び出されたプログラムが実行される直前に呼び出し側プログラム内で同期プリミティブ (ミューテックスなど) の 1 つをロックし、呼び出されたプログラムが戻る前にそのロックを解放します。

シリアルプログラム

シリアル属性をもつプログラムでは、システム作業領域は静的に割り当てられます。プログラムは開始時にロックされ、終了時にロック解放されます。 このロック機能により、一度に 1 つのスレッドのみがプログラムを実行することになるので、システムまたはユーザ作業領域での競合を回避できます。 他の明示的なアプリケーションのロジックは必要ありません。

コンパイル時に SERIAL コンパイラ指令を指定すると、プログラムにシリアル属性を与えます。

通常の COBOL プログラムをシリアルプログラムとして指定すると、プログラムのソースコードを変更しないでマルチスレッドアプリケーションにインクルードできます。

一方、シリアルプログラムの欠点は次のとおりです。

再入可能なプログラム

REENRANT コンパイラ指令を指定してコンパイルすると、マルチスレッドプログラムを再入可能なプログラムに設定できます。 マルチスレッドアプリケーションでは、ほとんどの (すべてではない) モジュールに対して、再入可能なプログラムを使用してください。

REENTRANT(1) を指定した場合には、コンパイラが生成したすべての一時作業領域はそれぞれのスレッドに割り当てられます。 環境部とデータ部に割り当てられたすべてのユーザデータ領域と FD ファイル領域は、すべてのスレッドで共有されます。 プログラマは、CBL_ 同期呼び出しを使用して、プログラムのデータを確実にシリアル化する必要があります。

REENTRANT(2) を指定した場合は、システム作業領域の他に、作業場所節 (Working-Storage Section) とファイル節 (File Section) のすべてのデータはそれぞれのスレッドに割り当てられます。 Local-Storage は、スタック上に動的に割り当てられます。 この割り当てにより、これらの領域での競合が回避されるため、複数のスレッドから同時に呼び出された場合でもプログラムは問題なく動作します。 プログラムのロックまたはロック解放は必要ありません。 このコンパイラ指令設定の欠点は、スレッド間でデータを共有しないことです (EXTERNAL で定義されたデータを除く)。

REENTRANT(2) は、プログラムをマルチスレッドアプリケーションで素早く、簡単に動作させる方法です。ただし、通常は REENTRANT(1) を使用してコンパイルしてください。

再入可能なプログラムでは、その Working-Storage Section と File Section 内のデータ項目で起こる可能性があるすべての競合を解決する必要があります。 1 つ以上の技法を使用して、データ競合を解決する必要があります。詳細については、次を参照してください。

データ属性の使用

データ項目の属性は、そのデータ項目にアクセスするスレッドで競合が発生するかどうかを決定します。 スレッドは次のデータ項目に対しては競合しません。

プログラムで定義された他のデータ項目はスレッド間で共有されるため、競合が発生する可能性があります。 次の場合に競合が発生することがあります。

通常、Local-Storage Section で定義されたデータは、再帰的な COBOL プログラムで使用されます。プログラムが再帰的に呼び出されるたびに、このデータのインスタンスが新規にスタックに割り当てられます。 マルチスレッドアプリケーションの各スレッドは独自のスタックを持っているので、この属性は、再入可能なプログラムで競合しない一時的な作業項目を定義するのに理想的です。 設計によっては、再入可能な COBOL プログラムを 1 つのスレッド内で再帰的に使用できます。

Local-Storage Section で定義されたデータの欠点は、再入可能または再帰的なプログラムが終了するとデータ項目を見失うことです。 プログラムの終了後に再度そのプログラムを開始すると、Local-Storage Section で定義されたデータ項目の値は未定義になります。 スレッドの要請により複数の呼び出しについてプログラムで状態を保存する必要がある場合は、別の機構を使用する必要があります。

これらの問題は、Thread-Local-Storage Section または THREAD-LOCAL 属性により、スレッドローカルとして定義されたデータを使用すると解決します。 スレッドローカルデータは各スレッドに対して一意で、呼び出し全体に対して固定されているため、value 句で初期化できます。 スレッドローカルデータはスレッド固有の Working-Storage Section のデータ項目として表示できます。 この固定データは、ほとんどの再入可能なプログラムでの競合問題を解決するのに非常に役立ちます。 多くの場合は、ファイル処理を行わないプログラムで Working-Storage Section 見出しを Thread-Local-Storage Section 見出しに変更するのみで、完全に再入可能なプログラムにできます。 読み込み専用の定数はすべて Working-Storage Section で、読み書きデータはすべて Thread-Local-Storage Section で定義すると、データの割り当てを微調整できます。

スレッドローカルデータの欠点は次のとおりです。

スレッドではプライベートなデータ以外のデータが必要となることがあります。 大部分のマルチスレッドアプリケーションは、厳密なプロトコルのもとで各スレッドがアクセスする共有データを使用して通信し合い、スレッドの実行を調整します。 COBOL では、競合によるデータの破壊を防ぐさまざまな同期プリミティブと Working Storage Section または File Section のデータを併用することでこれを実現します。 同期プリミティブの詳細については、次を参照してください。

同期プリミティブの使用

マルチスレッドアプリケーションで目的の結果を得るには、共有データにアクセスするスレッド間で同期を取ることが重要です。 スレッド間で行われるさまざまなデータアクセスの性質を理解することは、使用する同期プリミティブと方式を決定するための第一歩です。 データアクセスの特性を理解すると、すべてのスレッド間で使用するデータ項目の同期方式も簡単に理解できます。 次では、データを共有する上での一般的な問題とそれらの解決策について概説します。

ミューテックスの使用

データ共有の最も単純な問題は、処理中のある時点で複数のスレッドが同時に互いに排他的に共有データへアクセスする場合です。 この共有データにアクセスするコードの部分はクリティカルセクションと呼ばれます。これらのクリティカルセクションは、共有データ項目に論理的に関連付けられたミューテックスを使用することで保護できます。 「ミューテックス (mutex)」は、mutual exclusion (相互排他) から派生した用語です。 データ項目に関連付けられたミューテックスは、クリティカルセクションが実行される前にロックされ、終了された後にロック解放されます。

保護するデータにアクセスする前に、すべてのスレッドがミューテックスをロックすることが非常に重要です。 スレッドが 1 つでもこの同期方式に失敗し、ミューテックスをロックできなかった場合は、予期しない結果が発生します。

たとえば、次のコードは表に項目を追加するため、または表内の項目数を調べるためにその表にアクセスする 2 つのクリティカルセクションを保護する方法を示しています。 作業場所節のデータ項目 table-xxx は、table-mutex によって保護されます。

例 - クリティカルセクションの保護

この例では、あるスレッドが表を読み込んでいる間に別のスレッドがその表にデータを追加するのを防ぐために、ミューテックスが必要となります。

 Working-Storage Section.
 78  table-length        value 20.
 01  table-mutex         usage mutex-pointer.
 01  table-current       pic x(4) comp-x value 0.
 01  table-max           pic x(4) comp-x value 0.
 01  table-1.
    05  table-item-1     pic x(10) occurs 20.
 Local-Storage Section.
 01  table-count         pic x(4) comp-x.
 01  table-i             pic x(4) comp-x.

* シングルスレッドモードで実行される初期化コードです。
     move 0 to table-max
     open table-mutex

* table-1 に項目を追加します。これは、クリティカルセクションです。
     set table-mutex to on
     if table-current < table-length
         add 1 to table-current
         move 'filled in' to table-item-1(table-current)
     end-if
     set table-mutex to off

* table-1 の項目数を数えます。これは、クリティカルセクションです。
     set table-mutex to on
     move 0 to table-count
     perform varying table-i from 1 by 1 
             until table-i > table-current
         if  table-item-1(table-i) = 'filled in'
             add 1 to table-count
         end-if
     end-perform
     set table-mutex to off

ミューテックスを使用する上での 1 つの問題は、アプリケーション内でのマルチスレッドのレベルが厳密に制限されることです。 たとえば、表に項目を追加するプログラム、または表内の項目数を数えるプログラムなどいくつかのプログラムからなるアプリケーションがあるとします。 このアプリケーションでマルチスレッド処理を最大にするために、複数のスレッドを使用して表内の項目数を同時に数えたいとします。 ただし、あるプログラムが表に項目を追加している場合には、別のプログラムで項目の追加や項目数の計算を行いたくありません。

この問題を解決するには同期プリミティブを使用します。同期プリミティブは、読み取り専用のクリティカルセクションでは複数のスレッドをアクティブにし、書き込み可能なクリティカルセクションではあるスレッドがアクティブのときは他のスレッドのアクセスを許可しません。このような同期プリミティブの例としては、モニタがあります。

モニタの使用

モニタは、ミューテックスでは容易に処理できない特別なアクセス問題を解決します。 たとえば、データの読み込みは多数のスレッドで同時に行い、データの書き込みは 1 つのスレッドのみで行うことができます。 スレッドがデータを書き込んでいる間は、他のスレッドからの読み込みアクセスを禁止できます。

モニタは、クリティカルセクションで使用し、クリティカルセクションが保護データで実行するデータアクセスの種類を宣言します。データアクセスの種類とは、読み込み、書き込み、または参照を指します。

モニタの同期機能を拡張し、クリティカルセクションで参照を行うこともできます。この参照機能は、実際のアプリケーションで大変役立ちます。 参照用のクリティカルセクションは、データの読み込みや保護データの書き込みを行います。条件によっては、保護データ項目の書き込みを行わない場合もあります。 このクリティカルセクションがアクティブの場合には、単にデータを読み込むのみのクリティカルセションは複数実行できますが、参照または書き込みを行う他のクリティカルセクションは実行できません。 参照しているスレッドが保護データへの書き込みを必要と判断した場合は、参照ロックから書き込みロックへの変換を要求します。 変換プロセスは、データを読み込むクリティカルセクションがすべて終了した後で実行され、データの読み込みや書き込みを行う他のクリティカルセクションがデータにアクセスするのを防ぎます。 参照用のクリティカルセクションは、保護データ項目に排他的にアクセスし、書き込み処理を行います (この状態は、参照用のクリティカルセクションが保護データ項目を読み込んだときの状態を保証します)。

例 - モニタを使用した複数スレッドのアクセス制御

次のコードは、表内の項目数を数えたり、表に項目を追加したりする複数のスレッドのアクセスを制御するモニタを示します。 このコードでは、次の処理を行います。

 Working-Storage Section.
 78  table-length                   value 20.
 01  table-monitor              usage monitor-pointer.
 01  table-current              pic x(4) comp-x value 0.
 01  table-1.
     05  table-item-1           pic x(10) occurs table-length.
 Local-Storage Section.
 01  table-count                pic x(4) comp-x.
 01  table-i                    pic x(4) comp-x.
  . . .
*> シングルスレッドモードで実行される
*> 初期化コードです。
     move 0 to table-current
     open table-monitor

* table-1 に項目を追加します。これは、書き込み用のクリティカルセクションです。
     set table-monitor to writing
     if table-current < table-length
         add 1 to table-current
         move 'filled in' to table-item-1(table-current)
     end-if
     set table-monitor to not writing

* table-1 の項目数を数えます。これは、読み込み用のクリティカルセクションです。
     set table-monitor to reading
     move 0 to table-count
     perform varying table-i from 1 by 1
             until table-i > table-current
         if  table-item-1(table-i) = 'filled in'
             add 1 to table-count
         end-if
     end-perform
     set table-monitor to not reading

例 - モニタを使用する参照用のクリティカルセクション

簡単な参照用のクリティカルセクションの例を示します。

 Working-Storage Section.
 01  data-monitor       usage monitor-pointer.
 01  data-value         pic x(4) comp-x value 0.

* シングルスレッドモードで実行される
* 初期化コードです。
     open data-monitor

* table-1 に項目を追加します。これは、参照用のクリティカルセクションです。
     set data-monitor to browsing
if data-value < 20
         set data-monitor to writing 
             converting from browsing
         add 5 to data-value
         set data-monitor to not writing
     else
         set data-monitor to not browsing
     end-if

このような簡単な確認のために参照ロックを使用することはお奨めしません。 通常、参照ロックを使用する必要があるのは、実際に書き込みロックが必要かどうかを決定するために多大な作業が必要で、アプリケーション全体でマルチスレッドを最大限に利用する場合のみです。 他にも、アプリケーションでマルチスレッドのレベルを最大にするためのさまざまなモニタ変換があります。

セマフォの使用

セマフォは、同期プリミティブの一種で、ゲートのように、その値を減少することでコード内でのスレッドの実行を禁止したり、その値を増加させることでそのコード内での (他のスレッドの) 実行を許可したりします。 セマフォは、ミューテックスと似ているので、ミューテックスのかわりに使用できます。

例 - セマフォの使用

次のコードでは、セマフォの使用例を示します。

 Working-Storage Section.
 01  data-semaphore     usage semaphore-pointer.
 01  data-value         pic x(4) comp-x value 0.

* シングルスレッドモードで実行される初期化コードです。
     open data-semaphore
     set data-semaphore up by 1      *> 増加として初期化します。

* data-value の変更を追加します。これは、クリティカルセクションです。
     set data-semaphore down by 1
     add 1 to data-value
* セマフォにより他のスレッドの実行が可能になります。
     set data-semaphore up by 1      

OPEN 文の直後で、セマフォが 1 増加されていることに注目してください。これにより、最初のセマフォの獲得は成功しますが、それ以降の獲得はそのセマフォが再度解放されるまではすべてブロックされます。

セマフォは、ミューテックスほど効果的ではありませんが、より柔軟性があります。1 つのスレッドがセマフォを単に解放するのみで、他のスレッドはその解放されたセマフォを獲得できます。 ミューテックスと対比すると、 ミューテックスの場合は、常に解放される前に獲得する必要があります。ミューテックスの獲得操作と解放操作は同じスレッド内で行う必要があります。 セマフォはあるスレッドから別のスレッドへのシグナルとなります。

例 - 2 つのスレッド間でハンドシェイクを確立するためのセマフォの使用

次のコードでは、2 つのスレッド間のハンドシェイクを確立するために異なる 2 つのセマフォを使用します。 このハンドシェイクにより、一方のスレッドは新規データ値の生成を通知し、他方のスレッドはそのデータ値の使用を通知できます。

 Working-Storage Section.
 01  produced-semaphore         usage semaphore-pointer.
 01  data-value                 pic x(4) comp-x value 0.
 01  consumed-semaphore         usage semaphore-pointer.

* シングルスレッドモードで実行される初期化コードです。
     open produced-semaphore
     open consumed-semaphore
     set consumed-semaphore up by 1

* このコードは、data-value を生成するために 1 度実行されます。
     set consumed-semaphore down by 1
     add 10 to data-value
* data-value の変更を通知するシグナルです。
     set produced-semaphore up by 1	

* data-value を変更するために待機状態にある別のスレッドも、
* 1 度のみ、このコードを実行します。
     set produced-semaphore down by 1
     display data-value
* data-value の使用を通知するシグナルです。
     set consumed-semaphore up by 1	

この例では、作成者 / 使用者間の問題として知られる、もう一つの一般的な同期問題を示しています。 最も単純な作成者 / 使用者間の問題は、データを生成する 1 つのスレッドとそのデータを使用する もう 1 つのスレッドがある場合に、使用スレッドの実行中は処理対象のデータが常に存在するように、これらの作成スレッドと使用スレッド間で実行の同期を取る必要があるということです。

上記のセマフォは解放されていないセマフォ数を数え、多数のスレッドがセマフォを獲得しブロック解除を行うようにします。 セマフォをこのように数えると、使用スレッドがデータ値を獲得するまでの間、ブロックして待機する前に、作成スレッドは複数のデータの値 (通常は配列で) を生成できます。

例 - セマフォの数え方

次のコードは、作成側と使用側の組の簡単な例を示します。作成側は、データ表がいっぱいになるまでデータの値を作成できます。データ表がいっぱいになると、作成側は値が使用されるまで新たに値を作成しません。

 Working-Storage Section.
 78  table-size				value 20.
 01  produced-semaphore		 usage semaphore-pointer.
 01  filler.
     05  table-data-value	pic x(4) comp-x 
					        occurs table-size times value 0.
 01  consumed-semaphore		 usage semaphore-pointer.
 Local-Storage Section.
 01  table-i					pic x(4) comp-x.

* シングルスレッドモードで実行される初期化コード
     open produced-semaphore
     open consumed-semaphore
*  値を 20 回増加します。
     set consumed-semaphore up by table-size 

* 作成スレッド
     move 1 to table-i
     perform until 1 = 0
         set consumed-semaphore down by 1
         add table-i to table-data-value(table-i)
         set produced-semaphore up by 1
         add 1 to table-i
         if  table-i > table-size
             move 1 to table-i
         end-if
     end-perform.

* 使用スレッド
     move 1 to table-i
     perform until 1 = 0
         set produced-semaphore down by 1
         display '現在の作成値:' 
                 table-data-value(table-i)
         set consumed-semaphore up by 1
         add 1 to table-i
         if  table-i > table-size
             move 1 to table-i
         end-if
     end-perform.

イベントの使用

あるスレッドが別のスレッドに、注意を喚起する必要が生じたことをシグナルとして通知できる点で、イベントはセマフォと似ています。 イベントは、セマフォと比べて柔軟ですが、少し複雑になります。 その理由の 1 つは、イベントを一度ポストすると、そのイベントを明示的にクリアする必要があるということです。

例 - イベント同期の使用

次のコード例では、セマフォ同期のかわりにイベント同期を使用して作成者 / 使用者間の問題を解決します。

 Working-Storage Section.
 01  produced-event     usage event-pointer.
 01  data-value	        pic x(4) comp-x value 0.
 01  consumed-event     usage evnt-pointer.

* シングルスレッドモードで実行される初期化コード
     open produced-event
     open consumed-event
     set consumed-event to on   *> 「ポスト済み」として初期化します。

* 作成側のプロトコルです。
     wait for consumed-event
     set consumed-event to false *> イベントをクリアします。
     add 10 to data-value

* data-value の変更を通知するシグナルです。
     set produced-event to true  *> イベントをポストします。

* 使用側のプロトコルです。data-value の変更を
* 待ちます。
     wait for produced-event
     set produced-event to false *> イベントをクリアします。
     display data-value

* 実行可能であることを他のスレッドへ通知するシグナルです。このスレッドは
* data-value をもっています。
     set consumed-event to true  *> イベントをポストします。

上記のコードを実行するスレッドが 2 つ (作成スレッドと使用スレッド) のみの場合は、問題なく動作します。 これ以外のスレッドが作成スレッドまたは使用スレッドとして起動された場合は、予期しないイベントが発生します。 これは、イベントがセマフォとは異なり、一度ポストされるとそのイベントの発生を待っているスレッドをすべてアクティブにするためです。セマフォの場合は、セマフォが解放された後は 1 つのスレッドのみの実行を可能にします。 イベントがポストされ、待ち状態のスレッドがアクティブ化されると、これらの各スレッドはそのイベント (イベントのクリアを含む) に対して何らかのアクションを行うべきかどうかを決定する必要があります。

最後のポイントとして、待ち状態の複数のスレッドが存在し、特定の用途のために独自の同期オブジェクトの構築を可能にする場合には、イベントの使用は難しくなります。