.NETプログラミング研究 第23号 †
.NET Tips †
.NETのマルチスレッドプログラミング その5 †
スレッドプールの使い方 †
(ここではスレッドプールの使い方について説明することが目的ですが、「スレッドプールとは何か」ということについて詳しくは述べません。ヘルプの「スレッド プーリング」「スレッド プール」等に詳しく書かれていますので、これらをご覧ください。
.NET Frameworkで新しいスレッドを作成し実行するには、ThreadクラスのStartメソッドを使うのが一般的です。しかし、短いタスクのスレッドを複数扱うには、スレッドプールを使うと効率的です。
スレッドプールのキューにメソッドを追加するには、ThreadPoolクラスのQueueUserWorkItemメソッドを呼び出します。タスクがキューに置かれると、スレッドプールは自動的にバックグラウンドスレッドを作成して、メソッドを実行します。
それでは実際にThreadPool.QueueUserWorkItemメソッドを使用する例を見てみましょう。この例は、"こんにちは。"と表示するだけのメソッドを10回スレッドプールのキューに置くだけのものです。結果はもちろん、"こんにちは。"と10回表示されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| |
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
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| |
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クラスを作成し、使用しています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
| |
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()
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
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
| |
class TaskInfo
{
public string StringValue;
public string Result;
public TaskInfo(string str)
{
StringValue = str;
}
}
class MainClass
{
public static void Main()
{
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」(両方とも同じ内容です)にその方法が紹介されています。
ThreadPool.RegisterWaitForSingleObjectの使い方 †
続いては、ThreadPool.RegisterWaitForSingleObjectメソッドの使い方を紹介しましょう。
QueueUserWorkItemメソッドと比較した場合のRegisterWaitForSingleObjectメソッドの特徴は、WaitHandleを使って実行を待機できる点と、タイムアウトを指定できる点にあります。
RegisterWaitForSingleObjectメソッドは、指定したWaitHandleオブジェクトの状態をチェックし、非シグナル状態であれば、待機操作を登録します。そして、オブジェクトの状態がシグナル状態になると、デリゲートがワーカースレッドによって実行されます。(WaitHandleオブジェクトについては、前回説明しました。)
次の例ではRegisterWaitForSingleObjectメソッドで10個のデリゲートオブジェクトをスレッドプールのキューに登録しています。非シグナル状態のManualResetEventオブジェクトを指定しているため、登録されたタスクはすぐに実行されることはありませんが、エンターキーが押され、SetメソッドによりManualResetEventオブジェクトがシグナル状態になると、待機が解除され一斉に開始されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| |
Class MainClass
Public Shared Sub Main()
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()
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
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| |
class MainClass
{
public static void Main()
{
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();
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回実行されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| |
Class MainClass
Public Shared Sub Main()
Dim ev As New AutoResetEvent(False)
ThreadPool.RegisterWaitForSingleObject( _
ev, _
New WaitOrTimerCallback(AddressOf PrintTime), _
Nothing, _
1000, _
False)
Dim i As Integer
For i = 0 To 9
Thread.Sleep(3000)
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
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| |
class MainClass
{
public static void Main()
{
AutoResetEvent ev = new AutoResetEvent(false);
ThreadPool.RegisterWaitForSingleObject(
ev,
new WaitOrTimerCallback(PrintTime),
null,
1000,
false
);
for (int i = 0; i < 10; i++)
{
Thread.Sleep(3000);
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);
により、それ以上は実行されなくなります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
| |
Class TaskInfo
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()
Dim ti As New TaskInfo("1")
Dim ev As New AutoResetEvent(False)
ti.Handle = ThreadPool.RegisterWaitForSingleObject( _
ev, _
New WaitOrTimerCallback(AddressOf PrintTime), _
ti, _
1000, _
False)
Console.ReadLine()
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
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
| |
class TaskInfo
{
public RegisteredWaitHandle Handle = null;
public string StringValue;
public TaskInfo(string str)
{
StringValue = str;
}
}
class MainClass
{
public static void Main()
{
TaskInfo ti = new TaskInfo("1");
AutoResetEvent ev = new AutoResetEvent(false);
ti.Handle = ThreadPool.RegisterWaitForSingleObject(
ev,
new WaitOrTimerCallback(PrintTime),
ti,
1000,
false
);
Console.ReadLine();
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);
}
}
|
コメント †