++C++; // 未確認飛行 C 連載:次世代技術につながるSilverlight入門 C#たんと学ぶ/わりと硬派なソフトウェア開発講座 第3回「Webアプリケーション」(前編)

Top総合 目次C# によるプログラミング入門

非同期処理

このエントリーをはてなブックマークに追加

目次

キーワード

概要

注意: 2010年10月時点での CTP (community technology preview)版を元にした記事になっています。 製品版までに変更の可能性があります。 (async や await というキーワードも変更される可能性あり。)

Ver. 5.0

スレッドを使った非同期処理を行いたい動機としては、以下の2つが挙げられます。

  • 非ブロッキング処理: I/O 待ちとかで UI スレッドをフリーズさせないようにする
  • 並列処理: マルチコアを活かした並列処理でパフォーマンス向上

このうち、並列処理に関しては、Parallel クラスや Parallel LINQ で簡単に対応可能 (ラムダ式や LINQ を使えば、並列じゃない場合とほとんど変わらず書けます。 参考: 「[雑記] スレッド プールとタスク」 )。 一方の、非ブロッキング処理は、今までは結構面倒だったものの、 async/await の導入でかなり簡素化されることになります。

サンプル

要約 スライド資料

非ブロッキング処理、旧来的な書き方

URL 指定してダウンロードしてきた文字列をテキストボックスに表示という GUI アプリケーションを考えてみましょう。 同期的に書くなら、ボタンに対して以下のようなイベント ハンドラーを登録します。

private void Button_Click(object sender, RoutedEventArgs e)
{
    var client = new WebClient();
    var html = client.DownloadString(this.Url.Text);
    this.Output.Text = html;
}

このように同期でダウンロードを行うと、図1に示すように、ネットワークの通信速度が遅い環境では GUI がフリーズしてしまいます。 そこで、図2に示すように、非同期通信版を使って、UI スレッドをブロッキングしないようにします。

図1: 同期実行によって UI スレッドがブロックされる

図2: 非同期実行によって UI スレッドがブロックされないようにする

しかし、これまで、非同期呼び出しは少し面倒な書き方をする必要がありました。 いくつかのパターンがありますが、例えば、イベント非同期パターン(EAP: Event-based Asynchronous Pattern)と呼ばれるものの場合、以下のようになります。

private void Button_Click(object sender_, RoutedEventArgs e_)
{
    var client = new WebClient();
    client.DownloadStringCompleted += (sender, e) =>
    {
        this.Output.Text = e.Result;
    };
    client.DownloadStringAsync(new Uri(this.Url.Text));
}

以下のような面倒事が出てきています。

ダウンロード先が1個ならまだましで、例えば、複数の URL からダウンロードしてくる場合にはもっと複雑になります。

private void Button_Click(object sender, RoutedEventArgs e)
{
    var client = new WebClient();
    var urlList = this.Url.Text.Split(',');

    int i = -1;
    Action<DownloadStringCompletedEventArgs> a = null;

    client.DownloadStringCompleted += (sender, e) =>
    {
        var continuation = e.UserState as Action<DownloadStringCompletedEventArgs>;
        continuation(e);
    };

    a = e =>
    {
        if (e != null)
        {
            this.Output.Text += e.Result;
        }

        ++i;
        if (i >= urlList.Length)
        {
            return;
        }
        client.DownloadStringAsync(new Uri(urlList[i]), a);
    };

    this.Output.Text = string.Empty;
    a(null);
}

何番目までダウンロード完了したかを自前で状態管理しています。 やり方を知っていれば同期の場合のコードからこのような非同期コードを機械的な手順で書くこともできますが、 手間はかなりかかりますし、可読性は大きく下がります。

非同期メソッド

C# の新機能で、この手の非ブロッキング処理が簡単になりました。 (おそらく C# 5.0 で。今のところ、バージョンに関する明言なし。)

以下のように、async キーワードや await キーワードを使うことで、 同期っぽい書き方で非同期処理を記述できます。 比較のために、同期版と並べてみましょう。 (背景色を変えて強調表示している部分が同期版との差分です。 この部分を削除すればそのまま同期処理として動きます。)

private void Button_Click(object sender_, RoutedEventArgs e_)
{
    var client = new WebClient();
    var html = client.DownloadString(this.Url.Text);
    this.Output.Text = html;
}
private async void Button_Click(object sender_, RoutedEventArgs e_)
{
    var client = new WebClient();
    var html = await client.DownloadStringTaskAsync(this.Url.Text);
    this.Output.Text = html;
}

複雑な場合でも、ずいぶんと楽に書けるようになります。 前節の最後で書いた、複数の URL からダウンロードしてくる処理は以下のように書けます。

private async void Button_Click(object sender_, RoutedEventArgs e_)
{
    var client = new WebClient();
    var urlList = this.Url.Text.Split(',');

    this.Output.Text = string.Empty;

    foreach (var url in urlList)
    {
        var html = await client.DownloadStringTaskAsync(url);
        this.Output.Text += html;
    }
}

同期処理とほとんど同じ書き方ができます。

同期処理からの変更点

追加されたのは、async/await の2つのキーワードと、末尾に「TaskAsync」と付いた拡張メソッドです。

  • async(asynchronous: 非同期 の略)
    • メソッド内で await を使うために、メソッドを async キーワードで装飾します。
    • これ自体は単なる装飾で、コンパイル結果は通常のメソッドと変わりません。 (await という新キーワードが C# 4.0 以前のコードを破壊しないようにという意図のようです。)
    • async 修飾子の付いたメソッド(非同期メソッド)の戻り値の型は、 void、Task、もしくは、Task<T> のいづれかである必要があります。
  • await(「待つ」という意味)
    • await のところで、先物と継続(参考: 「[雑記] スレッド プールとタスク」 )を使って、 いったん別スレッドに制御を移した上で、タスク完了後に続きの処理を再開します。
    • await は「式」が書ける場所ならどこにでも書けます。 (foreach (var item in await task) とかも可能。)
    • await の直後には、Task クラス(もしくは、後述する“awaitable”なクラス)の値を与えます。
  • TaskAsync 拡張メソッド
    • await で非同期処理を行うためには、Task クラス(など)の値が必要なため、 WebClient などの既存のクラスに対する拡張メソッドとして、Task クラスを返すバージョンをライブラリ提供しています。
    • (CTP 版での情報。最終版では通常のメソッドとして追加される可能性もあります。)
    • 通例では、Task を返す非同期メソッドの名前は「Async」という語尾にします。 ただ、既存のクラスに関しては、すでに Async と付いたメソッドが存在している場合があるので、 この場合は「TaskAsync」という語尾を付けます。
非同期メソッドの戻り値の型

前述のとおり、非同期メソッドの戻り値の型は void、Task、もしくは、Task<T> のいずれかである必要があります。

まず、非同期処理を、最終的に Task.Wait メソッドで完了待ちする必要があるかどうかで戻り値の型選びます。 待つ必要があるなら Task もしくは Task<T> に、 必要なければ void にします。

(非同期でない)普通のメソッドから、(戻り値が Task 型の)非同期メソッドの完了を待つには以下のように書きます。

static void Main(string[] args)
{
    RunAsync().Wait();
}

static async Task RunAsync()
{
    await TaskEx.Delay(1000);
}

ただし、即座に Wait で完了待ちしてしまうと非同期にした意味があまりないので、 通常は、他の作業を並行して行ってから最後に Wait したり、 複数のタスクを同時実行したりします。

var task = RunAsync();
// 並行して別の処理
DoSomeTask();
task.Wait();
// 複数の処理を並行に実行
TaskEx.WhenAll(
    RunAsync(),
    RunAsync(),
    RunAsync()).Wait();

完了待ちが必要ない(戻り値が void)場合というのは、 例えば、GUI アプリケーションのイベント ハンドラーなどで利用します。

private async void Button_Click(object sender, RoutedEventArgs e)
{
    var client = new WebClient();
    var html = await client.DownloadStringTaskAsync(this.Url.Text);
    this.Output.Text = html;
}

余談: 実は必ずしも非同期ではない

async(非同期)や await(待つ)という名前に反して、 実は必ずしも非同期実行にはなりません。 というのも、Task クラスの値を await する際、タスクがすでに完了済み(IsCompleted プロパティが true)の可能性もあります。 この場合には、別にタスクを「待つ」必要はないので、 そのまま同期的に処理が続行します。

また、非同期であっても、必ずしもマルチスレッドで実行されるわけではありません。 async/await が Task クラスの上に成り立っているため、 可能な限り同じスレッドを使いまわそうとします。 (参考: 「スレッド プール」 )

余談2: 戻り値は Task

(書きかけ)

ITask インターフェイスとかにはしなかった。 Task 的なものの独自実装は結構危険。

↑この資料の9ページ目みたいな問題が。

なので、たぶん、インターフェイスや抽象クラスではなく、具象クラスである Task 固定に。

進捗報告とキャンセル処理

(書きかけ)

非同期メソッドの引数として、CancellationToke と IProgress を渡す。

キャンセル
CancellationToken を利用。
進捗報告

(参考: サンプルの ProgressSample プロジェクト。)

IProgress インターフェイスと EventProgress クラス

BackgroundWorker は、
  1. 非同期処理
  2. 進捗報告
  3. 完了通知
の3つの役目を1つのクラスで負ってて、機能の切り分けがあまりきれいじゃない。

async/await では、
  1. 非同期処理を同期っぽく書ける
  2. 進捗報告は IProgress インターフェイスを通して行う
  3. 完了通知は、同期っぽく、非同期メソッドの最後に書けばそれだけで OK。

[お問い合わせ](q)