第3回 マルチスレッドでデータの不整合を防ぐための排他制御 ― マルチスレッド・プログラミングにおける排他制御と同期制御(前編) ―:連載.NETマルチスレッド・プログラミング入門(2/3 ページ)
複数の処理が並行動作する場面では、スレッド間の排他制御は不可欠。.NETで用意される排他制御の仕組みをまとめる。
複数のスレッドに対して安全な「スレッドセーフ」
複数のスレッドからメソッドやプロパティにアクセスされても問題が起きない、もしくは複数スレッドからアクセスされることを想定し排他制御が行われているようなクラスやメソッドを「スレッドセーフ」と呼ぶ。
冒頭のList1では、Bankクラスがスレッドセーフでなかったために、複数スレッドからアクセスを行うと問題が生じた。スレッドセーフでないリソースを複数スレッドからアクセスする場合には排他制御を行わなくてはならないわけだが、これには2つの方法がある。
- アクセスされるリソースのクラスをスレッドセーフにする
- アクセスするクラスで排他制御を行う
List1の例で行った、ThreadMethodメソッドへのlockステートメントの追加は、後者の「アクセスするクラスで排他制御を行う」による排他制御である。
しかし、一般には1つ目の方法である、「アクセスされるリソースのクラスをスレッドセーフにする」方が望ましい。その理由は、排他制御を行う場所を局所化できるからである。こうすることによって、デッドロックの可能性や、パフォーマンスの低下の際の原因を追究することが容易になる。また、排他制御を行うコードを書く場所を集中化できるため、変更にも強いコードが出来上がる。
■クラスのスレッドセーフ化
実際に、List1においてBankクラスをスレッドセーフにする方法を考えてみよう。今回の場合は、Bankクラスのデータにアクセスする方法として新たにメソッドを定義し、排他制御をBankクラス内で行うようにする。
預金残高を示すフィールドbalanceに対して入金をするためのメソッド「AddBalance」を追加したコードを以下に示す(なお、Balanceプロパティは使われなくなったので削除した)。
// 預金残高(balance)を保持するBankクラス
class Bank
{
private object lockObject = new object();
private int balance = 1000;
public int AddBalance(int money)
{
lock (lockObject)
{
int balanceTmp = balance;
Thread.Sleep(1000);
balance = balanceTmp + 200;
return balance;
}
}
}
' 預金残高(balance)を保持するBankクラス
Class Bank
Private lockObject As New Object()
Private balance As Integer = 1000
Public Function AddBalance(money As Integer) As Integer
SyncLock lockObject
Dim balanceTmp As Integer = balance
Thread.Sleep(1000)
balance = balanceTmp + 200
Return balance
End SyncLock
End Function 'AddBalance
End Class 'Bank
これに伴って、AtmThreadクラスのThreadMethodメソッドでは排他制御を行う必要がなくなったので、lockステートメントは不要となる。また、Balanceプロパティを利用していた部分はAddBalanceメソッドに置き換える必要がある。それらの変更を加えたThreadMethodメソッドが次のコードである。
private void ThreadMethod()
{
int result = bank.AddBalance(200);
Console.WriteLine("{0}: bank.Balance + 200 = {1}", name, result);
}
Private Sub ThreadMethod()
Dim result As Integer = bank.AddBalance(200)
Console.WriteLine("{0}: bank.Balance + 200 = {1}", name, result)
End Sub 'ThreadMethod
このように、外部からアクセスされるメソッド単位でスレッドセーフであることが保証されているクラスを作成することによって、マルチスレッド環境においても、堅固で、変更に強いプログラムを作成することが可能となる。
スレッドセーフなクラスを作成するときは、そのクラスが持っているすべてのメソッドとプロパティがスレッドセーフであるようにしておくべきだ。そうすることによって、そのクラスを使用する側がマルチスレッド・プログラミング上の不具合を起こしにくくなる。
また、できるのであれば、作成したスレッドセーフなクラスを継承できないようにsealedなクラス(VB.NETではNotInheritableなクラス)にしておくと、継承先でスレッドセーフな状態が破壊されてしまう危険性が少なくなる。
■排他制御はパフォーマンスの劣化を招く
排他制御がマルチスレッド環境下でデータを守るために重要であることは理解していただけたであろう。そして、1つのアイデアとして、マルチスレッドにいつでも対応できるよう、あらかじめすべてのクラスにできるだけ排他制御を組み込んでおこう、という考えを持った方もいるだろう。
しかし、そのアイデアは必ずしも正しいとは限らない。その理由はパフォーマンスの問題である。つまり、排他制御を行うと、プログラムの実行パフォーマンスが悪くなる。その原因は主に2つある。
1つは、排他制御の仕組みそのものである。せっかくマルチスレッドによる並行処理によってパフォーマンスを上げようとしているのに、排他制御ではその並行処理を部分的に並行で動作しないように制御するということを行っている。つまりデータの整合性を保つために、部分的にマルチスレッドによるパフォーマンスの利点をつぶすことになる。
そして、もう1つはlockステートメントによるパフォーマンスの低下である。lockステートメントを実行してロックを取得する場合の実行コストは小さくない。
このような理由で.NET Frameworkのクラス・ライブラリを始めとする多くのクラスでは、通常マルチスレッド環境を意識しないクラス設計となっている。既存のクラスをマルチスレッドに対応したスレッドセーフなコンポーネントにするためには、ラッパー・クラス(スレッドセーフでないクラスを内部的に使用する、スレッドセーフなクラス)を使用することで対処できる。
基本的には、排他制御によるパフォーマンスの低下をできるだけ少なくするために、まずはロックによる排他制御を行わなくてもよいような設計を検討した方がよい。それでも排他制御を行わなくてはいけない個所においては、できるだけロックする範囲と時間を小さくするとよい。
Copyright© Digital Advantage Corp. All Rights Reserved.