mcommit's message

大阪でソフトウェア開発の仕事をしている simotinといいます。記事の内容でご質問やご意見がありましたらお気軽にコメントしてください\^o^/

C# Task async, await の使い方

ちょっとこちらの記事でも少し触れましたが、モダンなC#の並列処理の書き方についてきちんと勉強してみました。

mcommit.hatenadiary.com


■目次

きっかけ

上記の、WebAPIの記事でも触れましたが、C#4.0で登場したTaskというクラスの存在やawaitというキーワードについては名前は知ってはいました。
知ってはいたのですがどういう場合にどう使うのかは理解しておらず、

非同期だぁ~!?男は黙って同期だこの野郎!

という現実逃避を長いこと続けていました。

最近になって、C#でDropboxAPIを使う事になりドキュメントやサンプルコードを眺めていると非同期処理のオンパレードな実装をしないといけないようで、厳しい現実を突き付けられ、しぶしぶ非同期処理について勉強してみたという次第です。


一般的な実装方法

Taskクラスは.Net framework4からサポートされたクラスで、並列処理を簡単に実装するのに便利なクラスだそうです。
Taskクラスを使う際は、FactoryプロパティからTaskFactoryオブジェクトを取得し、StartNewというメソッドで実行したい処理を別スレッドで生成・開始するのが一般的な実装方法のようです。

using System;
using System.Threading.Tasks;
using System.Threading;

namespace async
{
    class Program
    {
        static int count = 1;
        static void Main(string[] args)
        {
            Task task = Task.Factory.StartNew(() => {
                Console.WriteLine(count.ToString() + "!");
                count++;
                Thread.Sleep(1000);
                Console.WriteLine(count.ToString() + "!");
                count++;
                Thread.Sleep(1000);
                Console.WriteLine(count.ToString() + "!");
                count++;
                Thread.Sleep(1000);
                Console.WriteLine("だ~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
                Thread.Sleep(1000);
            });

            // メインスレッドが先に終わらないように・・・
            while (true) ;
        }
    }
}

=> ラムダ式が分からんという人のために

上記のコードで、

            Task task = Task.Factory.StartNew(() => { 
            // ・・・以下省略・・・

という箇所が出てきましたが、 => はラムダ演算子という演算子です。

この機能はC#3.0で登場そうですが、デリゲートをその場で定義するための機能だそうです。

Javascriptjqueryajax呼び出しのonSuccessとかonErrorのコールバックをその場で書いたりするアレと同じノリですね。


一般的には無名メソッドと呼ばれるような機能は便利ですが、個人的にはコードの可読性が落ちる場合があるのであんまり好きじゃないやつです。
コールバックとかをその場で書いちゃうと、処理が短ければいいと思うのですが、長くなるとどっちが本質のコードなのかが分からなくなるんですよね。C言語の関数ポインタくらいがちょうどいいと感じます。

話はそれましたが、非同期処理ではその特性上、ラムダ演算子をよく使うことになりそうですので合わせて理解しないといけないですね・・・

同期はどうやってやるのか?

非同期ができれば合わせて同期したくなるのが人情です。
同期するのは簡単で単純にタスクのインスタンスWaitメソッドを呼び出してあげればいいようです。

猪木さんをあみんのようにいつまでも待ってあげるコードはこんな感じになります。

using System;
using System.Threading.Tasks;
using System.Threading;

namespace async
{
    class Program
    {
        static int count = 1;
        static void Main(string[] args)
        {
            Task task = Task.Factory.StartNew(() => {
                Console.WriteLine(count.ToString() + "!");
                count++;
                Thread.Sleep(1000);
                Console.WriteLine(count.ToString() + "!");
                count++;
                Thread.Sleep(1000);
                Console.WriteLine(count.ToString() + "!");
                count++;
                Thread.Sleep(1000);
                Console.WriteLine("だ~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
                Thread.Sleep(1000);
            });

            // 「だ~~~」が終わったタイミングで終了する
            task.Wait();
        }
    }
}


ちなみに、あみんのようにいつまでも待ってらんないわよという人は、

        // 1秒だけ待ってやる
        task.Wait(1000);

というように引数で時間を指定してあげる(ms単位)だけでよくて、時間が過ぎても相手が来なければさっさとタイムアウトしてくれます。

同期もいろいろ

同期させる方法は上記のインスタンスメソッドのWait()以外にもいろいろと提供されているようです。

メソッド名 内容
Task.WaitAll 引数で指定したタスク全てが終了するのを待つ Task.WaitAll(task1,task2,task3);
Task.WaitAny 引数で指定したタスクいずれかが終了するのを待つ Task.WaitAny(task1,task2,task3);

うんうん、分かり易くていいですね。スレッドが複数あった場合でも同期が簡単に実装できそうです。
Waitするタスクごとにタイムアウト時間を指定するオーバーロードも用意されています。

asyncとawaitの使い方

近頃のC#のコードにはasyncとawaitというキーワードがよく登場します。
※うろ覚えだけど、なんかAsyncTaskとか似たようなキーワードがandroidの開発でもあったような。。。

このasyncとawaitはどういったとき使うかがよく分かっていなかったのですが、この機能はC#5.0(VisualStdio2012)で追加された機能で、Taskクラスを使った並列処理を分かりやすく書くための機能になります。

使用例ですが、

using System;
using System.Threading.Tasks;
using System.Threading;

namespace async
{
    class Program
    {
        static async Task HelloWorldAsync()
        {
            Console.WriteLine("[Async] HelloWorldAsync Start...\n");
            await Task.Run(() => {
                Console.WriteLine("[Async] 1");
                Thread.Sleep(1000);
                Console.WriteLine("[Async] 2");
                Thread.Sleep(1000);
                Console.WriteLine("[Async] 3");
                Thread.Sleep(1000);
            });
            Console.WriteLine("[Async] HelloWorldAsync End...\n");
        }
        static void Main(string[] args)
        {
            Console.WriteLine("[Main] Start...\n");
            Task task = HelloWorldAsync();
            Console.WriteLine("[Main] HelloWorldAsync Called...\n");
            task.Wait();
            Console.WriteLine("[Main] End...\n");
            while (true) ;
        }
    }
}

asyncキーワードを指定したHelloWordAsyncは非同期メソッドと呼ばれるメソッドになるそうです。

awaitキーワードを指定した部分の処理がTaskを使った非同期処理になり別スレッドで実施されますが、非同期メソッド内の

Console.WriteLine("HelloWorldAsync End...\n");

の呼び出しは非同期処理が終わるまでは実行されません。

Console.Writelineの出力は以下のようになります。


[Main] Start...

[Async] HelloWorldAsync Start...

[Async] 1
[Main] HelloWorldAsync Called...

[Async] 2
[Async] 3
[Async] HelloWorldAsync End...

[Main] End...

という結果になります。

非同期処理→同期待ちという一連の処理を一つのメソッドにまとめて書けるのは分かり易くていいですね。

感想

こうしてきちんと勉強してみると、それほど難しい機能では無かったと思います。

今回の勉強では、パーフェクトC#を読んでみました。

改訂3版 パーフェクトC# (PERFECT SERIES 1)

改訂3版 パーフェクトC# (PERFECT SERIES 1)

非同期処理の機能についての記載はそこまで分量は割かれていませんでしたが分かりやすかったです。

余談

仕事でお客さんの会社とかで作業していると

C#ができる人が二人くらい欲しいって言われてる・・・」
C#を使う案件が・・・」
「○○さんは、C++はできるけどC#はやったことがないから」

みたいな会話がよく聞こえてきます。

C#(.Net)のようなスピードで言語が進化しているともはや一括りに、

C#ができる人」

と言ってしまうのはおかしいんじゃないかと思いました。

例えばコーディング規約とかを統一するのにも結構大変なんじゃないかなという気がしてきます。

AさんはC#バリバリで新機能とか含めてコーディングに対応できるけど、Bさんはそうでもない昔ながらのC#のコードを書いてしまうとかって出てきそう(というか既に起こってそうな気がします)

こういう場合にどこで折り合いをつけるのかとか判断しないといけないので話がややこしくなりますね。イケイケのプロジェクトあれば勉強熱心な人は有利だと思いますし、新機能は基本取り入れないよというようなプロジェクトであればプログラマーの数は集まるかもしれませんがC#好きな人からしたらつまらないプロジェクトになってしまうと思います。

言語の進化と採用については、プロジェクトのマネジャー層の人が責任を以て対応していかないといけない問題だと思いますがそういったとこまで把握できている人実際のところは少なそうな気がします。