第3回 マルチスレッドでデータの不整合を防ぐための排他制御 ― マルチスレッド・プログラミングにおける排他制御と同期制御(前編) ―連載.NETマルチスレッド・プログラミング入門(2/3 ページ)

» 2005年05月25日 00時00分 公開

複数のスレッドに対して安全な「スレッドセーフ」

 複数のスレッドからメソッドやプロパティにアクセスされても問題が起きない、もしくは複数スレッドからアクセスされることを想定し排他制御が行われているようなクラスやメソッドを「スレッドセーフ」と呼ぶ。

 冒頭のList1では、Bankクラスがスレッドセーフでなかったために、複数スレッドからアクセスを行うと問題が生じた。スレッドセーフでないリソースを複数スレッドからアクセスする場合には排他制御を行わなくてはならないわけだが、これには2つの方法がある。

  1. アクセスされるリソースのクラスをスレッドセーフにする
  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;
    }
  }
}

List2 スレッドセーフにしたList1のBankクラス(C#)

' 預金残高(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

List2 スレッドセーフにしたList1のBankクラス(VB.NET)

 これに伴って、AtmThreadクラスのThreadMethodメソッドでは排他制御を行う必要がなくなったので、lockステートメントは不要となる。また、Balanceプロパティを利用していた部分はAddBalanceメソッドに置き換える必要がある。それらの変更を加えたThreadMethodメソッドが次のコードである。

private void ThreadMethod()
{
  int result = bank.AddBalance(200);
  Console.WriteLine("{0}: bank.Balance + 200 = {1}", name, result);
}

List3 スレッドセーフにしたBankクラスにアクセスするAtmThreadクラスのThreadMethodメソッド(C#)

Private Sub ThreadMethod()
  Dim result As Integer = bank.AddBalance(200)
  Console.WriteLine("{0}: bank.Balance + 200 = {1}", name, result)
End Sub 'ThreadMethod

List3 スレッドセーフにしたBankクラスにアクセスするAtmThreadクラスのThreadMethodメソッド(VB.NET)

 このように、外部からアクセスされるメソッド単位でスレッドセーフであることが保証されているクラスを作成することによって、マルチスレッド環境においても、堅固で、変更に強いプログラムを作成することが可能となる。

スレッドセーフなクラス
マルチスレッド環境で、堅固で、変更に強いプログラムを作るには、外部からアクセスされるメソッド単位でスレッドセーフであることが保証されているクラスを作成するとよい。

 スレッドセーフなクラスを作成するときは、そのクラスが持っているすべてのメソッドとプロパティがスレッドセーフであるようにしておくべきだ。そうすることによって、そのクラスを使用する側がマルチスレッド・プログラミング上の不具合を起こしにくくなる。

 また、できるのであれば、作成したスレッドセーフなクラスを継承できないようにsealedなクラス(VB.NETではNotInheritableなクラス)にしておくと、継承先でスレッドセーフな状態が破壊されてしまう危険性が少なくなる。

■排他制御はパフォーマンスの劣化を招く

 排他制御がマルチスレッド環境下でデータを守るために重要であることは理解していただけたであろう。そして、1つのアイデアとして、マルチスレッドにいつでも対応できるよう、あらかじめすべてのクラスにできるだけ排他制御を組み込んでおこう、という考えを持った方もいるだろう。

 しかし、そのアイデアは必ずしも正しいとは限らない。その理由はパフォーマンスの問題である。つまり、排他制御を行うと、プログラムの実行パフォーマンスが悪くなる。その原因は主に2つある。

 1つは、排他制御の仕組みそのものである。せっかくマルチスレッドによる並行処理によってパフォーマンスを上げようとしているのに、排他制御ではその並行処理を部分的に並行で動作しないように制御するということを行っている。つまりデータの整合性を保つために、部分的にマルチスレッドによるパフォーマンスの利点をつぶすことになる。

 そして、もう1つはlockステートメントによるパフォーマンスの低下である。lockステートメントを実行してロックを取得する場合の実行コストは小さくない。

 このような理由で.NET Frameworkのクラス・ライブラリを始めとする多くのクラスでは、通常マルチスレッド環境を意識しないクラス設計となっている。既存のクラスをマルチスレッドに対応したスレッドセーフなコンポーネントにするためには、ラッパー・クラス(スレッドセーフでないクラスを内部的に使用する、スレッドセーフなクラス)を使用することで対処できる。

 基本的には、排他制御によるパフォーマンスの低下をできるだけ少なくするために、まずはロックによる排他制御を行わなくてもよいような設計を検討した方がよい。それでも排他制御を行わなくてはいけない個所においては、できるだけロックする範囲と時間を小さくするとよい。

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。