イベントハンドラで System.Threading.Tasks.Task を使う

#TLで指摘いただいたので、記事を追加しています。



ここひと月ほど、スレッドについて勉強してます。昨日 MSDN フォーラムに非同期に関するスレが立ったので System.Threading.Tasks.Task を使ったコードを提示してみたんですが、こちらにも備忘録で書いときます。


Form で Task を使う場合、以下のようになります。
コントロールは UIスレッドでないと操作できないため Invoke メソッドを使ってアクセスします。イベントハンドラ内で Task.Wait を呼び出すとアプリケーションがフリーズするため、完了時の処理は ContinueWith メソッドを使ってます。

Option Explicit On
Option Strict On

Imports System.Threading.Tasks

Public Class Form1
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Task.Factory.StartNew(
            Sub()
                Me.Invoke(Sub()
                              Me.Label1.Text = "実行中です。"
                              Me.Button1.Enabled = False
                          End Sub)
                System.Threading.Thread.Sleep(3000)
            End Sub).
        ContinueWith(
            Sub()
                Me.Invoke(Sub()
                              Me.Label1.Text = "終了しました。"
                              Me.Button1.Enabled = True
                          End Sub)
            End Sub)
    End Sub
End Class


WPF の場合。Forms と基本的に同じですが、非同期スレッドからコントロールにアクセスするには Dispatcher.Invoke メソッドを使います。また ViewModel なら Task.Wait メソッドを使えますが、WPF も Window のイベントハンドラで Task.Wait を呼び出すとフリーズするため、完了処理に ContinueWith を使ってます。

Option Explicit On
Option Strict On

Imports System.Threading.Tasks

Class MainWindow
    Private Sub Button1_Click(sender As Object, e As RoutedEventArgs) Handles Button1.Click
        Task.Factory.StartNew(
            Sub()
                Me.Dispatcher.Invoke(Sub()
                                         Me.TextBlock1.Text = "実行中です。"
                                         Me.Button1.IsEnabled = False
                                     End Sub)
                System.Threading.Thread.Sleep(5000)
            End Sub).
        ContinueWith(
            Sub()
                Me.Dispatcher.Invoke(Sub()
                                         Me.TextBlock1.Text = "終了しました。"
                                         Me.Button1.IsEnabled = True
                                     End Sub)
            End Sub)
    End Sub
End Class


Task のなにがいいのかってのは、スレッドプールを使うためスレッド数をハードに応じて自動調整してくれるし、System.Threading.Thread よりオーバーヘッドが少なく、待機や完了結果も取得可能、さらにキャンセルも可能で、タスク完了時に別タスクも起動でき、そのうえタスクをネストして使うことも可能という、極めて強力なクラスです。


#追記 2013/04/17 14:00
上のサンプルでは ContinueWith に渡すタスクで Dispatcher.Invoke 使ってますが、ContinueWith の第二引数に同期コンテキスト渡す方がが自然に思えると TL で指摘を頂きました。確かにこのタスクはコントロールしか操作しませんので 同期コンテキストタスクスケジューラー を渡した方が良さそうです。そこで TaskScheduler.FromCurrentSynchronizationContext を渡すよう実装してみました。
MSDN にも以下のように記述されてましたね(汗

同期コンテキストの指定
TaskScheduler.FromCurrentSynchronizationContext メソッドを使用すると、タスクが特定のスレッドで実行されるようにスケジュールできます。これは、Windows フォームや Windows Presentation Foundation などのフレームワークで役立ちます。これらのフレームワークでは、多くの場合、ユーザー インターフェイスオブジェクトへのアクセスが、その UI オブジェクトが作成されたスレッドで実行されているコードに制限されるからです。詳細については、「方法: ユーザー インターフェイス (UI) スレッドで作業をスケジュールする」を参照してください。

MSDN ライブラリ - タスク スケジューラ


また Task.Factory で開始するタスクも Dispatcher.Invoke 使ってますが、こちらはタスク呼び出す前にイベント内で直接実行すりゃいいじゃん。というわけで修正したコードです。確かにこの方がよりすっきりしてますね。

Option Explicit On
Option Strict On

Imports System.Threading.Tasks

Class MainWindow
    Private Sub Button1_Click(sender As Object, e As RoutedEventArgs) Handles Button1.Click
        Me.TextBlock1.Text = "実行中です。"
        Me.Button1.IsEnabled = False
        Task.Factory.StartNew(Sub() System.Threading.Thread.Sleep(5000)).
        ContinueWith(
            Sub()
                Me.TextBlock1.Text = "終了しました。"
                Me.Button1.IsEnabled = True
            End Sub, TaskScheduler.FromCurrentSynchronizationContext)
    End Sub
End Class

ちなみに書籍ですが、並列プログラミングに関しては プログラミング .NET Framework 第三版が一番詳しいです。170頁以上も使ってスレッドの基礎から徹底的に解説してるし、Task の解説だけでも15頁も割いてます。一冊7000円もする本ですが、値段以上の価値ありますよ。

プログラミング.NET FRAMEWORK 第3版 (Microsoft Press)

プログラミング.NET FRAMEWORK 第3版 (Microsoft Press)


概略だけなら.NET 開発テクノロジー入門にも書かれてます。概略とはいえ Task や Pallarel のサンプルや並列デバッガーの使い方までさくっと解説しています。

.NET開発テクノロジー入門 VISUAL STUDIO 2010対応版 (MSDNプログラミングシリーズ)

.NET開発テクノロジー入門 VISUAL STUDIO 2010対応版 (MSDNプログラミングシリーズ)


検索すれば、ネット上にもリソースが見つかります。以下の記事が詳しい。
関連記事タスク並列ライブラリ (TPL)
関連記事雑記 スレッド プールとタスク
関連記事TPL入門