┏第25号━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃         .NETプログラミング研究         ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ 〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜メニュー ■.NET Tips ・.NETのマルチスレッドプログラミング その7 - 別スレッドからフォーム、コントロールを扱う - スレッドへデータを渡す、スレッドからデータを取得する 〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜メニュー ─────────────────────────────── ■.NET Tips ─────────────────────────────── ●.NETのマルチスレッドプログラミング その7 ★別スレッドからフォーム、コントロールを扱う マルチスレッドプログラミングで注意すべき様々な問題について、第 21号の「スレッドの同期」などで説明してきました。GUIのWindowsア プリケーションでは、これらに加えて、さらに気をつけるべきことが あります。 というのは、コントロール(フォームも含む)のメソッドは、そのコ ントロールを作成したスレッドからのみ呼び出される必要があるから です(注1)。(これは、Windowsフォームがシングルスレッドアパー トメント(STA)モデルを使用しているためです。詳しくはヘルプの「マ ルチスレッド Windows フォーム コントロールのサンプル」等をご覧 ください。)コントロールを作成したスレッド以外のスレッドからそ のコントロールのメソッドを呼び出すには、マーシャリングにより、 コントロールを作成したスレッドからメソッドを呼び出します。この マーシャリングには多くのリソースを消費しますが、これを確実且つ 効率よく行う方法として、ControlクラスにはInvoke、BeginInvoke、 およびEndInvokeメソッドが用意されています。 ・マルチスレッド Windows フォーム コントロールのサンプル http://www.microsoft.com/japan/msdn/library/ja/cpguide/html/cpcondevelopingmultithreadedwindowsformscontrol.asp (注1:ただし、Invoke、BeginInvoke、EndInvoke、CreateGraphicsメ ソッドの呼び出しはスレッドセーフであり、その必要がありません。) まずはInvokeメソッドの使用法を説明します。Invokeメソッドを使う 際の手順は、 1.コントロールのメソッドを呼び出すためのメソッドを作成する。 2.そのメソッドに合ったデリゲートを宣言する。 3.コントロール(あるいはコントロールの親フォーム)のInvokeメソ ッドをデリゲートオブジェクトを指定して呼び出す。 といったようになります。 次にInvokeメソッドを使った具体例を示します。ここでは、Button1を クリックすることにより新しいスレッドを作成し、このスレッドによ りTextBox1に0から99までの数を順番に表示させています。なお、この コードはFormクラス内に書かれているものとします。 '[VB.NET]・・・・・・・・・・・・・・・・・・・・・・・・・・ 'Imports System.Threading 'が宣言されているものとする 'Invokeメソッドで使用するデリゲート Delegate Function AddStringDelegate(ByVal str As String) As Integer Private Sub Button1_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Button1.Click 'TextBox1の初期化 TextBox1.Text = "" 'スレッドの作成 Dim t As New Thread(New ThreadStart(AddressOf DoSomething)) t.IsBackground = True 'スレッドを開始する t.Start() End Sub '別スレッドで実行するメソッド Private Sub DoSomething() 'AddStringDelegateの作成 Dim dlg As New AddStringDelegate(AddressOf AddString) Dim i As Integer For i = 0 To 99 'TextBox1に追加する文字列 Dim str As String = i.ToString() 'TextBox1に文字列を追加する 'TextBox1は別スレッドが所有しているため、 'Invokeを使ってTextBox1を操作する Dim count As Integer = _ CInt(TextBox1.Invoke(dlg, New Object() {str})) '一秒停止 Thread.Sleep(1000) Next i End Sub 'TextBox1に文字列を追加する Private Function AddString(ByVal str As String) As Integer TextBox1.AppendText((str + ControlChars.CrLf)) Return TextBox1.Lines.Length End Function '・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //[C#]・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //using System.Threading; //が宣言されているものとする //Invokeメソッドで使用するデリゲート private delegate int AddStringDelegate(string str); private void Button1_Click(object sender, System.EventArgs e) { //TextBox1の初期化 TextBox1.Text = ""; //スレッドの作成 Thread t = new Thread(new ThreadStart(DoSomething)); t.IsBackground = true; //スレッドを開始する t.Start(); } //別スレッドで実行するメソッド private void DoSomething() { //AddStringDelegateの作成 AddStringDelegate dlg = new AddStringDelegate(AddString); for (int i = 0; i < 100; i++) { //TextBox1に追加する文字列 string str = i.ToString(); //TextBox1に文字列を追加する //TextBox1は別スレッドが所有しているため、 //Invokeを使ってTextBox1を操作する int count = (int) TextBox1.Invoke(dlg, new object[] {str}); //一秒停止 Thread.Sleep(1000); } } //TextBox1に文字列を追加する private int AddString(string str) { TextBox1.AppendText(str + "\r\n"); return TextBox1.Lines.Length; } //・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 上記の例では、TextBox1を作成したスレッドはメインスレッドですの で、それ以外のスレッド"t"からTextBox1の内容を変更するために、 Invokeメソッドを使用しています。Invokeメソッドを通じて AddStringメソッドを呼び出すことにより、AddStringメソッドは TextBox1を作成したメインメソッドで実行されるようになります。 次にBeginInvoke、EndInvokeメソッドを使ってみましょう。Invokeメ ソッドが同期的に実行するのに対して、BeginInvokeメソッドは非同期 的に実行します。EndInvokeメソッドはBeginInvokeメソッドにより実 行されたメソッドの返り値を取得するために使用します。 BeginInvokeメソッドを呼び出したときに非同期操作が終了していなけ れば、終了するまでブロックされます。 先ほどの例をBeginInvoke、EndInvokeメソッドを使って書き換えてみ ましょう。DoSomethingメソッド以外に変更はありませんので、 DoSomethingメソッドのみ示します。 '[VB.NET]・・・・・・・・・・・・・・・・・・・・・・・・・・ '別スレッドで実行するメソッド Private Sub DoSomething() 'AddStringDelegateの作成 Dim dlg As New AddStringDelegate(AddressOf AddString) Dim i As Integer For i = 0 To 99 'TextBox1に追加する文字列 Dim str As String = i.ToString() 'TextBox1に文字列を追加する 'TextBox1は別スレッドが所有しているため、 'Invokeを使ってTextBox1を操作する Dim ar As IAsyncResult = _ TextBox1.BeginInvoke(dlg, New Object() {str}) '返り値を取得する Dim count As Integer = CInt(TextBox1.EndInvoke(ar)) '一秒停止 Thread.Sleep(1000) Next i End Sub '・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //[C#]・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //別スレッドで実行するメソッド private void DoSomething() { //AddStringDelegateの作成 AddStringDelegate dlg = new AddStringDelegate(AddString); for (int i = 0; i < 100; i++) { //TextBox1に追加する文字列 string str = i.ToString(); //TextBox1に文字列を追加する //TextBox1は別スレッドが所有しているため、 //Invokeを使ってTextBox1を操作する IAsyncResult ar = TextBox1.BeginInvoke(dlg, new object[] {str}); //返り値を取得する int count = (int) TextBox1.EndInvoke(ar); //一秒停止 Thread.Sleep(1000); } } //・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 変わったのはInvokeの代わりにBeginInvokeとEndInvokeを使用してい る所だけです(BeginInvokeの後すぐにEndInvokeを呼び出しているた め、非同期の意味がありませんが)。BeginInvokeを呼び出したときに IAsyncResultオブジェクトを取得し、EndInvokeで返り値を取得するた めに使用します。返り値を取得する必要が無ければ、EndInvokeを呼び 出す必要はありません。 あるコントロールに対してInvokeメソッドを使ってメソッドを呼び出 す必要があるか調べる方法として、Control.InvokeRequiredプロパテ ィが用意されています。そのコントロールを作成したスレッドが InvokeRequiredを呼び出したスレッドと異なる場合はTrue、それ以外 はFalseを返します。 InvokeRequiredプロパティを調べ、Invokeメソッドを使う必要があれ ばInvokeを使い、そうでなければ使わないでメソッドを呼び出すコー ド例を以下に示します。ここでもDoSomethingメソッドのみ示し、それ 以外は前と同じです。 '[VB.NET]・・・・・・・・・・・・・・・・・・・・・・・・・・ '別スレッドで実行するメソッド Private Sub DoSomething() 'AddStringDelegateの作成 Dim dlg As New AddStringDelegate(AddressOf AddString) Dim count As Integer Dim i As Integer For i = 0 To 99 'TextBox1に追加する文字列 Dim str As String = i.ToString() 'TextBox1に文字列を追加する 'InvokeRequiredによりInvokeが必要か調べる If TextBox1.InvokeRequired Then 'Invokeが必要な時はInvokeによりデリゲートを実行 count = CInt(TextBox1.Invoke(dlg, New Object() {str})) Else 'Invokeが必要ないときはそのままデリゲートを実行 count = CInt(dlg.DynamicInvoke(New Object() {str})) End If '一秒停止 Thread.Sleep(1000) Next i End Sub '・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //[C#]・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //別スレッドで実行するメソッド private void DoSomething() { //AddStringDelegateの作成 AddStringDelegate dlg = new AddStringDelegate(AddString); int count; for (int i = 0; i < 100; i++) { //TextBox1に追加する文字列 string str = i.ToString(); //TextBox1に文字列を追加する //InvokeRequiredによりInvokeが必要か調べる if (TextBox1.InvokeRequired) //Invokeが必要な時はInvokeによりデリゲートを実行 count = (int) TextBox1.Invoke(dlg, new object[] {str}); else //Invokeが必要ないときはそのままデリゲートを実行 count = (int) dlg.DynamicInvoke(new object[] {str}); //一秒停止 Thread.Sleep(1000); } } //・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ このようなコードを書くことにより、DoSomethingメソッドはどのスレ ッドから呼び出しても適切にInvokeの使用を切り替えることができる ようになります。 補足: ここでは TextBox1.Invoke としましたが、 TextBox1のあるフォームのInvokeメソッドを呼び出し ても結構です(むしろその方が良いケースも多いでしょう)。この場 合、上記のコードでは、 this.Invoke (VB.NETでは、Me.Invoke) となります。 参考: ・コンポーネントのマルチスレッド http://www.microsoft.com/japan/msdn/library/ja/vbcon/html/vbconScalabilityMultithreadingInComponents.asp ・スレッドからのコントロールの操作 http://www.microsoft.com/japan/msdn/library/ja/vbcon/html/vbtskmanipulatingcontrolsfromthreads.asp ・チュートリアル : Visual Basic による簡単なマルチスレッド コン ポーネントの作成 http://www.microsoft.com/japan/msdn/library/ja/vbcon/html/vbconwalkthroughsimplemultithreadedcomponent.asp ・チュートリアル : Visual C# による簡単なマルチスレッド コンポー ネントの作成 http://www.microsoft.com/japan/msdn/library/ja/vbcon/html/vbwlkwalkthroughcreatingsimplemultithreadedcomponenetwithvisualc.asp ・スレッドからのコントロールの操作 http://www.microsoft.com/japan/msdn/library/ja/vbcon/html/vbtskmanipulatingcontrolsfromthreads.asp ・マルチスレッド Windows フォーム コントロールのサンプル http://www.microsoft.com/japan/msdn/library/ja/cpguide/html/cpcondevelopingmultithreadedwindowsformscontrol.asp ・フォームとコントロールでのマルチスレッド http://www.microsoft.com/japan/msdn/library/ja/vbcn7/html/vaconFreeThreadingWithFormsControls.asp ─────────────────────────────── ★スレッドへデータを渡す、スレッドからデータを取得する スレッドプールを使ってスレッドを開始する方法では、スレッドとデー タのやり取りをする方法が用意されていますが、Thread.Startメソッ ドでスレッドを開始した場合、そのスレッドへデータを渡したり、ス レッドからデータを取得することは簡単ではありません。なぜなら、 Thread.Startメソッドに必要なThreadStartデリゲートには引数も戻り 値もありませんので、引数や戻り値を持つメソッドによりスレッドを 開始することは出来ないからです。そこで、別の方法を考える必要が あります。 まず考えられるのが、データを渡す側、取得する側両方のスレッドか らアクセスできる変数を使ってデータのやり取りするという方法です。 ただしこの方法でマルチスレッドメソッドから戻り値を取得するには、 戻り値を返すスレッドがその結果を変数に確実に代入したことをその 戻り値を取得するスレッドの側で知る必要があります。下に示したサ ンプルコードでは、Joinメソッドを使って戻り値を返すスレッドが終 わるまで待機してからその結果を取得しています。 このコードでは、MyThreadメソッドを別スレッドで開始し、 sleepTimeフィールドを介してMyThreadメソッドにデータを渡し、 returnValueフィールドを介してMyThreadメソッドの結果を渡すように しています。 '[VB.NET]・・・・・・・・・・・・・・・・・・・・・・・・・・ Class MainClass 'エントリポイント Public Shared Sub Main() 'スレッドに渡すデータを設定する sleepTime = 10000 'Threadオブジェクトを作成する Dim t As New System.Threading.Thread( _ New System.Threading.ThreadStart(AddressOf MyThread)) 'スレッドを開始する t.Start() 'スレッドtが終わるまで待機する t.Join() '結果を表示する Console.WriteLine(returnValue) Console.ReadLine() End Sub 'スレッドが返す値 Private Shared returnValue As Integer 'スレッドに渡す値 Private Shared sleepTime As Integer '別スレッドで実行するメソッド Private Shared Sub MyThread() Dim start As Integer = System.Environment.TickCount '長い時間のかかる処理があるものとする Console.WriteLine("スレッド開始") 'sleepTimeミリ秒停止する System.Threading.Thread.Sleep(sleepTime) '処理が終わったことを知らせる Console.WriteLine("終わりました") '実際にかかった時間を返す returnValue = System.Environment.TickCount - start End Sub End Class '・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //[C#]・・・・・・・・・・・・・・・・・・・・・・・・・・・・ class MainClass { //エントリポイント public static void Main() { //スレッドに渡すデータを設定する sleepTime = 10000; //Threadオブジェクトを作成する System.Threading.Thread t = new System.Threading.Thread( new System.Threading.ThreadStart(MyThread)); //スレッドを開始する t.Start(); //スレッドtが終わるまで待機する t.Join(); //結果を表示する Console.WriteLine(returnValue); Console.ReadLine(); } //スレッドが返す値 private static int returnValue; //スレッドに渡す値 private static int sleepTime; //別スレッドで実行するメソッド private static void MyThread() { int start = System.Environment.TickCount; //長い時間のかかる処理があるものとする Console.WriteLine("スレッド開始"); //sleepTimeミリ秒停止する System.Threading.Thread.Sleep(sleepTime); //処理が終わったことを知らせる Console.WriteLine("終わりました"); //実際にかかった時間を返す returnValue = System.Environment.TickCount - start; } } //・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ ヘルプ「マルチスレッド プロシージャのパラメータと戻り値」では、 別スレッドとして実行するメソッドはクラスにラップし、パラメータ として機能するフィールドをそのクラスに定義するのが「最善の方法」 であると述べられています。これに従って先のコードを書き直すと次 のようになります。 ・マルチスレッド プロシージャのパラメータと戻り値 http://www.microsoft.com/japan/msdn/library/ja/vbcn7/html/vaconparametersreturnvaluesforfreethreadedprocedures.asp '[VB.NET]・・・・・・・・・・・・・・・・・・・・・・・・・・ 'マルチスレッドメソッドのためのクラス Class SampleClass 'スレッドが返す値 Public ReturnValue As Integer 'スレッドに渡す値 Public SleepTime As Integer '別スレッドで実行するメソッド Public Sub MyThread() Dim start As Integer = System.Environment.TickCount '長い時間のかかる処理があるものとする Console.WriteLine("スレッド開始") 'sleepTimeミリ秒停止する System.Threading.Thread.Sleep(SleepTime) '処理が終わったことを知らせる Console.WriteLine("終わりました") '実際にかかった時間を返す ReturnValue = System.Environment.TickCount - start End Sub End Class Class MainClass 'エントリポイント Public Shared Sub Main() 'SampleClassオブジェクトの作成 Dim sample As New SampleClass 'スレッドに渡すデータを設定する sample.SleepTime = 10000 'Threadオブジェクトを作成する Dim t As New System.Threading.Thread( _ New System.Threading.ThreadStart(AddressOf sample.MyThread)) 'スレッドを開始する t.Start() 'スレッドtが終わるまで待機する t.Join() '結果を表示する Console.WriteLine(sample.ReturnValue) Console.ReadLine() End Sub End Class '・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //[C#]・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //マルチスレッドメソッドのためのクラス class SampleClass { //スレッドが返す値 public int ReturnValue; //スレッドに渡す値 public int SleepTime; //別スレッドで実行するメソッド public void MyThread() { int start = System.Environment.TickCount; //長い時間のかかる処理があるものとする Console.WriteLine("スレッド開始"); //sleepTimeミリ秒停止する System.Threading.Thread.Sleep(SleepTime); //処理が終わったことを知らせる Console.WriteLine("終わりました"); //実際にかかった時間を返す ReturnValue = System.Environment.TickCount - start; } } class MainClass { //エントリポイント public static void Main() { //SampleClassオブジェクトの作成 SampleClass sample = new SampleClass(); //スレッドに渡すデータを設定する sample.SleepTime = 10000; //Threadオブジェクトを作成する System.Threading.Thread t = new System.Threading.Thread( new System.Threading.ThreadStart(sample.MyThread)); //スレッドを開始する t.Start(); //スレッドtが終わるまで待機する t.Join(); //結果を表示する Console.WriteLine(sample.ReturnValue); Console.ReadLine(); } } //・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 別スレッドから戻り値を取得するための方法としては、コールバック メソッドを使う方法もあります。この場合は上記の例のようにJoinメ ソッドでスレッドの終了を待機する必要がありません。この方法では、 データを渡すスレッドからデータを受け取るスレッドのメソッドをデ リゲート(またはイベント)により呼び出し、その引数として結果を 渡します。 次の例は上記のコードをさらに書き換え、別スレッドで実行される SpendLongTimeメソッドからコールバックメソッドGetCallbackResult を呼び出すことにより結果を返すようにしています。なお当然ですが、 GetCallbackResultはメインスレッドではなく、別スレッドで実行され ることに注意してください。 '[VB.NET]・・・・・・・・・・・・・・・・・・・・・・・・・・ 'マルチスレッドメソッドのためのクラス Class SampleClass 'スレッドが結果を返すためのコールバックデリゲート Delegate Sub MyThreadCallback(ByVal returnValue As Integer) Private callbackDelegate As MyThreadCallback 'スレッドに渡す値 Private sleepTime As Integer 'コンストラクタ Public Sub New(ByVal time As Integer, _ ByVal callback As MyThreadCallback) 'コンストラクタでデータを受け取る sleepTime = time callbackDelegate = callback End Sub '別スレッドで実行するメソッド Public Sub MyThread() Dim start As Integer = System.Environment.TickCount '長い時間のかかる処理があるものとする Console.WriteLine("スレッド開始") 'sleepTimeミリ秒停止する System.Threading.Thread.Sleep(sleepTime) '処理が終わったことを知らせる Console.WriteLine("終わりました") 'コールバックデリゲートを実行して結果を返す If Not (callbackDelegate Is Nothing) Then callbackDelegate((System.Environment.TickCount - start)) End If End Sub End Class Class MainClass 'エントリポイント Public Shared Sub Main() 'SampleClassオブジェクトの作成 '同時にデータを渡す Dim sample As New SampleClass(10000, _ New SampleClass.MyThreadCallback(AddressOf GetCallbackResult)) 'Threadオブジェクトを作成する Dim t As New System.Threading.Thread( _ New System.Threading.ThreadStart(AddressOf sample.MyThread)) 'スレッドを開始する t.Start() Console.ReadLine() End Sub 'スレッド終了時に呼び出されるコールバック関数 Private Shared Sub GetCallbackResult(ByVal returnValue As Integer) '結果を表示する Console.WriteLine(returnValue) End Sub End Class '・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //[C#]・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //マルチスレッドメソッドのためのクラス class SampleClass { //スレッドが結果を返すためのコールバックデリゲート public delegate void MyThreadCallback(int returnValue); private MyThreadCallback callbackDelegate; //スレッドに渡す値 private int sleepTime; //コンストラクタ public SampleClass(int time, MyThreadCallback callback) { //コンストラクタでデータを受け取る sleepTime = time; callbackDelegate = callback; } //別スレッドで実行するメソッド public void MyThread() { int start = System.Environment.TickCount; //長い時間のかかる処理があるものとする Console.WriteLine("スレッド開始"); //sleepTimeミリ秒停止する System.Threading.Thread.Sleep(sleepTime); //処理が終わったことを知らせる Console.WriteLine("終わりました"); //コールバックデリゲートを実行して結果を返す if (callbackDelegate != null) callbackDelegate(System.Environment.TickCount - start); } } class MainClass { //エントリポイント public static void Main() { //SampleClassオブジェクトの作成 //同時にデータを渡す SampleClass sample = new SampleClass(10000, new SampleClass.MyThreadCallback(GetCallbackResult)); //Threadオブジェクトを作成する System.Threading.Thread t = new System.Threading.Thread( new System.Threading.ThreadStart(sample.MyThread)); //スレッドを開始する t.Start(); Console.ReadLine(); } //スレッド終了時に呼び出されるコールバック関数 private static void GetCallbackResult(int returnValue) { //結果を表示する Console.WriteLine(returnValue); } } //・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ =============================== ■このマガジンの購読、購読中止、バックナンバー、説明に関しては  次のページをご覧ください。  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 - 2004 DOBON! All rights reserved. ===============================