xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

TPL入門 (20) - 非同期メソッド

落ち穂拾いも第4回です。今回はC# 5.0(仮)で搭載予定の、より簡易な非同期プログラミングについて見ていきます。今回の内容はこの記事を書いている段階でのものであり、正式リリース時には変更される可能性がありますので注意してください

C#の進化

これまでC#はバージョンを重ねるごとに着実に進化してきました。C# 2.0ではジェネリックの搭載、反復子のロジックを書きやすくするためのyieldなどの拡張、ローカル変数を利用可能な匿名メソッドの提供などが行われました。C# 3.0では型推論、ラムダ式、拡張メソッドなど、LINQを中心とした拡張が行われました。C# 4.0では動的オブジェクトモデルとの相互運用の強化のため、dynamic型が搭載されました。そして、次期バージョンであるC# 5.0では、非同期プログラミングをより容易に記述するためのasync修飾子/await演算子の追加が予定されています

去る2011年9月14日、Visual Studio 11 Developer Previewがリリースされました。その中にはC# 5.0および.NET Framework 4.5などの次期バージョンの機能が一部搭載/公開されており、async/awaitを利用した非同期メソッドの作成を試すことができます。

非同期メソッドのサンプル

まずはサンプルコードを見てみてください。async/awaitを付加することと、awaitに渡すためにTaskを利用している点を除けば、同期的なメソッドとほぼ同じ書き方ができることがわかります。

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
 
namespace WindowsFormsApplication
{
    public partial class MainForm : Form
    {
        public MainForm()
        {
            this.InitializeComponent();
            this.button.Click += this.Button_Click;
        }
 
        private async void Button_Click(object sender, EventArgs e)
        {
            this.button.Enabled = false;
            await this.HeavyWork();
            this.button.Enabled = true;
        }
 
        private Task HeavyWork()
        {
            return Task.Run(() => Thread.Sleep(3000));
        }
    }
}
using System;
using System.Threading;
using System.Windows.Forms;
 
namespace WindowsFormsApplication
{
    public partial class MainForm : Form
    {
        public MainForm()
        {
            this.InitializeComponent();
            this.button.Click += this.Button_Click;
        }
 
        private void Button_Click(object sender, EventArgs e)
        {
            this.button.Enabled = false;
            this.HeavyWork();
            this.button.Enabled = true;
        }
 
        private void HeavyWork()
        {
            Thread.Sleep(3000);
        }
    }
}

async修飾子

async修飾子.aspx)はメソッド内でawait演算子を利用するための修飾子です。async修飾子の付くメソッドの戻り値は、void、Task、Task<T>である必要があります。async修飾子はただの目印でしかなく、コンパイル結果は通常のメソッドと変わりません。asyncを付加する理由は、C# 4.0以前のコードとの互換性を取るため (参考 : C# 5の非同期、パート6: asyncはいずこへ?) と言われています。

async修飾子は「このメソッドをワーカースレッド上で非同期的に動作するよう指定する」という意味ではありません。async修飾子の意味は、「このメソッドは非同期操作を待つ必要がある制御フローを含んでおり、非同期処理の適切な時点でこのメソッドを再度途中から始められるようにコンパイラによって継続渡しに書き直される」です

await演算子

await演算子.aspx)はasync修飾子の付く非同期メソッドの中で1つ以上記述することができます。また、通常のメソッドだけでなく、匿名メソッド、ラムダ式など、式となる箇所ならどこにでも記述することができます。await演算子にはGetAwaiterメソッド (もしくはGetAwaiterという名前の拡張メソッド) を実装したAwaitableな型を渡すことができます。これはAwaitableパターンと呼ばれているらしく、その内容は非同期メソッドの内部実装に詳しく載っていますので参考にしてください。現在のところ、.NET Frameworkによって提供されている型ではTaskクラスが該当します。

await演算子は、awaitに渡される処理が完了するまでそのメソッドのawait以降の実行を中断し、制御をすぐに呼び出し元に戻します。awaitに渡された処理が完了したら、メソッド内に残されたawait以降の処理を "継続" として実行します。

しかしながら、必ずしもawaitに渡される処理が非同期的に実行され、それ以降を継続として実行するとは限りません。await以降を継続として登録する前に処理が完了してしまった場合は、通常のメソッド通り同期的に実行されます。

すでに上述しましたが、await演算子の意味は「待っているタスクがまだ完了していない場合、このメソッドの残りをそのタスクの継続として登録して呼び出し元に処理を戻す。タスクが完了したら、タスクは登録しておいた継続を実行する」です。「非同期処理が終了するまで呼び出し元スレッドをブロックして待機する」という意味ではないので注意してください。

継続は同期的

(ILを解読したわけでも、ILを逆コンパイルしたわけでもありませんが) 上記のasync/awaitのサンプルは大体次のように展開されていると思われます。

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
 
namespace WindowsFormsApplication
{
    public partial class MainForm : Form
    {
        public MainForm()
        {
            this.InitializeComponent();
            this.button.Click += this.Button_Click;
        }
 
        private void Button_Click(object sender, EventArgs e)
        {
            this.button.Enabled = false;
            this.HeavyWork().ContinueWith(task =>
            {
                this.button.Enabled = true;
            },
            TaskScheduler.FromCurrentSynchronizationContext());
        }
 
        private Task HeavyWork()
        {
            return Task.Run(() => Thread.Sleep(3000));
        }
    }
}

ここにあるように同期コンテキストを利用した継続になっているためUI要素に触れることができます。同期コンテキストを利用したUIコンポーネントの操作については、TPL入門 (15) - UIコンポーネントの操作を参考にしてください。

戻り値の不思議

次のコードはコンパイルを通すことができ、正常に動作します。しかし、中に記載したコメントのように一見おかしなコードのように思えます。

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
 
namespace WindowsFormsApplication
{
    public partial class MainForm : Form
    {
        public MainForm()
        {
            this.InitializeComponent();
            this.button.Click += this.Button_Click;
        }
 
        private void Button_Click(object sender, EventArgs e)
        {
            //----- 戻り値はTask型だが、TestMethodにreturn文がない
            var task = this.TestMethod();
            Debug.WriteLine("Button_Click : Task.Id : {0}", task.Id);
        }
 
        private async Task TestMethod()
        {
            this.button.Enabled = false;
            //----- 戻り値はTask<int>型なのに、int型で受ける
            //----- 結果は100が返ってくる
            int result        = await this.HeavyWork();
            this.button.Enabled = true;
        }
 
        private Task<int> HeavyWork()
        {
            var task = Task<int>.Factory.StartNew(() =>
            {
                Thread.Sleep(3000);
                return 100;
            });
            Debug.WriteLine("HeavyWork : Task.Id = {0}", task.Id);
            return task;
        }
    }
}

await演算子は最後に、内部的にGetResult関数を実行して値を戻します。そのため、HeavyWork関数の戻り値とは異なった型を受けることになります。もしHeavyWork関数の戻り値がTask<int>型ではなくTask型だった場合、GetResult関数はvoidを返します。そのため値を受けることはできません。この辺りの挙動については、非同期メソッドの内部実装を参考にしてください。

推測ですが、async修飾子の付いた関数はreturn文がなくてもUIスレッドをタスク化したもの、もしくは継続タスクが返るようです。値を戻したくない場合はvoid型を指定することもできます。void型の指定はイベントハンドラなどにasync修飾子を付けるときに便利です。非同期メソッドから「return 10;」のように値を返す場合は、戻り値の型をTask<T>型にする必要があります。