lockステートメントによるパフォーマンス低下を軽減するそのほかのアプローチとしては、.NET Frameworkのクラス・ライブラリで用意されている「Interlocked」と「ReaderWriterLock」というクラスを使用することもできる。そのクラスの仕様から、使用する機会は限定されるかもしれないが、それがフィットする場面では積極的に活用したい。次に、これら2つのクラスについて説明していく。
■スレッドセーフな変数の操作を提供するInterlockedクラス
lockステートメントは非常にコストのかかるオペレーションであるが、.NETにはこれよりもコストのずっと低い排他制御のオペレーションが用意されている。それがInterlockedクラス(System.Threading名前空間)である。
Interlockedクラスは、以下に示す(1)〜(4)の4つの処理に適用でき、lockステートメントよりも低いコストで実行することができる。排他制御を行わなくてはならないとき、まずInterlockedクラスが使えるかどうかを検討するとよい。
(1) 変数に1を足す「Interlocked.Incrementメソッド」
(2) 変数から1を引く「Interlocked.Decrementメソッド」
IncrementメソッドとDecrementメソッドは、変数に単純に1を足したり引いたりするオペレーションである。このような処理は、普通にプログラミングを行っていると「number++」(C#の場合)のように非常に短く単純な1行で記述できるため、この1行はスレッドセーフのように思えるかもしれないが、実はそうではない。
少なくとも、コードがコンパイルされMSILレベルになると、メモリから値を読み込む、1を加える、メモリに値を書き込むという3つのオペレーションに分解される。つまり、排他制御を行わなければ、マルチスレッド環境においてはデータの不整合を起こす可能性が出てくる。また、環境によって(特にマルチプロセッサやマルチコア・プロセッサなど)はMSILレベルで1オペレーションでも同時に実行されることがあるため、排他制御を行わない限りスレッドセーフにはならないと考えた方がよい。
InterlockedクラスのIncrement/Decrementメソッドは、この1を足したり引いたりする動作がスレッドセーフであることを保証する。
int number = 0;
Interlocked.Increment(ref number); // numberに1を足す
Dim number As Integer = 0
Interlocked.Increment(ref number) ' numberに1を足す
int number = 0;
Interlocked.Decrement(ref number); // numberから1を引く
Dim number As Integer = 0
Interlocked.Decrement(ref number) ' numberから1を引く
(3) 変数へ値を設定する「Interlocked.Exchangeメソッド」
Exchangeメソッドは、第1パラメータに指定された変数に対して、第2パラメータに指定された値を代入する動作をスレッドセーフで行う。メソッドは、第1パラメータに指定された変数の変更前の値を戻り値として返す。
int number = 0;
int originalNumber = 100;
Interlocked.Exchange(number, originalNumber);
// numberにoriginalNumberの値を代入
Dim number As Integer = 0
Dim originalNumber2 As Integer = 100
Interlocked.Exchange(number, originalNumber)
' numberにoriginalNumberの値を代入
(4) 値を比較し、等しければ指定した別の値を代入する「Interlocked.CompareExchangeメソッド」
CompareExchangeメソッドは3つのパラメータを持つ。このメソッドは、第1パラメータと第3パラメータの値を比較し、値が等しければ第2パラメータの値を第1パラメータに代入するという動作を行う。戻り値は第1パラメータの変更前の値になる。
次のサンプル・コードのように、何らかの初期化処理などで、オブジェクトxがnullであったらオブジェクトyをxに代入するといったコードを、lockステートメントを使用して記述してあるとしよう。
lock (syncObject) // syncObjectはロック用の適当なオブジェクト
{
if (x == null)
{
x = y;
}
}
SyncLock syncObject ' syncObjectはロック用の適当なオブジェクト
If x Is Nothing Then
x = y
End If
End SyncLock
このような場合には、次のようにCompareExchangeメソッドを使用してパフォーマンスの向上を図ることができる。
Interlocked.CompareExchange(x, y, null);
Interlocked.CompareExchange(x, y, null)
■書き込み時のみに排他制御を行うReaderWriterLockクラス
lockステートメントによる排他制御は、常に1つのスレッドからしか、あるデータにアクセスできないようにするものであった。しかし、データの更新がほとんどなく、複数スレッドによって参照されることが多いデータについては、排他制御にかかるパフォーマンスの低下を抑える方法がある。
それは、複数のスレッドがデータにアクセスをするときに、書き込みを行うスレッドがない場合には排他制御を行わないという方法である。なぜなら、データを更新せずに読み出すだけのときは、複数スレッドによるアクセスがあってもデータの不整合が起こることはないからである。ReaderWriterLockクラス(System.Threading名前空間)はそのような機能を提供するクラスである。
ReaderWriterLockクラスでは、読み取り時のロック(リーダーロック)と書き込み時のロック(ライターロック)の2種類のロックを取得および解放する手段を提供する。リーダー/ライターロックの取得と待機、解放は以下のような条件で行われる。
リーダーロック取得 | 可能 |
---|---|
ライターロック取得 | リーダーロック解放まで待機 |
あるスレッドがリーダーロックを取得している場合に、ほかのスレッドが取得可能なロック |
リーダーロック取得 | ライターロック解放まで待機 |
---|---|
ライターロック取得 | ライターロック解放まで待機 |
あるスレッドがライターロックを取得している場合に、ほかのスレッドが取得可能なロック |
次のList4はReaderWriterLockクラスを使ったサンプル・プログラムである。
using System;
using System.Threading;
public class List4
{
private static Resource resource = new Resource();
public static void Main()
{
for (int i=0; i < 10; i++)
{
(new Thread(new ThreadStart(resource.Read))).Start(); // (1)
(new Thread(new ThreadStart(resource.Write))).Start(); // (2)
}
}
}
public class Resource
{
private ReaderWriterLock rwLock = new ReaderWriterLock();
// 共有リソース
private int balance = 2000;
public void Read()
{
try
{
// リーダーロックを取得
rwLock.AcquireReaderLock(Timeout.Infinite); // (3)
Console.WriteLine(balance);
}
finally
{
// リーダーロックを解放
rwLock.ReleaseReaderLock(); // (4)
}
}
public void Write()
{
try
{
// ライターロックを取得
rwLock.AcquireWriterLock(Timeout.Infinite); // (5)
// balanceに100を足して、100を引く
int tempBalance = balance;
balance = tempBalance + 100;
Thread.Sleep(1000); // わざと競合を起こすため
tempBalance = balance;
balance = tempBalance - 100;
}
finally
{
// ライターロックを解放
rwLock.ReleaseWriterLock(); // (6)
}
}
}
Imports System
Imports System.Threading
Public Class List4
Private Shared resource As New Resource()
Public Shared Sub Main()
Dim i As Integer
For i = 0 To 9
Dim threadA As Thread = _
New Thread(New ThreadStart(AddressOf resource.Read)) ' (1)
Dim threadB As Thread = _
New Thread(New ThreadStart(AddressOf resource.Write)) ' (2)
threadA.Start()
threadB.Start()
Next i
End Sub 'Main
End Class 'List4
Public Class Resource
Private rwLock As New ReaderWriterLock()
' 共有リソース
Private balance As Integer = 2000
Public Sub Read()
Try
' リーダーロックを取得
rwLock.AcquireReaderLock(Timeout.Infinite) ' (3)
Console.WriteLine(balance)
Finally
' リーダーロックを解放
rwLock.ReleaseReaderLock()' (4)
End Try
End Sub 'Read
Public Sub Write()
Try
' ライターロックを取得
rwLock.AcquireWriterLock(Timeout.Infinite) ' (5)
' balanceに100を足して、100を引く
Dim tempBalance As Integer = balance
balance = tempBalance + 100
Thread.Sleep(1000) ' わざと競合を起こすため
tempBalance = balance
balance = tempBalance - 100
Finally
' ライターロックを解放
rwLock.ReleaseWriterLock() ' (6)
End Try
End Sub 'Write
End Class 'Resource
List4では、Resourceオブジェクトのデータを読み込むだけのスレッドと((1))、Resourceオブジェクトのデータを更新するスレッド((2))をそれぞれ10個ずつ作成し、動作させている。
そして、ResourceクラスのReadメソッドではリーダーロックを使用している。AcquireReaderLockメソッド((3))でリーダーロックを取得し、ReleaseReaderLockメソッド((4))でリーダーロックを解放する。この間に記述されたコードでは、balanceの値を表示しているのであるが、リーダーロックのため複数のスレッドが同時に値を読むことが可能となっている。
一方、Writeメソッドでは、balanceの値を更新するためにライターロックによる排他制御を行っている。AcquireWriterLockメソッド((5))でライターロックを取得し、ReleaseWriterLock((6))メソッドでライターロックを解放する。
このプログラムでは、あるスレッドがライターロックを取得してbalanceの値を更新している間は、ほかのスレッドはリーダーロックもライターロックも取得できず、balanceの値の更新や読み込みは、現在ライターロックを取得しているスレッドがbalanceの値を更新し、ライターロックを解放するのを待つこととなる。
ライターロックの取得と解放の行((5)と(6))をコメントアウトして実行してみると、排他制御がきちんと行われなくなってしまうことが分かるだろう。
マルチスレッド・プログラミングに排他制御を導入することによって、同時アクセスによるデータの不整合の発生を防ぐことができた。しかし、排他制御を行った場合には、今度は「デッドロック」という問題が起こり得る。
次回の後編では、このデッドロックについて、そして冒頭で少し触れた、複数のスレッド間で同期を取りながら処理を進める「同期制御」のためのプログラミングについて解説する。
Copyright© Digital Advantage Corp. All Rights Reserved.