これは InterSystems FAQ サイトの記事です。
永続クラス定義(またはテーブル定義)に対してオブジェクト操作でデータの参照・更新を行うとき、オブジェクトオープンで使用する%OpenId()、オブジェクトの削除に使用する%DeleteId()の第2引数を使用して並行処理の制御方法を選択できます。
ご参考:オブジェクト同時処理のオプション
既定値は1です。(永続クラスのDEFAULTCONCURRENCYクラスパラメータでデフォルト値を指定できます。特に変更していない場合は 1を使用します)
並行処理の基本事項は以下の通りです。
- アトミックな書き込みを保証したい場合は並行処理の値は0より大きい値を指定する必要があります。
- 並行処理の値が0より大きい場合、オブジェクトの保存や削除処理中は、ロックの取得と解放を実施します。
並行処理の値別の動作の違いは以下の通りです。(ドキュメントの「並行処理の値」に表がありますので併せてご覧ください)
- 値1の場合は、新規にオブジェクト生成したときにはロックを取得しません。既存オブジェクトを開く際に複数グローバルノードがある場合のみ共有ロックを取得し、開くのが終わったら開放します。(複数グローバルノードを持たないオブジェクトを開く場合はロックしません。)オブジェクトの保存時はロックを取得して保存処理が完了するとロックを解放します。
- 値2の場合は、既存オブジェクトを開くときに常に共有ロックを取得しますが開いた後はロックを開放します。残りは1と同様です。
- 値3の場合は、既存オブジェクトを開くとき、開いた後、保存処理が完了した後、共有ロックを取得します。新規にオブジェクトを作成するときはロックを取得しません。
- 値4の場合は、既存オブジェクトを開くとき、開いた後、新規オブジェクトを初めて保存するときに排他ロックを取得します。
- 値0の場合は、ロックを使用しないオプションです。 (※アトミックな書き込みを保証できないオプションです。このオプションを使用する場合は注意が必要です。)
以下の2クラスを使用して、下記2項目を解説します。
【1】並行処理オプションの値4でオープンしたインスタンスがある中でデフォルトオプションでオープンした場合のロック取得について
例の2クラスの定義は以下の通りです。
Test.Person
Class Test.Person Extends %Persistent
{
Property Name As %String;
Property Tel As %String;
}
ObjectScriptObjectScript
Test.Personを継承したTest.Employeeクラス
Class Test.Employee Extends Test.Person
{
Property EmpID As %String;
}
ObjectScriptObjectScript
データは、Test.Personクラスで1件、Test.Employeeクラスで1件用意するとします。
//Test.Person
set p=##class(Test.Person).%New()
set p.Name="Personです"
set p.Tel=""
set p.Tel="03-5321-6200"
write p.%Save()
write p.%Id() //割り当てられたID番号が返ります。
//Test.Employee
set e=##class(Test.Employee).%New()
set e.Name="従業員1"
set e.Tel="03-5321-6200"
set e.EmpID="EMP001"
write e.%Save()
write e.%Id() //ID番号を調査
ObjectScriptObjectScript
この時のグローバル変数は以下の通りです。
Test.Employeeのデータは複数のグローバルノードを持つデータであることを確認できます。(サブクラスに用意したプロパティのデータが第2ノード以下に格納されています。)
以降の説明では、Test.PersonのIDは 1、Test.EmployeeのIDは 2 として以下解説します。
【1】並行処理オプションの値4でオープンしたインスタンスがある中でデフォルトオプションでオープンした場合のロック取得について
《Test.Personでの例》
1) 1枚目のターミナルで任意の永続オブジェクトを並行処理の値 4でオープンします。
write $JOB // 実行中ターミナルのプロセスIDを取得できます
set p=##class(Test.Person).%OpenId(1,4)
ObjectScriptObjectScript
2) 2枚目のターミナルで1枚目のターミナル同じ永続オブジェクトを並行処理の値 4 でオープンします。
write $JOB // 実行中ターミナルのプロセスIDを取得できます
set p=##class(Test.Person).%OpenId(1,4,.status)
ObjectScriptObjectScript
2枚目のターミナルでは、(デフォルトで)10秒待たされます。待機中に管理ポータルの以下メニューにアクセスすると、ロックの状況を確認できます。
管理ポータル > [システムオペレーション] > [ロックの表示]
第3引数で指定した変数を使用して、オープン後のステータスを確認します。
>write $system.Status.GetErrorText(status)
エラー #5803: インスタンス 'Test.Person' の排他ロックの取得ができません
ObjectScriptObjectScript
排他ロックの取得待ちがタイムアウトしてオープンに失敗したことがわかります。
3) 2枚目のターミナルで今度は%OpenId()の第2引数を指定しない(並行処理のデフォルト)でオープンします。
set p=##class(Test.Person).%OpenId(1)
ObjectScriptObjectScript
オープンできます。
Test.Personのデータは^Test.PersonD(ID)以下にデータが格納され、複数のグローバルノードを持たないデータであるためオープン時にロック取得を行っていないことがわかります。
では、ここでプロパティを変更し保存してみます。
set p.Name="あ"
set status=p.%Save()
ObjectScriptObjectScript
すぐにプロンプトが戻らないことを確認できます。(ロック表示は1つ前の図解と同じです。)
ステータスを確認します。
>write $system.Status.GetErrorText(status)
エラー #5803: インスタンス 'Test.Person' の排他ロックの取得ができません
ObjectScriptObjectScript
この結果から、並行処理のデフォルトオプション(値 1)でオープンしたインスタンスであっても、保存時には排他ロックを取得しようしていることがわかります。
次の確認に進むため、それぞれのターミナルでオープンしていたオブジェクトを削除します。
《Test.Employeeでの例》
次に、複数のグローバルノードをデータに持つTest.Employeeのデータを使用して、オープン時のロック取得方法を確認します。(ID=2のデータが存在していることとします)
1) Test.Employeeのの永続オブジェクトを並行処理の値 4でオープンします。
set e=##class(Test.Employee).%OpenId(2,4)
ObjectScriptObjectScript
2) 2枚目のターミナルで1枚目と同じ永続オブジェクトを並行処理のデフォルトオプション(値1)でオープンします。
write $JOB // 実行中ターミナルのプロセスIDを取得できます
set e=##class(Test.Employee).%OpenId(2,,.status)
ObjectScriptObjectScript
(10秒待たされます。)
ロックの状況を確認すると、共有ロックを取得しようとしていることを確認できます。
>write $system.Status.GetErrorText(status)
エラー #5804: インスタンス 'Test.Employee' の読み込みロックの取得ができません
ObjectScriptObjectScript
Test.Employeeのデータが複数のグローバルノードを持つデータであるため、並行処理の値1を利用したオープン時、共有ロックを取得しようとしていたことを確認できます。
並行処理の値4(排他ロック)でオープンしたインスタンスがある中で他プロセスからデフォルトオプション(値1)でオープンする場合、そのインスタンスが複数のグローバルノード持つ/持たないによってロック取得方法が変わることを確認できました。
次の確認を行う場合は、すべてのインスタンスを削除(または開放)してください。
【2】トランザクション中のロックの開放について
続いて、トランザクション中のロックの取得と開放について確認します。ここでも、Test.PersonとTest.Employeeを使用して解説します。
《Test.Personでの例》
1) 1枚目のターミナルでTest.Person(ID=1)をデフォルトの並行処理の値でオープンし、プロパティを更新後、TSTARTコマンドを実行し、%Save()を実行します。その後でインスタンスがセットされた変数を消去します。
set p=##class(Test.Person).%OpenId(1)
set p.Name="テスト"
tstart
set st=p.%Save()
kill p
ObjectScriptObjectScript
ここで管理ポータルのロック表示を確認します。
デフォルトの並行処理の値を使用した場合、保存完了後はロックを保持しませんが、トランザクション中は排他処理を保持していることがわかります。
トランザクションを開始することで、一瞬で解放されるロックもトランザクションのコミットやロールバックが来るまで開放が待たされていることがわかります。またインスタンスを開放していたとしても、トランザクションが確定されるまでロックの開放が待たされていることも確認できます。
2) 2枚目のターミナルで1枚目と同じオブジェクトを並行処理の値4でオープンします。
write $JOB
set p=##class(Test.Person).%OpenId(1,4,.status)
ObjectScriptObjectScript
10秒のタイムアウト後にプロンプトが戻ります。タイムアウト前のロックの状態は下図の通りです。
エラーメッセージを確認します。
>write $system.Status.GetErrorText(status)
エラー #5803: インスタンス 'Test.Person' の排他ロックの取得ができません
ObjectScriptObjectScript
1枚目のターミナルでトランザクションが確定するまでロック解放は待たされます。
そのため2枚目のターミナルで行ったオブジェクトのオープンは排他ロック取得待ち状態となるため、ロックが取得できずエラーが返ります。
1枚目のターミナルでトランザクションを確定させてから(tcommit または trollback)、再度2枚目のターミナルで同じオブジェクトを並行処理の値4でオープンしてみるとオープンできることを確認できます。
この結果から、並行処理の値がデフォルトであってもトランザクション実行中の%Save()の実行により取得した排他ロックが継続します。
また、トランザクションが終了するまでロック解放は待たされます。(オブジェクトを破棄しても待たされます。)
次の確認を行うため、トランザクションはロールバックし、インスタンスを消去します。
《Test.Employeeでの例》
次に、複数のグローバルノードを持つTest.Employeeで同様のテストを行います。
1) 1枚目のターミナルでTest.Employee(ID=2)をデフォルトの並行処理の値でオープンし、プロパティを更新後、TSTARTコマンドを実行し、%Save()を実行します。
set e=##class(Test.Employee).%OpenId(2)
set e.EmpID="EMP999"
tstart
set status=e.%Save()
kill e
ObjectScriptObjectScript
この時のロックはTest.Personの時と同様に、^Test.Person(2)で排他ロックを取得しています。(インスタンスを設定した変数を削除してもロックを取得したままの状態です。)
2) 2枚目のターミナルでTest.Employee(ID=2)をデフォルトの並行処理の値でオープンしてみます。
set e=##class(Test.Employee).%OpenId(2,,.status)
ObjectScriptObjectScript
共有ロック待ちであることを確認できます。
>write $system.Status.GetErrorText(status)
エラー #5804: インスタンス 'Test.Employee' の読み込みロックの取得ができません
ObjectScriptObjectScript
Test.Employeeのように複数のグローバルノードを持つ永続オブジェクトの場合、オープン時に共有ロックを取得しようとするため、1枚目のターミナルのロックが開放されるまで(トランザクションが終了しインスタンスが破棄されるまで)2枚目のターミナルでインスタンスをオープンすることができません。
以上の結果から、トランザクション中に多くのオブジェクトを保存する場合は、トランザクションが終了するまでオブジェクトが破棄されていても、ロック開放が待たされることにご注意ください。
(テストを終了するため、トランザクションはロールバック、全インスタンスを消去してください。)