メニューボタン
IBMi海外記事2012.11.20

RPGの多重並列スレッドのサポート

スコット・クレメント 著

IBM i 6.1から、ILE RPGは1つのジョブの中での多重並列スレッドをサポートしています。これをサポートするためにはRPGコンパイラと実行時環境に大きな変更を加える必要がありました。修正規模が大きかったので、この修正は実現しないのではないかと何年にもわたって言われていました。しかしIBM i 6.1でそれがついに修正されたのです。しかしながら誰もこの機能を使用していません。

スレッドを使用すると1つのプログラムの中でマルチ・タスキングを行うことができます。たとえば10個のサブ・プロシージャのコピーを同じジョブの中ですべてお互い同時に実行することができます。実行したい各タスクに対してバッチ・ジョブを個別にサブミットする必要はありません。同じプログラム内でマルチ・タスクを実行することができるのです。複数のスレッド間で変数やその他の資源を共有することができます。これはご存知の通り、同じプログラム内にある複数のルーチンの場合と同じです。しかしそれでもスレッドの場合は、他のスレッドの完了を待つことなくお互いに独立して実行することができます。

スレッドを簡単に理解する方法はバッチ・ジョブと似ているとみなすことです。新しいジョブはサブミットされると、そのジョブをサブミットしたジョブと同時に実行されます。スレッドも同様ですが、同じジョブそして特に同じプログラムの中で同時に実行される点が異なります。スレッドを作成する際のオーバーヘッドはずっと少なくなっています。それは新しいジョブを作成する必要がないからです。そしてスレッドはプログラム中で実行されるので、プログラムの変数へもアクセスできます。

スレッドを誰も使わない理由は簡単です。ほとんどのビジネス・アプリケーションはスレッドを使うような処理を必要としていないのです。ビジネス・アプリケーションでは、順番に処理されなければならないいくつかのステップが含まれています。しかし必ずしもそうではない領域があることを私は発見したのです。それは通信プログラミングです。私が所属する部門には、クライアント・プログラムからのリクエストを受け取る複数のTCP/IPサーバー・プログラムがあります。クライアントは、サーバーに対するリクエストがあるためサーバーに接続してきて、われわれが開発したサーバー側プログラムがそのリクエストを即座に受け取って処理することを期待しています。サーバーに対して複数の接続が同時に発生したときに問題が生じます。複数の接続すべてを同時に処理するにはどうしたらよいのでしょうか。リリース6.1以前では各リクエストに対して新しいジョブをサブミットしなければなりませんでしたが、そうするとパフォーマンス・コストが高くなります。RPGが並列スレッドをサポートするようになりましたので、クライアントのリクエストを処理する新しいスレッドを作成することができるようになり、オーバーヘッドは大幅に少なくて済みます。

RPGのスレッドのサポート

RPGはV5R1以来、THREADというH仕様キーワードをサポートしていますが、6.1になるまではこのキーワードで使えたパラメータは*SERIALIZEのみで、「一度に1つ」という意味の特別な値でした。 (Javaのような)多重スレッド環境がTHREAD(*SERIALIZE)を含むRPGモジュール中の任意のルーチンを呼び出すとき、その呼び出しはキューに入り個別に実行されます。たとえば、3つのJavaスレッドがシリアライズされたモジュール中のRPGのサブ・プロシージャを呼び出すと、最初のスレッドが実行されますが、他の2つのスレッドは最初のスレッドの実行が完了するまで待ちます。最初のスレッド呼び出しが完了すると2番目のスレッドが実行されます。3番目のスレッドは2番目のスレッドの実行が完了するまで待ちます。各スレッドをキューの中で待たせておいて一度に1つずつ実行することを「シリアライズ」と呼びます。

6.1ではTHREADキーワードに*CONCURRENTという新しいパラメータが加わりました。このパラメータは多重のスレッドを並列に実行できるという意味です。*SERIALIZEパラメータを使用した時のように他のスレッドが完了するまで待つ必要はもうなくなりました。THREAD(*CONCURRENT)とコーディングすると、RPGモジュール中のルーチンを呼び出す各スレッドはすべての静的変数の固有のコピーを与えられます。静的変数はモジュールに対して大域的な変数であるか、またはサブ・プロシージャ内で定義されてSTATICキーワードでコーディングされたものです。各スレッドは静的変数の固有のコピーを所有しますので、1つのスレッドが値を変更しても他のスレッドの変数には影響を与えません。サブ・プロシージャ内で(staticキーワードを使用せずに)定義された変数は「automatic」格納域内に存在し、サブ・プロシージャが起動されたときに作成されて、終了したときに破壊されます。automatic変数はサブ・プロシージャが起動されたときに作成されますから、automatic変数もまたスレッドごとに別個のものです。結局、各スレッドはサブ・プロシージャの新しいコピーを起動しますので、それぞれ固有のautomatic変数を持つことになります。

RPGのスレッドのサポートでは新しいスレッドを作成するためのopcodeがありません。その代わりにオペレーティング・システムがPOSIX Thread (Pthread) APIを提供しています。このAPIをCプログラムやCOBOLプログラムの中や、WindowsやLinuxなどといった他のプラットフォーム上の中で使用することもできます。IBMはPthread APIのコピーブックを提供しており、IBM iオペレーティング・システムのSystem Openness Includes (57xx-SS1オプション13)オプションの一部であるQSYSINCライブラリ中に含まれています。System Openness Includesはオペレーティング・システムのオプション・コンポーネントです。これを使用する権利は誰にでもありますが、部門によってはIBM iのインストール用メディアから57xx-SS1オプション13をインストールしないと使用できない場合があります。 QSYSINCにはソースコードとしても使用できるコピーブックが含まれていますので、ソースコードからコンパイルを計画しているシステムだけにこれをインストールすればよいのです。

RPGでのスレッドの管理方法

多重スレッドの処理をするように構成されたILE RPGプログラムの開始部分を図1に示します。

図1: 多重並列スレッドを持つILE RPGプログラムの開始部分

図01

ここではH仕様にTHREAD(*CONCURRENT)を指定しPthreadのコピーブックを取り入れています。このようにしてPthread APIを呼び出してスレッドを処理するのに必要なプロトタイプと定数を作成します。

Pthread APIにはスレッドを扱うための多数のいろいろなルーチンが含まれています。ここではそのすべてについては説明しませんが、スレッドをまず使い始めるのに必要なものについて説明します。Pthread APIの全リファレンス・マニュアルは、Programming|APIs|Unix-Type|APIs|Pthread APIs下のIBM Information Centerにあります。

スレッドの作成と破壊に使用するAPIは以下の通りです:

  • pthread_create は新しいスレッドを作成します。これはSBMJOBコマンドと似ていますが、新しいジョブではなく独立したスレッドを作成するという点が異なります。
  • pthread_join はこのAPIを呼び出したスレッドを停止し別のスレッド(そのIDはパラメータとして指定)が終了するのを待ちます。
  • pthread_detach はスレッドが完了した後にプログラムがそのステータスをチェックしないようにシステムに対して伝えます。デフォルトでは、pthread_joinが呼ばれるまでシステムはスレッドのステータス情報をメモリ上に保存しており、他のために使用できるメモリを消費してしまいます。スレッドのステータスをチェックするつもりがまったくないかスレッドが完了するのを待つつもりがない場合は、pthread_detachを呼ぶことができます。こうするとシステムはスレッドが完了したときにステータス情報を破棄します。
  • pthread_exit は実行中のスレッド内から呼び出してそのスレッドを終了させるAPIです。pthread_exitを呼ばない場合、そのスレッドはpthread_create APIによって呼び出されたサブ・プロシージャが返ってきたときに終了します。
  • pthread_cancel はあるスレッドから呼び出して別のスレッドをキャンセルするためのAPIです。

図2に示す例ではpthread_create APIを10回呼び出すことでRPGプログラムから10個のスレッドを生成しています。

図2: 多重並列スレッドを持つRPGプログラムの例

図02

このプログラムが完了するとmyProcプロシージャのコピーが10個できて並列に実行されます。このプログラム例をシンプルにしかも理解しやすくしておくために、myProcサブ・プロシージャに対して30秒間sleep(「ジョブの遅延」)してから終了するようにしてあります。すべてのスレッドは並列に実行されますから、このプログラムも全体の実行時間が30秒になります。一方、従来の方法でmyProcを10回実行させると、このプログラムは実行時間が5分になります。

実行中のスレッドを監視するために、オペレーティング・システムはスレッドIDという(内部の)データ構造を使用します。プログラム中ではLIKEDSキーワードを使用してpthread_tテンプレートのようなデータ構造を宣言することでスレッドIDを定義します。

図021

ここでは10個のスレッドを実行していますので、スレッドIDを配列にしまっています。RPGプログラム中ではこのデータ構造のサブ・フィールドを使用することはないでしょうが、このデータ構造をパラメータとしていろいろなpthread APIに渡すことでjoin、cancel、detachするスレッドを識別することができます。次にスレッドIDをpthread_join APIに渡してすべてのスレッドが完了するのを待ちます。

図022
  • スレッド属性 *OMITをこのパラメータに渡すことで、APIに対してデフォルトのパラメータ属性を使用するように伝えます。より高度なプログラムでは、スレッド属性をこのパラメータ内に設定することでスレッドの動作を細かく調整することができます。
  • サブ・プロシージャへのポインタ これは自分自身のスレッド内で実行を始めるサブ・プロシージャです。システムはコンピュータ上のメモリ内の特定のアドレスにあるコードを実行することでこのサブ・プロシージャを呼び出します。プログラム中のサブ・プロシージャのアドレスを取得するには%PADDR組み込み関数を呼び出します。
    図023
  • パラメータへのポインタ 新しいスレッド内で起動されたサブ・プロシージャに渡したい変数へのポインタを提供することができます。このパラメータを使用するときは、作成したスレッドごとに別の変数が使用されるようにしてください。もし2つのスレッドが同じ変数(またはメモリ上の同じ領域)を共有している場合、スレッド・セーフ問題(後述のスレッド・セーフティの節参照)が生じる場合があります。

errnoの値

pthread_create APIは、呼び出しが正常に終了して新しいスレッドが作成された場合には整数の0を返し、API呼び出しに失敗した場合はシステムのerrnoの値に対応した正の整数を返します。errnoの値はUnixやCの環境で使用されている4桁の数で、プログラム中で最後に発生したエラーを示します。errnoはpthreadコピーブックと同じQSYSINCに含まれているerrnoコピーブックの名前つき定数に対応しています。またerrnoの値はQCPFMSGメッセージ・ファイル中のメッセージ記述にも対応しています。メッセージ記述を探すには、エラー番号に続くCPEを探してください。たとえば、pthread_create APIが3021を返してきた場合は不正なパラメータが渡されたことを意味しています。Errnoコピーブックを見てみると、EINVAL定数が3021と定義されているのが見つかります(EINVALは「不正な引数(invalid argument」の短縮形)。EINVALエラーのメッセージ記述を表示するには次のように入力します。

図024

errnoの値をレポートするために、ReportErrorというサブ・プロシージャを書きました。このプログラムは対応するCPEメッセージ記述を検索し、見つかったメッセージを*ESCAPEメッセージとして呼び出したプログラムに送信します。プログラム例でReportErrorを呼び出していますが、

図025

掲載した図にはそのコードは含まれていません。しかし、ダウンロード可能な本稿のコード・バンドル(iProDeveloper.com/code)にはReportErrorを含むすべてのソースが含まれています。

ただしよくある「わかった」に注意してください。pthread APIはプログラムが多重スレッド対応でないジョブ中にあることを示すのに「資源がビジー」(EBUSY)というわかりづらいerrnoなどを返したり、ジョブが使用できるスレッドの最大値に達したことを示すのに「オペレーションによりプロセスが一時中断しました」という(EAGAIN)エラーを返したりすることがあります。

スレッドの制約

多重並列スレッドを生成するプログラムは対話型のジョブとして実行することはできません。その代わりに、バッチで実行するようにサブミットしてそのバッチ・ジョブが多重スレッドを使用できるように指定しなければなりません。たとえば、図2のコードをテストするには次のようにしてプログラムを実行させなければなりません。

図026

IBMが提供しているコマンドの中には多重スレッド環境で実行するには安全でないコマンドもあります。DSPCMDを使用することでどのコマンドが安全でどのコマンドが安全でないかを調べることができます。たとえば、DSPJOBLOGコマンドは多重スレッド環境で実行することができません。次のように入力して、

図027

ページを下へたどるとこのコマンドがスレッド・セーフでないという文が表示されているのがわかります。

同様に、スレッド・セーフではないAPIもあります。IBM Information Centerでは、各APIの説明の一番上にそのAPIがスレッド・セーフであるか否かが記載されています。スレッド・セーフではないコマンドやAPIを実行しようとすると、オペレーティング・システムが「*LIBL/DSPJOBLOGコマンドは多重ジョブでは安全ではありません」(CPD000D)などといったエラー・メッセージを送ってきます。

スレッド・セーフティ

どのIBMコマンドやAPIがスレッド・セーフでない可能性があるかどうかを知ることに加えて、作成したプログラムが多重スレッド環境下で正しく動作することを保証しなければなりません。コマンドやAPIとは違って、システムは皆さんが書いたコードがスレッド・セーフな方法で記述されているか否かを知りませんので、多重並列スレッド下で実行しても安全なコードであることを保証するのは皆さんに委ねられています。

変数やその他の資源を複数のスレッドで共有するのは、スレッドが同時に実行されることを考えると頭痛の種となりかねません。たとえば、1つのスレッドがある変数の値をセットしようとしているときに他のスレッドがその同じ変数を読み出している場合を考えてみてください。1番目のスレッドが変数の値の一部しか更新していないときに2番目のスレッドが変数の値を読み出すと、とてもおかしなデータとなる可能性が高くなります。

ネイティブのファイル入出力操作のほとんどはスレッド・セーフですが、あるスレッドが(SETLL、READ、CHAINなどを使用して)ファイルの再配置をしようとしており、別のスレッドが同じオープン・データパスから読み出している場合に注意してください。SETLLの次にREADを実行しようとしていてSETLLがセットしたのとは全く異なるファイルのエリアからREADがレコードを取得するとどんな混乱が起こるのか想像してみてください。これは、SETLLとREADの間のほんの一瞬に別のスレッドがファイルの再配置を行ったということを意味しています。

SELECT、UPDATE、DELETE、INSERTなどといったSQLのデータ操作言語(DML: Data Manipulation Language)文はRPGプログラムの中でもっとも頻繁に使用する文ですが、これらはすべてスレッド・セーフです。ただし、Create Table、Create Alias、Create Indexなどといったデータ定義言語(DDL: Data Definition Language)文には気を付けてください。DDL文の中にはスレッド・セーフではないものがありますので、競合を避けるにはシリアライズしなければなりません。

RPGはP仕様でSERIALIZEというキーワードを提供しており、スレッド・セーフ化を助けてくれます。P仕様のSERIALIZEをサブ・プロシージャに指定すると、RPGは一度に1つのスレッドだけがそのプロシージャを実行するようにします。2番目のスレッドが同じプロシージャを呼び出そうとすると、RPGは2番目のスレッドを一時中断させて1番目のスレッドが終了するまで待たせます。この動作はH仕様でTHREAD(*SERIALIZE)を指定するのと似ていますが、SERIALIZEキーワードが定義されたプロシージャだけに作用するのに対して、H仕様のキーワードはモジュール全体に作用するという点が異なります。

サブ・プロシージャでSERIALIZEキーワードを指定する方法を図3に示します。この例では多重並列スレッドがその年の注文数を合計しています。全体の数をユーザーにレポートするために、各スレッドはfinTotal変数にそれぞれの合計を加算しなければなりません。

図3: 共有データへのアクセスのシリアライズ

図03

デフォルトでは大域変数のコピーはサブ・プロシージャごとに作成されますが、ここではRPGのSTATIC(*ALLTHREAD)キーワードを使用してこの動作を回避しています。

図031

STATIC(*ALLTHREAD)キーワードを使用すると、変数の1つのインスタンスをすべてのスレッドが共有しますのでスレッド・セーフに対する懸念も生じます。 そこでこの例では一度に1つのスレッドだけがfinTotalを更新できるということを保証しなければなりません。これを実現するために、addToFinTotalというルーチンを作成し、そのコードにSERIALIZEキーワードを付加しました。

図032

スレッドが常にこのルーチンを呼び出して合計数を修正する限り、この例のプログラムはスレッド・セーフとなります。

pthread APIには記憶域を同期させるためのその他の方法が含まれていますが、SERIALIZEキーワードを使用するのが最も簡単な方法だと思います。

コード・バンドルの内容

本稿のコード・バンドルにはソケットAPIを使用したTCP/IPサーバー・プログラムが含まれています。このサーバー・プログラムは新しい接続を受け入れ、受け入れた新しい接続をスレッドに渡します。したがって、メイン・プログラムはさらに新しい接続がくるのを黙って待っていて、リモート・ユーザーからのリクエストはスレッドに処理させることができます。私はこのテクニックを使用して製造工場にあるデータ収集機器からのデータを取得していますが、本稿の目的にかなうため、WindowsのTelnetクライアントからの接続を受け入れ、そのクライアントに名前を尋ね、その名前を画面上に表示しています。このテクニックを使用すると20個以上の同時接続をすべて1つのソケット・プログラムで簡単に処理することができ、新しいジョブをバッチにサブミットする必要も、複雑なspawn APIを使用する必要もありません。スレッドの作成をより早くより簡単にできるのです。

またこのTCP/IPプログラムはpthread_detachの例も提供しています。このサーバー・プログラムでは各スレッドのステータスを読み出す必要はありませんので、単純にスレッドをdetachしています。こうすることで、あとでpthread_joinでスレッドを待つ必要がありません。

いかがでしょうか

今日スレッドを使用しているRPGの開発現場はほとんどありませんし、それが変わるとは思っておりません。多重スレッドのプログラムを記述する必要はビジネス・アプリケーションではそれほど頻繁にあるものではありません。ただしその必要が生じたときに、どこから始めてどのように動作するのかを理解するのが大変なのです。本稿により皆さんの考えに少しでも刺激が与えられて、私が思いもつかなかった新しい創造的な多重スレッド・アプリケーションの使用方法を見つけてくれることを願っています。そのようなアプリケーションの使用方法を見つけたら是非meat@scottklement.comまでメールをください。そういうお話を是非伺いたいです。

あわせて読みたい記事

PAGE TOP