スレッドプール
今まではThread
クラスのStart
メソッドを使ってスレッドを起動する方法を紹介してきました。しかし、このようにスレッドを作成するには少なからぬリソースが必要なため、スレッドの作成と廃棄を何回も繰り返すことは効率的ではありません。「スレッドプール」は、すでに作成された複数のスレッドをプールして使いまわすことにより、このような無駄を減らし、複数のスレッドを効率的に使う機能を提供します。
理屈はさておき、早速スレッドプールを使ったサンプルを「threadpool_01」を見てください。
Class Class1 'エントリポイント Public Shared Sub Main() Console.WriteLine("スタート") 'メソッドをスレッドプールのキューに追加する System.Threading.ThreadPool.QueueUserWorkItem( _ New System.Threading.WaitCallback( _ AddressOf DoSomething)) Console.WriteLine("Enterキーを押してください") Console.ReadLine() End Sub 'スレッドで実行するメソッド Private Shared Sub DoSomething(ByVal obj As Object) '何らかの処理があるものとする System.Threading.Thread.Sleep(10000) Console.WriteLine("終わりました") End Sub End Class
class Class1 { //エントリポイント public static void Main() { Console.WriteLine("スタート"); //メソッドをスレッドプールのキューに追加する System.Threading.ThreadPool.QueueUserWorkItem( new System.Threading.WaitCallback(DoSomething)); Console.WriteLine("Enterキーを押してください"); Console.ReadLine(); } //スレッドで実行するメソッド private static void DoSomething(object obj) { //何らかの処理があるものとする System.Threading.Thread.Sleep(10000); Console.WriteLine("終わりました"); } }
このアプリケーションを実行すると、
スタート Enterキーを押してください 終わりました
の順番で出力されます。「Enterキーを押してください」と表示されてすぐに[Enter]キーを押すと、アプリケーションはすぐに終了します。このことから、スレッドプールのスレッドはバックグラウンドスレッドであることが分かります。
このコードでは、ThreadPool
クラスのQueueUserWorkItem
メソッドにより、スレッドプールのキューにDoSomething
メソッドを追加しています。キューに置かれたメソッドは、スレッドプールで管理されたスレッドにより効率的に処理されていきます。
引数の指定
QueueUserWorkItem
メソッドの特徴の一つは、指定するメソッドがWaitCallback
デリゲートによってラップされるため、メソッドに引数(状態オブジェクト)を渡すことができることです。この特徴を生かせば、スレッドにデータを渡すことは言うまでもなく、結果を取得することもできます。
サンプル「threadpool_02」では、その一例を紹介しています。
'状態オブジェクトのためのクラス Class TaskInfo 'メソッドに渡すデータ Public StringValue As String 'メソッドからの返り値 Public Result As String 'コンストラクタ Public Sub New(ByVal str As String) StringValue = str Result = Nothing End Sub End Class Class Class1 'エントリポイント Public Shared Sub Main() 'TaskInfoオブジェクトを作成する Dim ti As New TaskInfo("こんにちは") 'メソッドをスレッドプールのキューに追加する 'この時、状態オブジェクトも指定する System.Threading.ThreadPool.QueueUserWorkItem( _ New System.Threading.WaitCallback( _ AddressOf WriteData), ti) '待機する System.Threading.Thread.Sleep(3000) '結果を表示 Console.WriteLine(ti.Result) Console.ReadLine() End Sub 'スレッドで実行するメソッド Private Shared Sub WriteData(ByVal state As Object) '渡されたデータを取得 Dim ti As TaskInfo = CType(state, TaskInfo) '取得したデータを表示 Console.WriteLine(ti.StringValue) '結果を格納 ti.Result = "さようなら" End Sub End Class
//状態オブジェクトのためのクラス class TaskInfo { //メソッドに渡すデータ public string StringValue; //メソッドからの返り値 public string Result; //コンストラクタ public TaskInfo(string str) { StringValue = str; Result = null; } } class Class1 { //エントリポイント public static void Main() { //TaskInfoオブジェクトを作成する TaskInfo ti = new TaskInfo("こんにちは"); //メソッドをスレッドプールのキューに追加する //この時、状態オブジェクトも指定する System.Threading.ThreadPool.QueueUserWorkItem( new System.Threading.WaitCallback(WriteData), ti); //待機する System.Threading.Thread.Sleep(3000); //結果を表示 Console.WriteLine(ti.Result); Console.ReadLine(); } //スレッドで実行するメソッド private static void WriteData(object state) { //渡されたデータを取得 TaskInfo ti = (TaskInfo) state; //取得したデータを表示 Console.WriteLine(ti.StringValue); //結果を格納 ti.Result = "さようなら"; } }
ここではスレッドにデータを渡して結果を取得するために、パラメータと戻り値として機能するフィールドを定義したクラス「TaskInfo」を作成し、そのオブジェクトを状態オブジェクトとしてメソッドに渡しています。
ThreadPool.QueueUserWorkItem
メソッドによりスレッドプールを使う手順をまとめると、次のようになります。
WaitCallback
デリゲートと同じシグネチャ(パラメータや戻り値の型などの定義)のメソッドを作成し、別スレッドで実行したい仕事をこの中に記述する。- データの授受を行う場合は、そのためのクラスを作成する。
WaitCallback
デリゲートオブジェクトを作成し、ThreadPool.QueueUserWorkItem
メソッドに渡す。データの授受を行う場合は、状態オブジェクトを作成し、これもQueueUserWorkItem
メソッドに渡す。
スレッドプールの制限
ThreadPool
オブジェクトは一つのプロセスに一つだけ存在します。スレッドプールが同時にアクティブにできるワーカースレッドの数は決まっており、既定では、1つのシステムプロセッサにつき25個となっています(注参照)。これ以上の数のメソッドをスレッドプールのキューに追加することもできますが、この場合キューに置かれたメソッドは別のメソッドが終了するまで実行されません。
スレッドプール内のワーカースレッドの最大数はThreadPool.GetMaxThreads
メソッドで、使用できるワーカースレッドの数はThreadPool.GetAvailableThreads
メソッドでそれぞれ取得できます。
サンプル「threadpool_03」を実行すると、スレッドプールで使用できるワーカースレッドの数が減っていき、最後には0になる様子が分かります。
Class Class1 'エントリポイント Public Shared Sub Main() 'スレッドプール内のワーカースレッドの最大数を表示 Dim workers, ios As Integer System.Threading.ThreadPool.GetMaxThreads(workers, ios) Console.WriteLine("ワーカースレッドの最大数:{0}", workers) Dim i As Integer For i = 0 To 49 'メソッドをスレッドプールのキューに追加する System.Threading.ThreadPool.QueueUserWorkItem( _ New System.Threading.WaitCallback( _ AddressOf DoSomething)) Next i Console.ReadLine() End Sub 'スレッドで実行するメソッド Private Shared Sub DoSomething(ByVal obj As Object) 'スレッドプールで使用できるワーカースレッドの数を表示 Dim workers, ios As Integer System.Threading.ThreadPool.GetAvailableThreads(workers, ios) Console.WriteLine _ ("使用できるワーカースレッドの数:{0}", workers) '何らかの処理があるものとする System.Threading.Thread.Sleep(10000) End Sub End Class
class Class1 { //エントリポイント public static void Main() { //スレッドプール内のワーカースレッドの最大数を表示 int workers, ios; System.Threading.ThreadPool. GetMaxThreads(out workers, out ios); Console.WriteLine("ワーカースレッドの最大数:{0}", workers); for (int i = 0; i < 50; i++) { //メソッドをスレッドプールのキューに追加する System.Threading.ThreadPool.QueueUserWorkItem( new System.Threading.WaitCallback(DoSomething)); } Console.ReadLine(); } //スレッドで実行するメソッド private static void DoSomething(object obj) { //スレッドプールで使用できるワーカースレッドの数を表示 int workers, ios; System.Threading.ThreadPool. GetAvailableThreads(out workers, out ios); Console.WriteLine ("使用できるワーカースレッドの数:{0}", workers); //何らかの処理があるものとする System.Threading.Thread.Sleep(10000); } }
スレッドプールにはこのような制限があるため、すべてのスレッドを使い切ってしまうと処理が滞るだけでなく、デッドロックの危険性も生じます。例えば、あるスレッドが別のメソッドの開始と終了を待つが、スレッドプールがいっぱいのためにそのメソッドが実行されないという場合にデッドロックとなります。
また、.NET Frameworkではスレッドプールをデリゲートによる非同期呼び出し、スレッドタイマ、System.Netソケット接続、非同期I/O完了などでも利用しています(非同期メソッドの解説で一部を紹介しています)。そのため、知らないうちにスレッドプールを予想以上に使用している恐れがあります。
以上のような理由から、短時間で終わるメソッドの実行にはスレッドプールを積極的に活用し、時間のかかるメソッドの実行にはThread.Start
を使うのが無難です。
一般的には、Thread.Start
よりもスレッドプール(非同期デリゲートを含む)を使った方法の支持率が圧倒的に高く、中には「Thread.Start
は使うな」と言う人さえいます。しかし逆にThread.Start
を好むという人もおり、結局のところは、それぞれの長所と短所を理解して使い分けるのが最もよい方法ということになりそうです。
非同期デリゲート
.NET Frameworkには、デリゲートを使ってメソッドを非同期的に呼び出す簡単な方法が用意されています。この「非同期デリゲート」を使用すれば、基本的にすべてのメソッドを非同期的に呼び出すことができます。しかも、パラメータや戻り値の受け渡しも簡単に行えます。
この非同期呼び出しにはスレッドプールが使われます。よって今まで紹介してきたマルチスレッドプログラミングやスレッドプールを扱う際の注意事項は、非同期デリゲートを使用する場合にも留意する必要があります。
サンプル「delegate_01」では、非同期デリゲートを使った簡単な例を示します。このサンプルでは、WriteString
メソッドを非同期的に呼び出しています。
Class Class1 '非同期で呼び出すメソッドと同じシグネチャのデリゲート Delegate Function WriteStringAsyncDelegate( _ ByVal msg As String, ByVal sleepTime As Integer) As Integer 'エントリポイント Public Shared Sub Main() 'デリゲートオブジェクトの作成 Dim dlgt As New WriteStringAsyncDelegate _ (AddressOf WriteString) '非同期呼び出しを開始 Dim ar As IAsyncResult = _ dlgt.BeginInvoke("こんにちは", 5000, Nothing, Nothing) Console.WriteLine("WriteStringメソッドの終了を待機中...") '非同期処理の終了を待ち、結果を取得 Dim ret As Integer = dlgt.EndInvoke(ar) Console.WriteLine("結果:{0}", ret) Console.ReadLine() End Sub '非同期で呼び出すメソッド Private Shared Function WriteString(ByVal msg As String, _ ByVal sleepTime As Integer) As Integer System.Threading.Thread.Sleep(sleepTime) Console.WriteLine(msg) Return msg.Length End Function End Class
class Class1 { //非同期で呼び出すメソッドと同じシグネチャのデリゲート private delegate int WriteStringAsyncDelegate (string msg, int sleepTime); //エントリポイント public static void Main() { //デリゲートオブジェクトの作成 WriteStringAsyncDelegate dlgt = new WriteStringAsyncDelegate(WriteString); //非同期呼び出しを開始 IAsyncResult ar = dlgt.BeginInvoke("こんにちは", 5000, null, null); Console.WriteLine("WriteStringメソッドの終了を待機中..."); //非同期処理の終了を待ち、結果を取得 int ret = dlgt.EndInvoke(ar); Console.WriteLine("結果:{0}", ret); Console.ReadLine(); } //非同期で呼び出すメソッド private static int WriteString(string msg, int sleepTime) { System.Threading.Thread.Sleep(sleepTime); Console.WriteLine(msg); return msg.Length; } }
このプログラムを実行すると、
「WriteStringメソッドの終了を待機中...」と出力 (5秒間待機) 「こんにちは」と出力 「結果:5」と出力
となります。
WriteString
メソッドを非同期で呼び出すために、まずWriteString
メソッドと同じシグネチャのデリゲート「WriteStringAsyncDelegate
」を定義します。すると、適切なシグネチャを持つBeginInvoke
メソッドとEndInvoke
メソッドがこのデリゲートに自動的に定義されます。
このBeginInvoke
メソッドを呼び出すことにより、WriteString
メソッドの非同期呼び出しを開始できます。ここではBeginInvoke
の引数は4つとなり、その内2つはWriteString
メソッドに渡す引数を指定します。残りの2つ(上記の例ではnull
が指定されている)に関しては説明を省略します。
結果を取得するためには、BeginInvoke
が返すIAsyncResult
オブジェクトを指定して、EndInvoke
メソッドを呼び出します。EndInvoke
メソッドはWriteString
メソッドが終了するまでスレッドをブロックします。なお、EndInvoke
メソッドは必ず呼び出さなくてはいけません。
メソッドの終了を知る
EndInvoke
メソッドを呼び出すとスレッドがブロックされますので、非同期処理が終了したタイミングでEndInvoke
を呼び出したいところです。ここからは、非同期的に呼び出したメソッドがいつ終わったか知る方法について説明します。MSDNライブラリの「非同期プログラミングの概要」では、次の4つの方法が紹介されています。
EndInvoke
メソッドで呼び出しが終了するまでブロックする。- 待機ハンドルを使って待機する。
IAsyncResult.IsCompleted
プロパティがTrue
になるまで待つ。- 呼び出しが終了したときにコールバックメソッドが実行されるようにする。
1.は、サンプル「delegate_01」で紹介した方法です。2.はサンプル「delegate_02」で、3.はサンプル「delegate_03」でその例を示すこととし、ここでは触れません。4.の方法のみを紹介します。
コールバックメソッドの使用
コールバックメソッドを使った例が、サンプル「delegate_04」です。これが通常もっともお勧めできる方法です。
Class Class1 '非同期で呼び出すメソッドと同じシグネチャのデリゲート Delegate Function WriteStringAsyncDelegate( _ ByVal msg As String, ByVal sleepTime As Integer) As Integer 'エントリポイント Public Shared Sub Main() 'デリゲートオブジェクトの作成 Dim dlgt As New WriteStringAsyncDelegate _ (AddressOf WriteString) '非同期呼び出しを開始 'コールバックメソッドを指定する 'デリゲートオブジェクトをコールバックメソッドから ' IAsyncResult.AsyncStateで取得できるようにする Dim ar As IAsyncResult = dlgt.BeginInvoke("こんにちは", _ 1000, New AsyncCallback(AddressOf CallbackMethod), dlgt) Console.ReadLine() End Sub '非同期で呼び出すメソッド Private Shared Function WriteString(ByVal msg As String, _ ByVal sleepTime As Integer) As Integer System.Threading.Thread.Sleep(sleepTime) Console.WriteLine(msg) Return msg.Length End Function 'コールバックメソッド Private Shared Sub CallbackMethod(ByVal ar As IAsyncResult) 'デリゲートオブジェクトの取得 Dim dlgt As WriteStringAsyncDelegate = _ CType(ar.AsyncState, WriteStringAsyncDelegate) 'EndInvokeを呼び出し、結果を取得 Dim ret As Integer = dlgt.EndInvoke(ar) '結果を表示 Console.WriteLine("結果:{0}", ret) End Sub End Class
class Class1 { //非同期で呼び出すメソッドと同じシグネチャのデリゲート private delegate int WriteStringAsyncDelegate (string msg, int sleepTime); //エントリポイント public static void Main() { //デリゲートオブジェクトの作成 WriteStringAsyncDelegate dlgt = new WriteStringAsyncDelegate(WriteString); //非同期呼び出しを開始 //コールバックメソッドを指定する //デリゲートオブジェクトをコールバックメソッドから // IAsyncResult.AsyncStateで取得できるようにする IAsyncResult ar = dlgt.BeginInvoke("こんにちは", 1000, new AsyncCallback(CallbackMethod), dlgt); Console.ReadLine(); } //非同期で呼び出すメソッド private static int WriteString(string msg, int sleepTime) { System.Threading.Thread.Sleep(sleepTime); Console.WriteLine(msg); return msg.Length; } //コールバックメソッド private static void CallbackMethod(IAsyncResult ar) { //デリゲートオブジェクトの取得 WriteStringAsyncDelegate dlgt = (WriteStringAsyncDelegate) ar.AsyncState; //EndInvokeを呼び出し、結果を取得 int ret = dlgt.EndInvoke(ar); //結果を表示 Console.WriteLine("結果:{0}", ret); } }
コールバックメソッドは、非同期処理が終了すると実行されます。つまりこの例では、WriteString
メソッドが終了すると、CallbackMethod
メソッドが呼び出されます。
この例では状態オブジェクトとして、WriteStringAsyncDelegate
デリゲートオブジェクト(dlgt
)を指定しています。これを使うことにより、CallbackMethod
では適切なデリゲートオブジェクトのEndInvoke
メソッドを呼び出すことができます。
状態オブジェクトに別の値を指定したいときは、スレッドプールの例のように、状態オブジェクトのためのクラスを用意し、そのフィールドにデリゲートオブジェクトやその他の値を保存するようにします。
コールバックメソッドは、AsyncCallback
デリゲートと同じシグネチャである必要があります。また、コールバックメソッドはスレッドプールのスレッドで実行されることに注意してください。
非同期デリゲートを使用して非同期処理を行う手順をまとめると、次のようになります。
- 非同期で呼び出したいメソッドと同じシグネチャのデリゲートを宣言する。
- デリゲートオブジェクトを作成する。
- メソッドへの引数を指定して、デリゲートオブジェクトの
BeginInvoke
メソッドを呼び出す。コールバックメソッドを使用するときは、AsyncCallback
と状態オブジェクトも指定する。この時、状態オブジェクトとしてデリゲートオブジェクトを指定しておくと、コールバックメソッドからEndInvoke
メソッドを呼び出す時に楽である。 BeginInvoke
メソッドが返すIAsyncResult
オブジェクトを指定して、EndInvoke
メソッドを呼び出し、メソッドからの戻り値を取得する。
非同期メソッド
.NET Frameworkには、「Begin...」で始まるメソッドと、それと対になる「End...」で始めるメソッドが幾つかあります。このうち、例えば、
Stream.BeginRead
BeginWrite、Socket.BeginReceive
WebRequest.BeginGetResponse
MessageQueue.BeginReceive
などのメソッドは、非同期操作で使用されます。このような非同期操作で使用されるメソッドはスレッドプールが使用され、終了時に呼び出されるコールバックメソッドはスレッドプールのスレッドで実行されます。つまり、「非同期デリゲート」とほぼ同じと使い方をし、注意点も同様です。
また、次のようなイベントのイベントハンドラなども、スレッドプールのスレッドで呼び出されます。
EventLog
クラスのEntryWritten
イベントFileSystemWatcher
クラスのChanged
、Created
、Deleted
、Renamed
イベントMessageQueue
クラスのReceiveCompleted
、PeekCompleted
イベントProcess
クラスのExited
イベントSystem.Timers.Timer
クラスのElapsed
イベント
しかし、これらのクラスではSynchronizingObject
オブジェクトにフォーム(またはコントロール)を設定することにより、そのフォームを作成したスレッドで呼び出されるようになります(つまり、記事冒頭で紹介したマーシャリングが行われます)。これらのクラスはVS.NETの[ツールボックス]の[コンポーネント]内にあり、ここからフォームに貼り付けたときは、SynchronizingObject
プロパティとして自動的にそのフォームが設定されますので、スレッドプールを意識することはないでしょう。