連載
» 2005年06月15日 00時00分 公開

連載.NETマルチスレッド・プログラミング入門:第4回 デッドロックの回避とスレッド間での同期制御 ― マルチスレッド・プログラミングにおける排他制御と同期制御(後編) ― (2/3)

[高木 健一(http://www.woodensoldier.info/),著]

スレッド同士を協調動作させる「同期制御」

 マルチスレッドは処理を並行して同時実行する技術であるが、場合によってはそのスレッド同士がタイミングを計って協調動作しなければならないときがある。このような制御を「同期制御」と呼ぶ。同期制御の実現にはいくつかの方法が用意されているが、ここでは代表的な手法であるWait/Pulse方式を使用した同期制御の方法を紹介する。

■Wait/Pulse方式(Monitorクラス)によるスレッドの同期制御

 同期制御を行うための便利な機能として、Monitorクラス(System.Threading名前空間)には、Wait/Pulse/PulseAllという静的メソッドが用意されている。

 これらのメソッドは、任意のオブジェクトの「ウェイトセット」というスペースに、スレッドを待機させたり、そのスレッドの処理を再開させたりすることができる機能を提供する。

 以下にWaitメソッドとPulseAllメソッドの動きを示した(なお、ウェイトセットはWait/Pulseメソッドによる処理の概念を理解するためのモデルであり、実際の.NET Frameworkでの実装では、実行待ちキューや待機キューなどで構成される)。

WaitメソッドとPulseAllメソッドによるウェイトセットの利用

 Waitメソッドを実行したスレッドは、そのパラメータに指定されたオブジェクトのウェイトセットへ入り、処理を中断し待機モードとなる。((1)(2))。

 その後、ほかのスレッドによってPulseメソッドもしくはPulseAllメソッドが実行されると、ウェイトセット内のスレッドは待機をやめ、処理を再開する。((3)(4))。ウェイトセットには複数のスレッドを待機させておくことができ、Pulseメソッドはウェイトセット内のスレッドを1つだけ、PulseAllメソッドはウェイトセット内のすべてのスレッドを全部起こす。

 Wait/Pulseの仕組みでは、ロックに関してのいくつかのルールがある。Waitメソッドを実行する際には、そのウェイトセットのあるオブジェクトに対してのロックを保持している必要がある。もし、ロックを保持していないオブジェクトに対してWaitメソッドを実行すると、SynchronizationLockException例外が発生してしまう。Waitメソッドが正しく実行されると、そのスレッドはウェイトセットに入りそのオブジェクトに対してのロックを解放する。

 PulseメソッドもしくはPulseAllメソッドによってスレッドが起こされたときには、あらためてそのオブジェクトのロックを取得しに行き、ロックを取得することができたスレッドは処理を再開することができる。つまり、PulseあるいはPulseAllメソッドで起こされてもロックを取得できなかったスレッドは、ロックが解放されるまでまたしばらく待機しなくてはならない。

 .NETでは、ロックの取得には、lockステートメントとMonitorクラスのEnterメソッドの2つの方法がある。

 lockステートメントによるロックの取得は、すでに第3回で説明したとおりであるが、lockステートメントは実はMonitorクラスのEnter/Exitメソッドを使用している(コンパイラにより書き換えられる)。

 つまり、次のコードは、

lock (object1)
{
  // object1のロックを取得
}

実際には次のようなMonitorクラスを使用した処理と同等である。

Monitor.Enter(object1)
try
{
  // object1のロックを取得
}
finally
{
  Monitor.Exit(object1)
}

■Monitorクラスを使用したProducer-Consumerパターンによる同期制御

 以下に示すList3は、実際にWait/PulseAllを使用したサンプル・コードである。これは「Producer-Consumer(生産者-消費者)パターン」というもので、マルチスレッドにおける同期制御の1つのパターンになっている。

 このパターンは、ProducerとConsumerが同期を取りながら独立して動作するパターンの1つで、Producerは「物」を生産して、共有のスペースに置き、Consumerは物が共有のスペースにあったら、その物を消費するという動作を行う。

 共有スペースは、物を置くための容量に制限があり、共有スペースがいっぱいだったらProducerは共有スペースが空くまで待機し、逆に、Consumerは共有スペースに物がないときは、Producerによって物が置かれるまで待機する。

 以上のような動作を、Wait/PulseAllメソッドを使って実現したのが以下のサンプル・プログラムである。このプログラムでは、Producerを「料理人」、Consumerを「客」、共有スペースを「テーブル」、共有ペースに置く物を「料理」と見立てている。

using System;
using System.Collections;
using System.Threading;

// Producer-Consumerパターン
// WaitとPulseAllのサンプル
public class List3
{
  static void Main ()
  {
    Table table = new Table(); // 共有スペースであるテーブル

    for(int i=0; i < 5; i++)
    {
      (new Producer(table)).ThreadStart();
      (new Consumer(table)).ThreadStart();
    }
  }
}

public class Producer // 料理人
{
  private readonly Table table;
  private static int id = 0;
  private readonly object lockObject = new object();

  public Producer(Table table)
  {
    this.table = table;
  }

  public void ThreadStart()
  {
    (new Thread(new ThreadStart(Produce))).Start();
  }

  private void Produce()
  {
    string dish;

    while (true)
    {
      lock (lockObject) // idの値を排他制御するためのロック
      {
        dish = "No." + id++;
      }

      // 料理(dish)を作成して、テーブルに置く
      Console.WriteLine(dish + " 作成");
      table.put(dish);

      Thread.Sleep(5000); // 5秒ごとに1料理作成できる
    }
  }
}

public class Consumer // 客
{
  private readonly Table table;

  public Consumer(Table table)
  {
    this.table = table;
  }

  public void ThreadStart()
  {
    (new Thread(new ThreadStart(Consume))).Start();
  }

  public void Consume()
  {
    while(true)
    {
      // テーブルから料理を取り、消費する
      Console.WriteLine(table.take() + " テーブルから取り消費");

      Thread.Sleep(10000); // 消費には時間がかかる
    }
  }
}

public sealed class Table // テーブル
{
  // テーブルに置くことができる料理の最大数
  private readonly int max = 3;
  private readonly Queue queue = new Queue(); // テーブルの実体
  private readonly object lockObject = new object();

  public void put(string dish)
  {
    Monitor.Enter(lockObject);

    try
    {
      while (queue.Count >= max)
      {
        // テーブルがいっぱいで料理を置くことができなかったら、
        // ウェイトセットに入る
        Monitor.Wait(lockObject);
      }
      queue.Enqueue(dish);
      Console.WriteLine(dish + " テーブルに置かれた");

      // ウェイトセットのスレッドを起こす
      Monitor.PulseAll(lockObject);
    }
    catch
    {
    }
    finally
    {
      Monitor.Exit(lockObject);
    }
  }

  public string take()
  {
    Monitor.Enter(lockObject);

    string dish = string.Empty;

    try
    {
      while (queue.Count == 0)
      {
        // テーブルに料理がなかったら、ウェイトセットに入る
        Monitor.Wait(lockObject);
      }
      dish = queue.Dequeue() as string;

      // ウェイトセットのスレッドを起こす
      Monitor.PulseAll(lockObject);
    }
    catch
    {
    }
    finally
    {
      Monitor.Exit(lockObject);
    }

    return dish;
  }
}

List3 Producer-Consumerパターンを利用したC#のサンプル・プログラム
List3.csのダウンロード

Imports System
Imports System.Collections
Imports System.Threading

' Producer-Consumerパターン

' WaitとPulseAllのサンプル
Public Class List3

  Shared Sub Main()
    Dim table As New Table() ' 共有スペースであるテーブル

    Dim i As Integer
    For i = 0 To 4
      Dim producer = New Producer(table)
      producer.ThreadStart()
      Dim consumer = New Consumer(table)
      consumer.ThreadStart()
    Next i
  End Sub 'Main
End Class 'List3

Public Class Producer ' 料理人
  Private table As Table
  Private Shared id As Integer = 0
  Private lockObject As New Object()

  Public Sub New(table As Table)
    Me.table = table
  End Sub 'New

  Public Sub ThreadStart()
    Dim thread As Thread = New Thread(New ThreadStart(AddressOf Produce))
    thread.Start()
  End Sub 'ThreadStart

  Private Sub Produce()
    Dim dish As String

    While True
      SyncLock lockObject ' idの値を排他制御するためのロック
        id = id + 1
        dish = "No." + id.ToString()
      End SyncLock

      ' 料理(dish)を作成して、テーブルに置く
      Console.WriteLine(dish + " 作成")
      table.put(dish)

      Thread.Sleep(5000) ' 5秒ごとに1料理作成できる
    End While
  End Sub 'Produce
End Class 'Producer

Public Class Consumer ' 客
  Private table As Table

  Public Sub New(table As Table)
    Me.table = table
  End Sub 'New

  Public Sub ThreadStart()
    Dim thread As Thread = New Thread(New ThreadStart(AddressOf Consume))
    thread.Start()
  End Sub 'ThreadStart

  Public Sub Consume()
    While True
      ' テーブルから料理を取り、消費する
      Console.WriteLine(table.take() + " テーブルから取り消費")

      Thread.Sleep(10000) ' 消費には時間がかかる
    End While
  End Sub 'Consume
End Class 'Consumer


NotInheritable Public Class Table ' テーブル
  ' テーブルに置くことができる料理の最大数
  Private max As Integer = 3
  Private queue As New Queue() ' テーブルの実体
  Private lockObject As New Object()

  Public Sub put(dish As String)
    Monitor.Enter(lockObject)

    Try
      While queue.Count >= max
        ' テーブルがいっぱいで料理を置くことができなかったら、
        ' ウェイトセットに入る
        Monitor.Wait(lockObject)
      End While

      queue.Enqueue(dish)
      Console.WriteLine(dish + " テーブルに置かれた")

      ' ウェイトセットのスレッドを起こす
      Monitor.PulseAll(lockObject)
    Catch
    Finally
      Monitor.Exit(lockObject)
    End Try
  End Sub 'put

  Public Function take() As String
    Monitor.Enter(lockObject)

    Try
      While queue.Count = 0
        ' テーブルに料理がなかったら、ウェイトセットに入る
        Monitor.Wait(lockObject)
      End While

      Dim dish As String = queue.Dequeue()

      ' ウェイトセットのスレッドを起こす
      Monitor.PulseAll(lockObject)
      Return dish
    Catch
    Finally
      Monitor.Exit(lockObject)
    End Try
  End Function 'take
End Class 'Table

List3 Producer-Consumerパターンを利用したVB.NETのサンプル・プログラム
List3.vbのダウンロード

 このプログラムではProducerとConsumerがそれぞれ5つずつ作成され、Producerはdish(料理)を作成して、共有スペースであるTableに置く。Tableに空きがなければ、空きができるまで待つ。Tableには最大で3つまでdishを置くことができるように設定している。一方、ConsumerはTableに料理があれば消費し(食べ)、なければ料理が置かれるまで待機する。このような動作を延々と繰り返す。

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

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

メールマガジン登録

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