忍者ブログ

Memeplexes

プログラミング、3DCGとその他いろいろについて

C#5.0入門:asyncとawait

先日せっかくVS11 Developer Preview版をインストールしましたから、C#の最新機能を使ってみたいと思います。
asyncawaitです。

この2つのキーワードをみる前に準備段階として
こんなコードを考えてみましょう:
<Window x:Class="CSharp5Test.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Button Click="Button_Click"/>
</Window>

using System;
using System.Windows;

namespace CSharp5Test
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            heavyLogic();
            Title = "now : " + DateTime.Now;
        }

        private void heavyLogic()
        {
            System.Threading.Thread.Sleep(3000);
        }
    }
}


でかいボタンが表示されたウィンドウが出てきます。

00785dcb.jpg


このボタンをクリックするとどうなるでしょうか?
上のC#コードから予想できると思います。
3秒経過するとウィンドウのタイトルを現在時刻に書き変えます。
が、その間ブロックするためボタンは固まった状態からなかなか戻りません。
重い処理をすれば、UIが固まるというわけです。
ユーザーはストレスがたまるでしょう。



asyncとawait

そこで登場するのがasyncawaitキーワードです。
これは不思議な魔法を使って、Button_Clickが重い処理にたどり着くと制御を返して、重い処理は別スレッドで実行し、重い処理が終わるとメソッドの続きを元のスレッドで実行してくれます。

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

namespace CSharp5Test
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private async void Button_Click(object sender, RoutedEventArgs e)
        {
            await heavyLogicTaskAsync();
            Title = "now : " + DateTime.Now;
        }

        private Task heavyLogicTaskAsync()
        {
            return Task.Run(() => heavyLogic());
        }

        private void heavyLogic()
        {
            System.Threading.Thread.Sleep(3000);
        }
    }
}


新しく加わったheavyLogicTaskAsync()は、重い処理heavyLogic()を別スレッドで実行してくれます。
Taskクラスは.net4.0から登場した、別スレッドでの実行をつかさどるクラスです。
このTaskはasyncやawaitと連携して非同期処理を行ってくれます。

今度はボタンを押しても、ありがたいことに固まりません。
クリックした後すぐに押せる状態に復帰します。
Button_ClickがheavyLogicTaskAsync()に差し掛かった瞬間制御を返すからです。
もっともタイトルが変化するのにかかる時間は相変わらず3秒ですが・・・

しかし本当に重い処理を別スレッドで走らせてくれているのでしょうか?
そしてその後の処理は元のスレッドで走らせてくれているのでしょうか?
(もっともこれは自明です。元のスレッドで無いとUIのTitleプロパティなんかは変更できません。例外をスローします)
確かめてみましょう。

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

namespace CSharp5Test
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private async void Button_Click(object sender, RoutedEventArgs e)
        {
            writeThreadId();
            await heavyLogicTaskAsync();
            Title = "now : " + DateTime.Now;
            writeThreadId();
        }

        private Task heavyLogicTaskAsync()
        {
            return Task.Run(() => heavyLogic());
        }

        private void heavyLogic()
        {
            writeThreadId();
            System.Threading.Thread.Sleep(3000);
        }

        private static void writeThreadId()
        {
            Console.WriteLine(
                System.Threading.Thread.CurrentThread.ManagedThreadId
                );
        }
    }
}



このプログラムは3つのスレッドIDを出力します。

1.重い処理を始める前のスレッドID
2.重い処理を行うスレッドID
3.重い処理が終わった後のスレッドID

の3つです。
IDがどうなるかは環境によるでしょうが、私の環境ではこうなりました:

9
11
9

つまり、1と3が同じスレッドで、2は別に実行されているということです。
これはやや非直観的というか魔法のようですが、便利ですね。


戻り値を返す場合

以上のサンプルでは、メソッドの戻り値はvoidでした。
しかし実際には重い処理が何か結果を返したくなることもあるでしょう。
そういう時にはこのように書きます。
Task<T>をawaitするのですね。

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

namespace CSharp5Test
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private async void Button_Click(object sender, RoutedEventArgs e)
        {
            DateTime now = await heavyLogicTaskAsync();
            Title = "now : " + now;
        }

        private Task<DateTime> heavyLogicTaskAsync()
        {
            return Task.Run(() => heavyLogic());
        }

        private DateTime heavyLogic()
        {
            System.Threading.Thread.Sleep(3000);
            return DateTime.Now;
        }
    }
}

このプログラムは重い処理をして現在時刻を返すメソッドを呼びます。
ただしasyncでawaitしています。

Task<T>をawaitすると、それは代わりにTになります。
ここではTask<DateTime>をawaitしているので現在時刻の入ったDateTimeが戻ってきているのです。
nowには現在時刻が入ります。



コンソールアプリケーションでasync、await

ここまでの例はWPFを使ったものでしたが、もちろんコンソールアプリケーションでも使えます。
本来は単純なこっちを先に例として出すべきだったかもしれません。
こうなります。

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

namespace AwaitConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            execute();

            Console.WriteLine("startSleeping: ");
            Thread.Sleep(3000);
        }

        private static async void execute()
        {
            await Task.Run(() => heavyLogic());
            Console.WriteLine("heavy logic completed:");
        }

        private static void heavyLogic()
        {
            Console.WriteLine("heavy logic thread:");
            Thread.Sleep(300);
        }
    }
}




実行結果はこうなります。

startSleeping:
heavy logic thread:
heavy logic completed:

どうでしょうか。
よくわかりませんね。
スレッドのIDを表示してみましょう。

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

namespace AwaitConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            execute();

            Console.WriteLine("startSleeping: " + getThreadId());
            Thread.Sleep(3000);
        }

        private static async void execute()
        {
            await Task.Run(() => heavyLogic());
            Console.WriteLine("heavy logic completed:" + getThreadId());
        }

        private static void heavyLogic()
        {
            Console.WriteLine("heavy logic thread:"  + getThreadId());
            Thread.Sleep(300);
        }

        private static int getThreadId()
        {
            return Thread.CurrentThread.ManagedThreadId;
        }
    }
}


各スレッドのIDを表示するようにしました。
どうなっているでしょう?

startSleeping: 10
heavy logic thread:11
heavy logic completed:11
 
まあこんなもんでしょう。

・・・・・・

!?


どういうことでしょうかこれは!
ある意味当たり前ですが。
私はこれまでてっきりheavy logic completedのスレッドIDは常に10になると思っていました。
というかそういう解説をしてきましたしネットでもそんな解説です。
WPFの実験でもそうでした。
が、コンソールではそうなりません。
await ~~;の後のステートメントも別スレッドで実行されています。

これはコンソールではスレッドが暇になる保証がないからでしょうか?
WPFにはメッセージループがありますからね
しかしそうなら、呼び出し元がGUIかコンソールか、どうやって知るのでしょう?
うーんわかりません
自分でも何を口走っているのか分からなくなってきました。

こういうことは言いたくありませんが、まさかバグでしょうか?
(何せこれはまだDeveloper Preview版ですからね)
しかしフレームワークのバグよりもまず自分を疑えという格言もあります。

そもそもWPF版のが魔法のようだったのでこれが普通なのかもしれませんね。

というようなことを考えていると、こんなページを見つけました。
曰く:

「追記:何人かの人々は私に尋ねた。「つまりそれは、Task Asynchrony Patternがメッセージ・ループを持っているUIスレッドでだけ機能するという意味なのか?」いいえ。Task Parallel Libraryは並列性に関係する問題を解決するために明示的に設計された;タスク非同期はその仕事を拡張する。ASP.NET.のように、ユーザ・インタフェースを駆動するメッセージ・ループのないマルチスレッド環境で、非同期が動くのを許すメカニズムがある; 本稿の意図は非同期がUIスレッドでどのようにマルチスレッディングなしで機能するかについて述べることで、非同期がマルチスレッディングなしのUIスレッドだけで機能すると言うことではない。私は、後日、他の種類の「オーケストレーション」コードが、どのタスクをいつ走らせるかを解決するという、サーバー・シナリオについて話すだろう。」

だそうです。
つまり・・・どういうことでしょう??

さらにこんなページを見つけました。
このページによると、await~~の後のコードが°のスレッドで実行されるかはTaskScheduler.FromCurrentSynchronizationContext()によって決定されるようです。
これで謎が解けました。
メッセージループを持つUIなんかではこれによってUIスレッドで継続するわけですね。














拍手[6回]

PR