• 追加された行はこの色です。
  • 削除された行はこの色です。
#title(.NETプログラミング研究 第23号)

#navi(.NETプログラミング研究)

#contents

*.NETプログラミング研究 第23号 [#tb7e7602]

**.NET Tips [#ubd0d0bf]

**.NETのマルチスレッドプログラミング その5 [#je1dbcec]

#column(注意){{
この記事の最新版は「[[.NETのマルチスレッドプログラミング>http://dobon.net/vb/dotnet/programing/multithread.html]]」で公開しています。
この記事の最新版は「[[.NETのマルチスレッドプログラミング>https://dobon.net/vb/dotnet/programing/multithread.html]]」で公開しています。
}}

***スレッドプールの使い方 [#qa6096b3]

(ここではスレッドプールの使い方について説明することが目的ですが、「スレッドプールとは何か」ということについて詳しくは述べません。ヘルプの「スレッド プーリング」「スレッド プール」等に詳しく書かれていますので、これらをご覧ください。

-[[スレッド プーリング>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回表示されます。

#code(vbnet){{
'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
}}

#code(csharp){{
//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クラスを作成し、使用しています。

#code(vbnet){{
'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
}}

#code(csharp){{
//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の使い方 [#a9e88d62]

続いては、ThreadPool.RegisterWaitForSingleObjectメソッドの使い方を紹介しましょう。

QueueUserWorkItemメソッドと比較した場合のRegisterWaitForSingleObjectメソッドの特徴は、WaitHandleを使って実行を待機できる点と、タイムアウトを指定できる点にあります。

RegisterWaitForSingleObjectメソッドは、指定したWaitHandleオブジェクトの状態をチェックし、非シグナル状態であれば、待機操作を登録します。そして、オブジェクトの状態がシグナル状態になると、デリゲートがワーカースレッドによって実行されます。(WaitHandleオブジェクトについては、前回説明しました。)

次の例ではRegisterWaitForSingleObjectメソッドで10個のデリゲートオブジェクトをスレッドプールのキューに登録しています。非シグナル状態のManualResetEventオブジェクトを指定しているため、登録されたタスクはすぐに実行されることはありませんが、エンターキーが押され、SetメソッドによりManualResetEventオブジェクトがシグナル状態になると、待機が解除され一斉に開始されます。

#code(vbnet){{
'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
}}

#code(csharp){{
//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回実行されます。

#code(vbnet){{
'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
}}

#code(csharp){{
//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);
	}
}
}}

結果は例えば次のようになります。

#pre{{
システム起動後の経過時間: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);
により、それ以上は実行されなくなります。

#code(vbnet){{
'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
}}

#code(csharp){{
//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);
	}
}
}}

**コメント [#pd1dbf34]
#comment

//これより下は編集しないでください
#pageinfo([[:Category/.NET]],2003-12-15 (月) 06:00:00,DOBON!,2010-03-21 (日) 01:11:33,DOBON!)

[ トップ ]   [ 新規 | 子ページ作成 | 一覧 | 単語検索 | 最終更新 | ヘルプ ]