第2回 .NETにおけるマルチスレッドの実装方法を総括連載.NETマルチスレッド・プログラミング入門(3/4 ページ)

» 2005年04月20日 00時00分 公開

ThreadPoolクラスによるマルチスレッド

 サーバ型のプログラムなどで、リクエストが次々と送られてきて、その1つ1つに対する処理をマルチスレッドで動作させたいようなときには、スレッドプールを利用した方がリソース的には効率がよいことは前述したとおりである。

 次のList2は、スレッドプールを使用したサンプル・プログラムである。このプログラムでは、2つの処理をスレッドプールに登録し(スレッドプールのキューに追加し)、それぞれを別スレッドで実行する。

using System;
using System.Threading;

public class List2
{
  public static void Main()
  {
    // ThreadMethodをスレッドプールで実行できるように
    // WaitCallbackデリゲートを作成
    WaitCallback waitCallback
      = new WaitCallback(ThreadMethod); // (1)

    // スレッドプールに登録
    ThreadPool.QueueUserWorkItem(waitCallback, "A"); // (2)
    ThreadPool.QueueUserWorkItem(waitCallback, "B"); // (3)

    //何かキーが押されるまで、プログラムを実行
    Console.ReadLine();
  }

  // 別スレッドで動作させるメソッド
  private static void ThreadMethod(object state) // (4)
  {
    for (int i = 0; i < 100; i++)
    {
      Thread.Sleep(5);
      Console.Write(" {0} ", state);
    }
  }
}

List2 スレッドプールを利用したC#のサンプル・プログラム(List2.cs)
List2.csのダウンロード

Imports System
Imports System.Threading

Public Class List2

  Public Shared Sub Main()

    ' ThreadMethodをスレッドプールで実行できるように
    ' WaitCallbackデリゲートを作成
    Dim waitCallback As _
      New WaitCallback(AddressOf ThreadMethod) ' (1)

    ' スレッドプールに登録
    ThreadPool.QueueUserWorkItem(waitCallback, "A") ' (2)
    ThreadPool.QueueUserWorkItem(waitCallback, "B") ' (3)

    ' 何かキーが押されるまで、プログラムを実行
    Console.ReadLine()
  End Sub

  ' 別スレッドで動作させるメソッド
  Private Shared Sub ThreadMethod(state As Object) ' (4)
    For i As Integer = 0 To 99
      Thread.Sleep(5)
      Console.Write(" {0} ", state)
    Next i
  End Sub

End Class

List2 スレッドプールを利用したVB.NETのサンプル・プログラム(List2.vb)
List2.vbのダウンロード

 スレッドプールを利用するにはThreadPoolクラスを使用する。その手順をまとめると、以下のとおりとなる。

(1)別スレッドで実行したいメソッドを、「void メソッド名(object パラメータ)」の形式で作成する((4))。

private static void ThreadMethod(object state) // (4)
{
  ……
}

(2)いま作成したメソッドをパラメータとして、WaitCallbackデリゲート(System.Threading名前空間)のインスタンスを作成する((1))。

WaitCallback waitCallback = new WaitCallback(ThreadMethod); // (1)

(3)ThreadPool.QueueUserWorkItemメソッドを実行して、スレッドを開始する((2)(3))。

ThreadPool.QueueUserWorkItem(waitCallback, "A"); // (2)
ThreadPool.QueueUserWorkItem(waitCallback, "B"); // (3)

 この手順に沿って、もう一度上記のList2のコードを眺めてほしい。

 (1)の別スレッドとして実行させたいメソッドは、ThreadMethodメソッドである。確かにThreadMethodメソッドは、スレッドプールの仕様に合わせて、戻り値がvoid、パラメータがobject型になっているのが確認できる。

 そして、(2)のWaitCallbackデリゲートのインスタンスが作成されている。その際にThreadMethodメソッドをコンストラクタのパラメータとして指定している。これでスレッドプールを利用してThreadMethodメソッドを実行する準備は整ったことになる。

 最後に(3)のThreadPool.QueueUserWorkItemメソッドが実行されている。WaitCallbackのインスタンスをパラメータに指定してThreadPool.QueueUserWorkItemメソッド(静的メソッド)を実行すると、スレッドプールのキューにThreadMethodメソッドの実行指令が登録されることになる。後は、スレッドプール内のスレッドが自動的にスケジューリングされ、登録された実行指令が次々と実行されていく。

 ところで、ThreadPool.QueueUserWorkItemメソッドの第2パラメータには、object型のパラメータを指定することができる。このパラメータについての型チェックなどは行われないが、これを利用することによって、スレッドプールにより実行されるメソッドにパラメータを渡すことができる。今回のサンプルでは、“A”と“B”という文字列を渡している。

 なお、先ほども述べたように、ThreadPoolクラスを使って同時に実行できるスレッドの最大数は、1つのプロセス当たり「プロセッサ数×25」と決められており、同時に実行できるスレッドの残りの数はThreadPool.GetAvailableThreadsメソッドで取得できる。

デリゲートによるマルチスレッド

 デリゲート・オブジェクトに用意されているBeginInvokeメソッドでも、デリゲートによるメソッドの呼び出しを、スレッドプールを利用して行うことができる。

 この場合には、メソッドのパラメータや戻り値を通常のメソッド呼び出しと同じように利用することができ、また、スレッドの終了時には特定のメソッドが呼び出される(コールバックされる)ようにもできる。このため、ThreadクラスやThreadPoolクラスによるスレッドの実行よりも便利に使える。

 以下のList3に、デリゲートのBeginInvokeメソッドを利用したコード例を示す。

using System;
using System.Threading;

public class List3
{
  // 戻り値とパラメータのあるデリゲート
  delegate DateTime ThreadMethodDelegate(string c); // (1)
  static ThreadMethodDelegate threadMethodDelegate; // (2)

  public static void Main()
  {
    threadMethodDelegate
      = new ThreadMethodDelegate(ThreadMethod); // (3)

    // デリゲートによるスレッド処理呼び出し
    threadMethodDelegate.BeginInvoke(".",
      new AsyncCallback(MyCallback), DateTime.Now); // (4)

    Console.ReadLine();
  }

  // 別スレッドで呼び出されるメソッド
  private static DateTime ThreadMethod(string c)
  {
    // 10ミリ秒ごとに100回cを出力
    for (int i = 0; i < 100; i++)
    {
      Thread.Sleep(10);
      Console.Write(c);
    }
    return DateTime.Now;
  }

  // スレッド処理終了後に呼び出されるコールバック・メソッド
  private static void MyCallback(IAsyncResult ar) // (5)
  {
    DateTime result = threadMethodDelegate.EndInvoke(ar); // (6)
    DateTime beginTime = (DateTime)ar.AsyncState; // (7)

    Console.WriteLine();
    Console.WriteLine(
      "{0}に処理を開始し、{1}に処理を完了しました。",
      beginTime, result);
  }
}

List3 デリゲートのBeginInvokeメソッドを利用したC#のサンプル・プログラム(List3.cs)
List3.csのダウンロード

Imports System
Imports System.Threading

Public Class List3

  ' 戻り値とパラメータのあるデリゲート
  Delegate Function ThreadMethodDelegate(c As String) As DateTime ' (1)
  Shared threadMethodDelegateInstance As ThreadMethodDelegate ' (2)

  Public Shared Sub Main()

    threadMethodDelegateInstance _
      = New ThreadMethodDelegate(AddressOf ThreadMethod) ' (3)

    ' デリゲートによるスレッド処理呼び出し
    threadMethodDelegateInstance.BeginInvoke(".", _
      New AsyncCallback(AddressOf MyCallback), DateTime.Now) ' (4)

    Console.ReadLine()
  End Sub

  ' 別スレッドで呼び出されるメソッド
  Private Shared Function ThreadMethod(c As String) As DateTime

    ' 10ミリ秒ごとに100回cを出力
    For i As Integer = 0 To 99
      Thread.Sleep(10)
      Console.Write(c)
    Next i

    Return DateTime.Now
  End Function

  ' スレッド処理終了後に呼び出されるコールバック・メソッド

  Private Shared Sub MyCallback(ar As IAsyncResult) ' (5)

    Dim result As DateTime _
      = threadMethodDelegateInstance.EndInvoke(ar) ' (6)

    Dim beginTime As DateTime _
      = CType(ar.AsyncState, DateTime) ' (7)

    Console.WriteLine()
    Console.WriteLine( _
      "{0}に処理を開始し、{1}に処理を完了しました。", _
      beginTime, result)
  End Sub

End Class

List3 デリゲートのBeginInvokeメソッドを利用したVB.NETのサンプル・プログラム(List3.vb)
List3.vbのダウンロード

 このList3は、典型的なデリゲートによるマルチスレッド処理のサンプル・コードである。「ThreadMethod」という名前のメソッドを、デリゲートを利用して別スレッドで実行し、ThreadMethodメソッドの処理が終了した時点でMyCallbackメソッドが自動的にコールバックされるように設定している。MyCallbackメソッド内ではThreadMethodの戻り値を利用してメッセージを出力している。以下、順に詳しく解説していこう。

 まず、別スレッドとして処理したいメソッドをデリゲート宣言する。((1)

delegate DateTime ThreadMethodDelegate(string c); // (1)

 このときのパラメータの型と個数、戻り値の型は、別スレッドとして処理したいメソッドに合わせておく。サンプルではThreadMethodメソッドに対して、ThreadMethodDelegateというデリゲートを宣言している。ThreadMethodメソッド自体は、何も特別な部分のない通常のメソッドであることに注意していただきたい。

 ここで宣言したデリゲートを実際に利用するために、デリゲートのインスタンスを作成し、フィールド変数threadMethodDelegateとして保持する。((2)(3)

threadMethodDelegate = new ThreadMethodDelegate(ThreadMethod); // (3)

 次に、コールバック・メソッドを作成する。コールバック・メソッドとは、別スレッドによる処理が終了したことをトリガーとして、自動的に呼び出されるメソッドのことである。サンプルでは「MyCallback」というメソッド名にしている((5))。

private static void MyCallback(IAsyncResult ar) // (5)
{
  ……
}

 コールバック・メソッドでは、戻り値をvoid、パラメータをIAsyncResult型とする必要がある。つまりコールバック・メソッドは、IAsyncResultインターフェイス(System名前空間)のオブジェクトをパラメータとして受け取るわけである。MyCallbackメソッドの中身については後ほど説明する。

 これで準備はほぼ完了である。デリゲートのBeginInvokeメソッドを呼び出すことによって、別スレッドの処理を開始する((4))。

threadMethodDelegate.BeginInvoke(".", new AsyncCallback(MyCallback), DateTime.Now); // (4)

 BeginInvokeメソッドのパラメータは、

  • デリゲートを通じて呼び出すメソッドへのパラメータ(複数ある場合は複数指定する)
  • AsyncCallbackデリゲートでラッピングしたコールバック・メソッド
  • ステート

の順で指定する。具体例はサンプル・プログラムを見ていただきたい。

 AsyncCallbackデリゲートは、コールバックしたいメソッドをコンストラクタのパラメータに指定して、そのインスタンスを作成する。コールバック・メソッドを利用しない場合には、AsyncCallbackデリゲートのインスタンスを作成せずに、単に「null」を指定すればよい。

 最後のパラメータであるステートには、object型のオブジェクトを自由に指定できる。指定したオブジェクトはコールバック・メソッドのパラメータやBeginInvokeメソッドの戻り値であるIAsyncResultオブジェクト内に保持されている。今回はBeginInvokeメソッドの呼び出し時刻を指定しているが、使用しない場合はnullとしても構わない。

 すでに説明したように、デリゲートによるマルチスレッド処理では、コールバック・メソッドを指定することによって、別スレッドの処理終了時にアクションを起こすことができる。サンプルではThreadMethodメソッドの終了時に、コールバック・メソッドとして指定したMyCallbackメソッドが自動的に呼び出されるようになっている((5))。

 このMyCallbackメソッドでは、ThreadMethodメソッドの戻り値を取得できる。戻り値を取得するためには、デリゲート・オブジェクトに対してEndInvokeメソッドを呼び出す((6))。

DateTime result = threadMethodDelegate.EndInvoke(ar); // (6)

 EndInvokeメソッドの戻り値は、そのデリゲートの戻り値と一致する。つまりこのサンプルでは、ThreadMethodメソッドの戻り値であるDateTime型が、ThreadMethodメソッドのデリゲートであるThreadMethodDelegateオブジェクトのEndInvokeメソッドでの戻り値となる。

 EndInvokeメソッドには、コールバック・メソッドのパラメータとして渡されるIAsyncResultインターフェイスのオブジェクトを指定する。このオブジェクトには、スレッドの処理結果に関するいくつかの情報が格納される。サンプルでは、BeginInvokeメソッドの呼び出し時に「ステート」として格納しておいた処理開始時刻をIAsyncResultオブジェクトのAsyncStateプロパティにより取得している((7))。

DateTime beginTime = (DateTime)ar.AsyncState; // (7)

 なお、上述したようにBeginInvokeメソッドは戻り値としてIAsyncResultオブジェクトを返す。このオブジェクトにより、スレッドの進ちょく状況を監視することも可能だ(詳しくは、リファレンス・マニュアルの非同期プログラミングの概要などを参照していただきたい)。

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

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

メールマガジン登録

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