TechFULの中の人

triple-four’s blog

やさC#で簡単な非同期処理1 ~Task.Run()~

こんにちは!魚介類のサバです。 最近、といっても数か月前ですが、C#の非同期処理が必要になって触った機会があったので、忘れないうちに出力したいと思います!

今回は 「別スレッドで処理を実行する」 ことのみが目的です。つまり並列処理です。
親切な解説ができるように努力します!!

※魚介類なので、浅い知識で書いています。ふわふわした解説や、ちょっとした間違いはご了承ください。

結論

Task.Run( ラムダ式 ) で別スレッド実行!

単純に別スレッドで実行をしたい場合は、名前空間 System.Threading.Tasks にある Task の staticメソッド、Runラムダ式C#的にはデリゲート)を渡すだけです。このラムダ式の戻り値の型は自由ですが、引数には何も受け取らないでください。

using System.Threading.Tasks;

static void Main(string[] args)
{
    
    Task.Run(()=>
    {
        // 別スレッドから標準出力に出力
        Console.WriteLine("I'm another thread");
    });

    // メインスレッドから標準出力に出力
    Console.WriteLine("I'm main thread");
}

"I'm another thread"が出力されない!?

以下のように書き換えてください。上記のままでは確率で出力しません。

Task.Run(()=>{
    Console.WriteLine("I'm another thread");
}).Wait();
  • 別スレッドが標準出力する前にメインスレッドの処理が終わり、プログラム自体が終わってしまうことで、別スレッドの文字列が出力されない場合があります。対策としてTask.Run()の戻り値が持つメソッドWait()を呼ぶことで、メインスレッドがハードウェイトして別スレッドの処理が終わるのを待ってくれます。

ラムダ式に戻り値を使いたい!

ラムダ式の戻り値はResultプロパティで受け取れます!

int result = Task.Run(() => 1).Result; // 1を返すラムダ式を別スレッドで実行し、戻り値を受け取る
  • Resultは内部で Wait() を呼び出し、別スレッドの処理が終わるのをハードウェイトした後に、結果を返してくる点に注意してください。

解説

必要なところだけつまんでください!

並列処理が分からない?

一つのプログラムが複数のスレッドを使って行う処理のことです。

  • もちろんたくさんスレッドを使ったほうが早くなりやすいですが、普通のプログラムはスレッドを一つだけ使います。 主な理由はスレッド同士でメモリ等リソースの割り当てが面倒だったり、その割り当て自体にコストがかかって性能が伸びづらかったりするためです。並列処理ソースコード内のランダムな場所が同時実行されるわけで、これで不具合が起きないようにする必要があるわけです。 (つまり並列処理はすごい面倒)

スレッドが分からない?

CPUはわかりますか?
パソコンの脳みそとして計算を行う部分です。実は最近のCPUは、一つに見えて複数個の脳みそを内部に持っています。スレッドはこのCPU内にある脳みそを指します。(適当)

  • より正確には、論理的な側面から見るとスレッド(OSから見える脳みそ)、物理的な側面から見るとコア(実際にある脳みそ)と呼ばれます。最近のCPUには、物理的に複数個ある脳みそを、さらに倍の個数に見せかけて高速化するものがあるため、これらは明確に区別されます。(多分)

ラムダ式が分からない?

即席で作られたメソッドです。カップ麺の親戚です。
わざわざ名前を付けるまでもない使い切りメソッドを、変数初期化のように定義でき、メソッドをさらに別のメソッドの引数とする際にもとても便利です。
(キャプチャ等もありますがここでは省略… )
(筆者は関数型言語が何たるかを全く理解できていません。ご容赦ください。)

C#ラムダ式( 引数リスト )=>{ 関数の中身 }と書きます。普通の関数定義の3倍くらい楽ですね!

static void Main(string[] args)
{

    // 「戻り値と引数がないメソッド用の型 (System.Action型) 」の変数にラムダ式を代入
    Action hello =
        () => 
        {
            Console.WriteLine("Hello Lambda!");
        };

    // ラムダ式を呼び出す
    hello();// Hello Lambda!


    // 「int引数一つを受け取りbool型を返すメソッド用の型 (System.Func<int, bool>型) 」の変数にラムダ式を代入
    Func<int, bool> IsEven =
        (int value) =>
        {
            if (value % 2 == 0) return true;
            else                return false;
        };

    // ラムダ式を呼び出す
    Console.WriteLine(IsEven(12)); // True
    Console.WriteLine(IsEven(11)); // False
}

ラムダ式は使いたくない!

そんなあなたでもキャストすれば使えます!

static void Main(string[] args)
{
    var tv = Task.Run( (Action)    funcv ); // 引数を受け取らず、何も返さないデリゲートにキャスト
    var ti = Task.Run( (Func<int>) funci ); // 引数を受け取らず、int型を返すデリゲートにキャスト

    tv.Wait();
    Console.WriteLine( ti.Result );
}

static void funcv()
{
    Console.WriteLine("haaai");
}

static int funci()
{
    return 100;
}

並列処理なんて使う?

確かに使用頻度はとても低いですし、競技プログラミングでは使用禁止です。
わかりやすい使用用途としては、ゲームなどの読み込み画面です。
多くの読み込み画面は、データをメモリに読み込む処理と、読み込みがどれほど進んだか表示する処理の二つを、別々のスレッドで処理しています。

  • 普通読み込み処理は、読み込むデータやストレージに応じて大きく左右され、終わる時間が予測できなかったり、安定感や処理速度に欠けます。つまり、読み込み処理を行うスレッドはカクカクしてしまうということです。そのため、ユーザがイラつくことなく待っていられるようなUIを提供するために、読み込み処理を別スレッドに任せられる並列処理が用いられることがあります。

少し遊ぼう

並列処理を感じる

for 文を別スレッドで回せば並列処理が体感できます。
処理の進め具合は各スレッドにお任せなので毎回出力結果が変わるはずです。

static void Main(string[] args)
{
    Task.Run(() => 
    {
        for (int i = 0; i < 1000; ++i)
        {
            Console.WriteLine("HO");
            Thread.Sleep(1);
        }
    });

    for (int i = 0; i < 1000; ++i)
    {
        Console.WriteLine("GEEEEE");
        Thread.Sleep(1);
    }
}
  • forループが速すぎてコンソール出力が追い付かないため、Thread.Sleep(1)を挟んでfor文の実行速度を落としています。

最後に

この記事ではC#の並列処理の基本中の基本、Task.Run()について触れてみました。
さすがC#!別スレッドで処理するだけならすごく簡単です!自分も驚きました。
「はぇ~そんなこともできるんだ~」って思ってもらえると嬉しいです!

追記

この記事を書いた当時の筆者は、非同期処理と,並列処理を曖昧に理解していました。そのため一部誤用があり、修正いたしましたのでご了承ください。

運営会社 / サービス

444株式会社

triple-four.com

TechFUL

procon.techful.jp