佐々木屋

技術的なことから趣味まで色々書きます

非同期処理、マルチスレッド(余談:別スレッドからフォームコントロールを操作)

そもそもフォームもクラスですし、クラスにあるオブジェクトを別スレッドから操作は出来ません。フォーム自体がスレッドセーフではないから当然と言えば当然なんですが、非同期処理をやるとカウンタや進捗表示など、どうしてもフォームオブジェクトへアクセスしたくなる場面に出くわします。

ボタン1個、ラベル1個を用意したフォームアプリケーションを用意して下さい。
以下はやったらダメな例です。System.ThreadingをUsingして実行して下さい。

private void button1_Click(object sender, EventArgs e) {
    Thread testThread = new Thread(new ThreadStart(HeavyProc));
    testThread.Start();
} 
private void HeavyProc() {
    for (int i = 0; i <= 5; ++i) {
        Thread.Sleep(1000);
    }
    label1.Text = "終わったよ"; //←ここでInvalidOperationExceptionが発生
}
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    Dim testThread As New Thread(New ThreadStart(AddressOf HeavyProc))
    testThread.Start()
End Sub
Private Sub HeavyProc()
    For i As Integer = 0 To 5
        Thread.Sleep(1000)
    Next
    Label1.Text = "終わったよ" '←ここでInvalidOperationExceptionが発生
End Sub

メインスレッドでもラベルに対して操作出来る状況で、別スレッドから操作されたら駄目なのでエラーになります。


Invokeメソッドの利用

この問題を解決するにはいくつか方法がありますが、一番メジャーな方法としてはInvokeメソッドを利用することです。元々System.Windows.Forms名前空間には別スレッドからの操作用としてInvokeメソッドが提供されています。
コントロール操作をデリゲートで作成し、そのインスタンスInvokeメソッドへ渡せば実現可能となります。

//ラベル操作のデリゲート
delegate void labelnaiyo(string naiyo);
private void SetLabel(string naiyo) {
    label1.Text = naiyo;
}

private void button1_Click(object sender, EventArgs e) {
    Thread testThread = new Thread(new ThreadStart(HeavyProc));
    testThread.Start();
} 
private void HeavyProc() {
    for (int i = 0; i <= 5; ++i) {
        Thread.Sleep(1000);
    }
    Invoke(new labelnaiyo(SetLabel),"終わったよ"); //←デリゲートのインスタンスを渡す
}
’ラベル操作のデリゲート
Delegate Sub labelnaiyo(ByVal naiyo As String)
Private Sub SetLabel(ByVal naiyo As String)
    Label1.Text = naiyo
End Sub

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    Dim testThread As New Thread(New ThreadStart(AddressOf HeavyProc))
    testThread.Start()
End Sub
Private Sub HeavyProc()
    For i As Integer = 0 To 5
        Thread.Sleep(1000)
    Next
    Invoke(New labelnaiyo(AddressOf SetLabel), "終わったよ") '←デリゲートのインスタンスを渡す
End Sub

コード上は別スレッドに書かれていますが、デリゲート自体はメインスレッドで実行されますので、特に問題無くラベルに文字が表示されるようになります。

なお、System.Windows.Forms名前空間にはInvokeRequiredプロパティが提供されています。これはInvokeが必要なのかどうかをチェックするプロパティで、これを使ってInvokeメソッドの実装必要性を判断すると良いです。