.NETプログラミング研究 第25号 †
.NET Tips †
.NETのマルチスレッドプログラミング その7 †
別スレッドからフォーム、コントロールを扱う †
マルチスレッドプログラミングで注意すべき様々な問題について、第21号の「スレッドの同期」などで説明してきました。GUIのWindowsアプリケーションでは、これらに加えて、さらに気をつけるべきことがあります。
というのは、コントロール(フォームも含む)のメソッドは、そのコントロールを作成したスレッドからのみ呼び出される必要があるからです(注1)。(これは、Windowsフォームがシングルスレッドアパートメント(STA)モデルを使用しているためです。詳しくはヘルプの「マルチスレッド Windows フォーム コントロールのサンプル」等をご覧ください。)コントロールを作成したスレッド以外のスレッドからそのコントロールのメソッドを呼び出すには、マーシャリングにより、コントロールを作成したスレッドからメソッドを呼び出します。このマーシャリングには多くのリソースを消費しますが、これを確実且つ効率よく行う方法として、ControlクラスにはInvoke、BeginInvoke、およびEndInvokeメソッドが用意されています。
(注1:ただし、Invoke、BeginInvoke、EndInvoke、CreateGraphicsメソッドの呼び出しはスレッドセーフであり、その必要がありません。)
まずはInvokeメソッドの使用法を説明します。Invokeメソッドを使う際の手順は、
- コントロールのメソッドを呼び出すためのメソッドを作成する。
- そのメソッドに合ったデリゲートを宣言する。
- コントロール(あるいはコントロールの親フォーム)のInvokeメソッドをデリゲートオブジェクトを指定して呼び出す。
といったようになります。
次にInvokeメソッドを使った具体例を示します。ここでは、Button1をクリックすることにより新しいスレッドを作成し、このスレッドによりTextBox1に0から99までの数を順番に表示させています。なお、このコードはFormクラス内に書かれているものとします。
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
| |
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.Text = ""
Dim t As New Thread(New ThreadStart(AddressOf DoSomething))
t.IsBackground = True
t.Start()
End Sub
Private Sub DoSomething()
Dim dlg As New AddStringDelegate(AddressOf AddString)
Dim i As Integer
For i = 0 To 99
Dim str As String = i.ToString()
Dim count As Integer = _
CInt(TextBox1.Invoke(dlg, New Object() {str}))
Thread.Sleep(1000)
Next i
End Sub
Private Function AddString(ByVal str As String) As Integer
TextBox1.AppendText((str + ControlChars.CrLf))
Return TextBox1.Lines.Length
End Function
|
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
| |
private delegate int AddStringDelegate(string str);
private void Button1_Click(object sender, System.EventArgs e)
{
TextBox1.Text = "";
Thread t = new Thread(new ThreadStart(DoSomething));
t.IsBackground = true;
t.Start();
}
private void DoSomething()
{
AddStringDelegate dlg = new AddStringDelegate(AddString);
for (int i = 0; i < 100; i++)
{
string str = i.ToString();
int count = (int) TextBox1.Invoke(dlg, new object[] {str});
Thread.Sleep(1000);
}
}
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メソッドのみ示します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| | Private Sub DoSomething()
Dim dlg As New AddStringDelegate(AddressOf AddString)
Dim i As Integer
For i = 0 To 99
Dim str As String = i.ToString()
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
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| | private void DoSomething()
{
AddStringDelegate dlg = new AddStringDelegate(AddString);
for (int i = 0; i < 100; i++)
{
string str = i.ToString();
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メソッドのみ示し、それ以外は前と同じです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| | Private Sub DoSomething()
Dim dlg As New AddStringDelegate(AddressOf AddString)
Dim count As Integer
Dim i As Integer
For i = 0 To 99
Dim str As String = i.ToString()
If TextBox1.InvokeRequired Then
count = CInt(TextBox1.Invoke(dlg, New Object() {str}))
Else
count = CInt(dlg.DynamicInvoke(New Object() {str}))
End If
Thread.Sleep(1000)
Next i
End Sub
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| | private void DoSomething()
{
AddStringDelegate dlg = new AddStringDelegate(AddString);
int count;
for (int i = 0; i < 100; i++)
{
string str = i.ToString();
if (TextBox1.InvokeRequired)
count = (int) TextBox1.Invoke(dlg, new object[] {str});
else
count = (int) dlg.DynamicInvoke(new object[] {str});
Thread.Sleep(1000);
}
}
|
このようなコードを書くことにより、DoSomethingメソッドはどのスレッドから呼び出しても適切にInvokeの使用を切り替えることができるようになります。
補足: ここでは
TextBox1.Invoke
としましたが、 TextBox1のあるフォームのInvokeメソッドを呼び出しても結構です(むしろその方が良いケースも多いでしょう)。この場合、上記のコードでは、
this.Invoke (VB.NETでは、Me.Invoke)
となります。
参考:
スレッドへデータを渡す、スレッドからデータを取得する †
スレッドプールを使ってスレッドを開始する方法では、スレッドとデータのやり取りをする方法が用意されていますが、Thread.Startメソッドでスレッドを開始した場合、そのスレッドへデータを渡したり、スレッドからデータを取得することは簡単ではありません。なぜなら、Thread.Startメソッドに必要なThreadStartデリゲートには引数も戻り値もありませんので、引数や戻り値を持つメソッドによりスレッドを開始することは出来ないからです。そこで、別の方法を考える必要があります。
まず考えられるのが、データを渡す側、取得する側両方のスレッドからアクセスできる変数を使ってデータのやり取りするという方法です。
ただしこの方法でマルチスレッドメソッドから戻り値を取得するには、戻り値を返すスレッドがその結果を変数に確実に代入したことをその戻り値を取得するスレッドの側で知る必要があります。下に示したサンプルコードでは、Joinメソッドを使って戻り値を返すスレッドが終わるまで待機してからその結果を取得しています。
このコードでは、MyThreadメソッドを別スレッドで開始し、sleepTimeフィールドを介してMyThreadメソッドにデータを渡し、returnValueフィールドを介してMyThreadメソッドの結果を渡すようにしています。
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
| | Class MainClass
Public Shared Sub Main()
sleepTime = 10000
Dim t As New System.Threading.Thread( _
New System.Threading.ThreadStart(AddressOf MyThread))
t.Start()
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("スレッド開始")
System.Threading.Thread.Sleep(sleepTime)
Console.WriteLine("終わりました")
returnValue = System.Environment.TickCount - start
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
| | class MainClass
{
public static void Main()
{
sleepTime = 10000;
System.Threading.Thread t =
new System.Threading.Thread(
new System.Threading.ThreadStart(MyThread));
t.Start();
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("スレッド開始");
System.Threading.Thread.Sleep(sleepTime);
Console.WriteLine("終わりました");
returnValue = System.Environment.TickCount - start;
}
}
|
ヘルプ「マルチスレッド プロシージャのパラメータと戻り値」では、別スレッドとして実行するメソッドはクラスにラップし、パラメータとして機能するフィールドをそのクラスに定義するのが「最善の方法」であると述べられています。これに従って先のコードを書き直すと次のようになります。
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
| | Class SampleClass
Public ReturnValue As Integer
Public SleepTime As Integer
Public Sub MyThread()
Dim start As Integer = System.Environment.TickCount
Console.WriteLine("スレッド開始")
System.Threading.Thread.Sleep(SleepTime)
Console.WriteLine("終わりました")
ReturnValue = System.Environment.TickCount - start
End Sub
End Class
Class MainClass
Public Shared Sub Main()
Dim sample As New SampleClass
sample.SleepTime = 10000
Dim t As New System.Threading.Thread( _
New System.Threading.ThreadStart(AddressOf sample.MyThread))
t.Start()
t.Join()
Console.WriteLine(sample.ReturnValue)
Console.ReadLine()
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
| | class SampleClass
{
public int ReturnValue;
public int SleepTime;
public void MyThread()
{
int start = System.Environment.TickCount;
Console.WriteLine("スレッド開始");
System.Threading.Thread.Sleep(SleepTime);
Console.WriteLine("終わりました");
ReturnValue = System.Environment.TickCount - start;
}
}
class MainClass
{
public static void Main()
{
SampleClass sample = new SampleClass();
sample.SleepTime = 10000;
System.Threading.Thread t =
new System.Threading.Thread(
new System.Threading.ThreadStart(sample.MyThread));
t.Start();
t.Join();
Console.WriteLine(sample.ReturnValue);
Console.ReadLine();
}
}
|
別スレッドから戻り値を取得するための方法としては、コールバックメソッドを使う方法もあります。この場合は上記の例のようにJoinメソッドでスレッドの終了を待機する必要がありません。この方法では、データを渡すスレッドからデータを受け取るスレッドのメソッドをデリゲート(またはイベント)により呼び出し、その引数として結果を渡します。
次の例は上記のコードをさらに書き換え、別スレッドで実行されるSpendLongTimeメソッドからコールバックメソッドGetCallbackResultを呼び出すことにより結果を返すようにしています。なお当然ですが、GetCallbackResultはメインスレッドではなく、別スレッドで実行されることに注意してください。
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
| | 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("スレッド開始")
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()
Dim sample As New SampleClass(10000, _
New SampleClass.MyThreadCallback(AddressOf GetCallbackResult))
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
|
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 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("スレッド開始");
System.Threading.Thread.Sleep(sleepTime);
Console.WriteLine("終わりました");
if (callbackDelegate != null)
callbackDelegate(System.Environment.TickCount - start);
}
}
class MainClass
{
public static void Main()
{
SampleClass sample = new SampleClass(10000,
new SampleClass.MyThreadCallback(GetCallbackResult));
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);
}
}
|
コメント †