┏第23号━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃         .NETプログラミング研究         ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ 〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜メニュー ■.NET Tips ・.NETのマルチスレッドプログラミング その5 - スレッドプールの使い方 〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜メニュー ─────────────────────────────── ■.NET Tips ─────────────────────────────── ●.NETのマルチスレッドプログラミング その5 ★スレッドプールの使い方 (ここではスレッドプールの使い方について説明することを目的とし ていますが、「スレッドプールとは何か」ということについて詳しく は述べません。ヘルプの「スレッド プーリング」「スレッド プール」 等に詳しく書かれていますので、これらをご覧ください。 ・スレッド プーリング http://www.microsoft.com/japan/msdn/library/ja/cpguide/html/cpconthreadpooling.asp ・スレッド プール http://www.microsoft.com/japan/msdn/library/ja/vbcn7/html/vaconthreadpooling.asp ) .NET Frameworkで新しいスレッドを作成し実行するには、Threadクラ スのStartメソッドを使うのが一般的です。しかし、短いタスクのスレ ッドを複数扱うには、スレッドプールを使うと効率的です。 スレッドプールのキューにメソッドを追加するには、ThreadPoolクラ スのQueueUserWorkItemメソッドを呼び出します。タスクがキューに置 かれると、スレッドプールは自動的にバックグラウンドスレッドを作 成して、メソッドを実行します。 それでは実際にThreadPool.QueueUserWorkItemメソッドを使用する例 を見てみましょう。この例は、"こんにちは。"と表示するだけのメソ ッドを10回スレッドプールのキューに置くだけのものです。結果はも ちろん、"こんにちは。"と10回表示されます。 '[VB.NET]・・・・・・・・・・・・・・・・・・・・・・・・・・ 'Imports System.Threading 'が宣言されているものとする Class MainClass 'エントリポイント Public Shared Sub Main() Dim i As Integer For i = 0 To 9 'メソッドをスレッドプールのキューに追加する ThreadPool.QueueUserWorkItem( _ New WaitCallback(AddressOf MyProc)) Next i End Sub 'スレッドで実行するメソッド Private Shared Sub MyProc(ByVal state As Object) Console.WriteLine("こんにちは。") End Sub End Class '・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //[C#]・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //using System.Threading; //が宣言されているものとする class MainClass { //エントリポイント public static void Main() { for (int i = 0; i < 10; i++) { //メソッドをスレッドプールのキューに追加する ThreadPool.QueueUserWorkItem(new WaitCallback(MyProc)); } } //スレッドで実行するメソッド private static void MyProc(object state) { Console.WriteLine("こんにちは。"); } } //・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ QueueUserWorkItemメソッドの特徴の一つは、指定するメソッドが WaitCallbackデリゲートによってラップされるため、メソッドに引数 (状態オブジェクト)を渡すことができることです。この特徴を生か せば、スレッドにデータを渡すことは言うまでもなく、結果を取得す ることもできます。そのためには、パラメータと戻り値として機能す るフィールドを定義したクラスを作成し、そのオブジェクトを状態オ ブジェクトとしてメソッドに渡すようにします。 次に状態オブジェクトを利用してデータの授受を行う例を示します。 この例では、別のスレッドで実行するPrintDataメソッドにデータを渡 し、且つ結果を取得するために、TaskInfoクラスを作成し、使用して います。 '[VB.NET]・・・・・・・・・・・・・・・・・・・・・・・・・・ 'Imports System.Threading 'が宣言されているものとする '状態オブジェクトのためのクラス Class TaskInfo 'メソッドに渡すデータ Public StringValue As String 'メソッドからの返り値 Public Result As String 'コンストラクタ Public Sub New(ByVal str As String) StringValue = str End Sub End Class Class MainClass 'エントリポイント Public Shared Sub Main() 'TaskInfoオブジェクトを10個作る Dim tis(10) As TaskInfo Dim i As Integer For i = 0 To tis.Length - 1 tis(i) = New TaskInfo(i.ToString()) Next i For i = 0 To tis.Length - 1 'メソッドをスレッドプールのキューに追加する 'この時、状態オブジェクトも指定する ThreadPool.QueueUserWorkItem( _ New WaitCallback(AddressOf PrintData), tis(i)) Next i '待機する Console.ReadLine() '結果を表示 For i = 0 To tis.Length - 1 Console.WriteLine(tis(i).Result) Next i Console.ReadLine() End Sub 'スレッドで実行するメソッド Private Shared Sub PrintData(ByVal state As Object) '渡されたデータを取得 Dim ti As TaskInfo = CType(state, TaskInfo) '取得したデータを表示 Dim i As Integer For i = 0 To 999 Console.Write("{0}{1}", ti.StringValue, i) Next i '結果を格納 ti.Result = ti.StringValue + ":END" End Sub End Class '・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //[C#]・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //using System.Threading; //が宣言されているものとする //状態オブジェクトのためのクラス class TaskInfo { //メソッドに渡すデータ public string StringValue; //メソッドからの返り値 public string Result; //コンストラクタ public TaskInfo(string str) { StringValue = str; } } class MainClass { //エントリポイント public static void Main() { //TaskInfoオブジェクトを10個作る TaskInfo[] tis = new TaskInfo[10]; for (int i = 0; i < tis.Length; i++) { tis[i] = new TaskInfo(i.ToString()); } for (int i = 0; i < tis.Length; i++) { //メソッドをスレッドプールのキューに追加する //この時、状態オブジェクトも指定する ThreadPool.QueueUserWorkItem( new WaitCallback(PrintData), tis[i]); } //待機する Console.ReadLine(); //結果を表示 for (int i = 0; i < tis.Length; i++) { Console.WriteLine(tis[i].Result); } Console.ReadLine(); } //スレッドで実行するメソッド private static void PrintData(object state) { //渡されたデータを取得 TaskInfo ti = (TaskInfo) state; //取得したデータを表示 for (int i = 0; i < 1000; i++) Console.Write("{0}{1}", ti.StringValue, i); //結果を格納 ti.Result = ti.StringValue + ":END"; } } //・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ ThreadPoolオブジェクトは一つのプロセスに一つだけ存在し、 ThreadPool.QueueUserWorkItemなどによりメソッドがキューに追加さ れると作成されます。スレッドプールがプロセスで同時にアクティブ にできるスレッド(ワーカースレッド)の数は決まっており、既定で は、1つのシステムプロセッサにつき、25個となっています(注1)。 これ以上の数をスレッドプールに登録することもできますが、そのと きこのタスクは別のタスクが終了するまで実行されません。スレッド プール内のワーカースレッドの最大数はThreadPool.GetMaxThreadsメ ソッドで、追加で開始できるワーカースレッドの数はThreadPool. GetAvailableThreadsメソッドでそれぞれ取得できます。なお、.NET Frameworkはスレッドプーリングをデリゲートによる非同期呼び出し、 スレッドタイマ、System .Netソケット接続、非同期I/O完了などでも 使用しています。 スレッドプールはとても便利ですが、Threadクラスと違い、優先順位 などのプロパティを変えたり、AbortやSuspendなどを使って操作した りすることができず、さらに追加したタスクをキャンセルすることも できません。 なお、ThreadPoolクラスにはUnsafeQueueUserWorkItemというメソッド もあります。これは、セキュリティチェックの必要がない時に QueueUserWorkItemの代わりに使用でき、こちらを使った方がパフォー マンスが向上します。 (注1:この数は変更できるとヘルプにあります。しかし、実際に変更 するにはかなり大変みたいです。「The Code Project - Changing the default limit of 25 threads of ThreadPool class」または「C # Corner - Changing the default limit of 25 threads of ThreadPool class」(両方とも同じ内容です)にその方法が紹介され ています。 ・The Code Project - Changing the default limit of 25 threads of ThreadPool class http://www.codeproject.com/csharp/threadpool_limit.asp ・C# Corner - Changing the default limit of 25 threads of ThreadPool class http://www.c-sharpcorner.com/Code/2003/June/ThreadPoolLimit25.asp ) ★ThreadPool.RegisterWaitForSingleObjectの使い方 続いては、ThreadPool.RegisterWaitForSingleObjectメソッドの使い 方を紹介しましょう。 QueueUserWorkItemメソッドと比較した場合の RegisterWaitForSingleObjectメソッドの特徴は、WaitHandleを使って 実行を待機できる点と、タイムアウトを指定できる点にあります。 RegisterWaitForSingleObjectメソッドは、指定したWaitHandleオブジ ェクトの状態をチェックし、非シグナル状態であれば、待機操作を登 録します。そして、オブジェクトの状態がシグナル状態になると、デ リゲートがワーカースレッドによって実行されます。(WaitHandleオ ブジェクトについては、前回説明しました。) 次の例ではRegisterWaitForSingleObjectメソッドで10個のデリゲート オブジェクトをスレッドプールのキューに登録しています。非シグナ ル状態のManualResetEventオブジェクトを指定しているため、登録さ れたタスクはすぐに実行されることはありませんが、エンターキーが 押され、SetメソッドによりManualResetEventオブジェクトがシグナル 状態になると、待機が解除され一斉に開始されます。 '[VB.NET]・・・・・・・・・・・・・・・・・・・・・・・・・・ 'Imports System.Threading 'が宣言されているものとする Class MainClass 'エントリポイント Public Shared Sub Main() '非シグナル状態でManualResetEventを作成 Dim ev As New ManualResetEvent(False) Dim i As Integer For i = 0 To 9 'デリゲートをキューに追加する ThreadPool.RegisterWaitForSingleObject( _ ev, _ New WaitOrTimerCallback(AddressOf PrintTime), _ Nothing, _ -1, _ True) Next i '待機する Console.WriteLine("Enterキーを押してね") Console.ReadLine() 'ManualResetEventをシグナル状態にする ev.Set() Console.ReadLine() End Sub 'システム起動後の経過時間を表示 Private Shared Sub PrintTime(ByVal state As Object, _ ByVal timedOut As Boolean) Console.WriteLine("システム起動後の経過時間:{0}ミリ秒", _ System.Environment.TickCount) End Sub End Class '・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //[C#]・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //using System.Threading; //が宣言されているものとする class MainClass { //エントリポイント public static void Main() { //非シグナル状態でManualResetEventを作成 ManualResetEvent ev = new ManualResetEvent(false); for (int i = 0; i < 10; i++) { //デリゲートをキューに追加する ThreadPool.RegisterWaitForSingleObject( ev, new WaitOrTimerCallback(PrintTime), null, -1, true ); } //待機する Console.WriteLine("Enterキーを押してね"); Console.ReadLine(); //ManualResetEventをシグナル状態にする ev.Set(); Console.ReadLine(); } //システム起動後の経過時間を表示 private static void PrintTime(object state, bool timedOut) { Console.WriteLine("システム起動後の経過時間:{0}ミリ秒", System.Environment.TickCount); } } //・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ さらに、タイムアウトする時間を4番目の引数に指定して RegisterWaitForSingleObjectメソッドを呼び出すこともできます。こ のときは、WaitHandleがシグナル状態にならなくても、タイムアウト 間隔が経過するとデリゲートが実行されます。 また、RegisterWaitForSingleObjectメソッドの5番目の引数をfalseに すると、デリゲートが何回も実行できるようになります。よってタイ ムアウトとあわせて使うと、時間が経過するたびに何回もデリゲート が実行され、タイマのような働きをします。 WaitOrTimerCallbackデリゲートの2番目の引数から、そのメソッドが シグナル通知により実行されたか、タイムアウトにより実行されたか が分かります(タイムアウトにより実行された時はtrueとなります)。 次の例ではPrintTimeメソッドが1秒おきに実行されるようにしていま す。さらに、3秒間隔でWaitHandle.Setメソッドによっても、 PrintTimeが10回実行されます。 '[VB.NET]・・・・・・・・・・・・・・・・・・・・・・・・・・ 'Imports System.Threading 'が宣言されているものとする Class MainClass 'エントリポイント Public Shared Sub Main() '非シグナル状態でAutoResetEventを作成 Dim ev As New AutoResetEvent(False) 'デリゲートをキューに追加する '1秒おきに実行する ThreadPool.RegisterWaitForSingleObject( _ ev, _ New WaitOrTimerCallback(AddressOf PrintTime), _ Nothing, _ 1000, _ False) Dim i As Integer For i = 0 To 9 '3秒待機する Thread.Sleep(3000) 'AutoResetEventをシグナル状態にする ev.Set() Next i Console.ReadLine() End Sub 'システム起動後の経過時間を表示 Private Shared Sub PrintTime(ByVal state As Object, _ ByVal timedOut As Boolean) Console.WriteLine("システム起動後の経過時間:{0}ミリ秒({1})", _ System.Environment.TickCount, timedOut) End Sub End Class '・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //[C#]・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //using System.Threading; //が宣言されているものとする class MainClass { //エントリポイント public static void Main() { //非シグナル状態でAutoResetEventを作成 AutoResetEvent ev = new AutoResetEvent(false); //デリゲートをキューに追加する //1秒おきに実行する ThreadPool.RegisterWaitForSingleObject( ev, new WaitOrTimerCallback(PrintTime), null, 1000, false ); for (int i = 0; i < 10; i++) { //3秒待機する Thread.Sleep(3000); //AutoResetEventをシグナル状態にする ev.Set(); } Console.ReadLine(); } //システム起動後の経過時間を表示 private static void PrintTime(object state, bool timedOut) { Console.WriteLine("システム起動後の経過時間:{0}ミリ秒({1})", System.Environment.TickCount, timedOut); } } //・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 結果は例えば次のようになります。 システム起動後の経過時間:5140281ミリ秒(True) システム起動後の経過時間:5141272ミリ秒(True) システム起動後の経過時間:5142274ミリ秒(True) システム起動後の経過時間:5142274ミリ秒(False) システム起動後の経過時間:5143275ミリ秒(True) システム起動後の経過時間:5144277ミリ秒(True) システム起動後の経過時間:5145278ミリ秒(True) システム起動後の経過時間:5145278ミリ秒(False) システム起動後の経過時間:5146279ミリ秒(True) システム起動後の経過時間:5147281ミリ秒(True) ...(以下省略)... WaitHandleがシグナル状態になるまで、またはタイムアウトするまで 実行を待機するという待機操作は、RegisteredWaitHandle. Unregisterメソッドによりキャンセルすることができます。 Unregisterメソッドと、さらに状態オブジェクトを使った例を以下に 示します。この例では1秒おきにPrintTimeメソッドが実行されますが、 AutoResetEvent.Setメソッドによりシグナルを送ってPrintTimeメソッ ドを実行すると、 Unregister(null); により、それ以上は実行されなくなります。 '[VB.NET]・・・・・・・・・・・・・・・・・・・・・・・・・・ 'Imports System.Threading 'が宣言されているものとする '状態オブジェクトのためのクラス Class TaskInfo '待機操作のためのRegisteredWaitHandle Public Handle As RegisteredWaitHandle = Nothing 'メソッドに渡すデータ Public StringValue As String Public Sub New(ByVal str As String) StringValue = str End Sub End Class Class MainClass 'エントリポイント Public Shared Sub Main() 'TaskInfoオブジェクトの作成 Dim ti As New TaskInfo("1") '非シグナル状態でAutoResetEventを作成 Dim ev As New AutoResetEvent(False) 'デリゲートをキューに追加する '状態オブジェクトを指定して、1秒おきに実行する ti.Handle = ThreadPool.RegisterWaitForSingleObject( _ ev, _ New WaitOrTimerCallback(AddressOf PrintTime), _ ti, _ 1000, _ False) '待機 Console.ReadLine() 'AutoResetEventをシグナル状態にする ev.Set() Console.ReadLine() End Sub 'システム起動後の経過時間を表示 Private Shared Sub PrintTime(ByVal state As Object, _ ByVal timedOut As Boolean) '状態オブジェクトの取得 Dim ti As TaskInfo = CType(state, TaskInfo) '表示 Console.WriteLine("システム起動後の経過時間:{0}ミリ秒({1})", _ System.Environment.TickCount, timedOut) 'シグナルが送信されたら 'コールバックメソッドが実行されないように '待機操作をキャンセルする If Not timedOut And Not (ti.Handle Is Nothing) Then ti.Handle.Unregister(Nothing) End If End Sub End Class '・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //[C#]・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //using System.Threading; //が宣言されているものとする //状態オブジェクトのためのクラス class TaskInfo { //待機操作のためのRegisteredWaitHandle public RegisteredWaitHandle Handle = null; //メソッドに渡すデータ public string StringValue; public TaskInfo(string str) { StringValue = str; } } class MainClass { //エントリポイント public static void Main() { //TaskInfoオブジェクトの作成 TaskInfo ti = new TaskInfo("1"); //非シグナル状態でAutoResetEventを作成 AutoResetEvent ev = new AutoResetEvent(false); //デリゲートをキューに追加する //状態オブジェクトを指定して、1秒おきに実行する ti.Handle = ThreadPool.RegisterWaitForSingleObject( ev, new WaitOrTimerCallback(PrintTime), ti, 1000, false ); //待機 Console.ReadLine(); //AutoResetEventをシグナル状態にする ev.Set(); Console.ReadLine(); } //システム起動後の経過時間を表示 private static void PrintTime(object state, bool timedOut) { //状態オブジェクトの取得 TaskInfo ti = (TaskInfo) state; //表示 Console.WriteLine("システム起動後の経過時間:{0}ミリ秒({1})", System.Environment.TickCount, timedOut); //シグナルが送信されたら //コールバックメソッドが実行されないように //待機操作をキャンセルする if (!timedOut && ti.Handle != null) ti.Handle.Unregister(null); } } //・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ =============================== ■このマガジンの購読、購読中止、バックナンバー、説明に関しては  次のページをご覧ください。  http://www.mag2.com/m/0000104516.htm ■発行人・編集人:どぼん!  (Microsoft MVP for Visual Basic, Oct 2003-Oct 2004)  http://dobon.net  dobon_info@yahoo.co.jp ■ご質問等はメールではなく、掲示板へお願いいたします。  http://dobon.net/vb/bbs.html ■上記メールアドレスへのメールは確実に読まれる保障はありません  (スパム、ウィルス対策です)。メールは下記URLのフォームメール  から送信してください。  http://dobon.net/mail.html Copyright (c) 2003 DOBON! All rights reserved. ===============================