ThreadじゃなくTaskを使おうか?

  • 212
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

C#は、マルチコア時代、すなわち非同期時代とともに進化を遂げた言語です。
それはつまり、非同期処理の書きやすさを追従していると言うこと。

古いやり方が蔓延している

C# スレッドなんかでググると、古い記事が出てくるわけです。
そしてそこには、Threadクラスを使ったものが出てきます。

Thread thread1 = new Thread( new ThreadStart( method ) );

しかし、これ.NET 1.1時代のもので今となっては、よほどの理由がない限り、使いません。
初心者向けの古いサイトには、そういったやり方がいくつも蔓延しています。

なぜThreadを使ってはいけないのか?

コストが掛かる

スレッドの作成コスト

  • スレッドごとに必要なメモリの確保
  • プロセス内のDLL(モジュール)に通知し、それらのモジュールがDllMain関数を実行する

スレッドの切り替えコスト

マルチスレッドは、同時に実行しているのではなく、OSによってスレッドを切り替えながら
同時に実行しているように見せかけてます。

スレッドが切り替わるときに何をしているかというと・・・

  1. 実行中のスレッド:メモリ、CPUレジスタの待避
  2. 切り替え先のスレッド:メモリ、CPUレジスタの復元
制御が難しい

重たい処理を別スレッドで実行したとして、その結果を知る必要が出てきます。

  • 正常に終了したのか
  • 例外が発生して中断したのか
  • 何らかの理由でキャンセルされたのか

しかし、ThreadクラスやThreadPoolクラスでは、このような結果を通知しません。

買い物を頼んだら、何も言わず帰ってくるようなものです


async/awaitとTask

今時の主流は、async/awaitです。

Task

一般的に何時間もスレッドを占有するような処理は、ほとんどなく、
せいぜい数秒~数十秒程度の時間が掛かる処理ばかりです。

たかが数秒程度とはいえ、UIスレッド(操作画面)を止めることは、ユーザービリティ的に
よくありません。クリックしてもタップしても反応しないとなるとストレスになります。

一つ一つの処理をタスクと言います。

最も手軽に別スレッドで重たい処理をやらせる方法は、Task.Runを使用する方法です。
Task.Runを使用すると、タスクの作成から実行までやってくれます。

Task.Run( () => {
    Thread.Sleep( 5000 ); // 重たい処理のつもり
} );

async / await

スレッドの扱いが難しいと言われる理由のひとつに、UIは、UIスレッドからして触れないと言う制約があることです。
Windowsフォームでは、フォームやコントロールにInvokeメソッドがあって、それを通してデリゲートを実行することにより、UIスレッドに切り替えてUIに結果を反映させていました。

async/awaitとTaskを使うと、以下のように書けます。

public async void SampleAsync() {
    this.label.Text = "実行中";

    await Task.Run( () => {
        Thread.Sleep( 5000 );  // 重たい処理のつもり
    } );

    this.label.Text = "完了";
}

非同期メソッドには、ガイドラインとして~Asyncという接尾子を付けることになっていますが、
強制ではありません。

待機可能

VisualStudioで、Task.RunのRunの部分にマウスポインタを持って行くと、(待機可能)と表示されます。
待機可能ということは、それが非同期メソッドであり、awaitの対象なり得るということです。

async awaitの流れ

  1. 実行中のスレッドの同期コンテキストを覚えておく。
  2. Task.Runなどの非同期メソッドに処理を委譲
  3. awaitされた時点で、いったん実行中のメソッドから抜ける
  4. Task.Runなどの非同期メソッドが完了したら、1で保持した同期コンテキストに制御を戻す。
  5. awaitの続きから再開する

同期コンテキストとは、SynchronizationContextクラスのことで、特定のスレッドに処理を委譲するために使われる仕組みです。