忍者ブログ

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回]


v4.0でv2.0のアセンブリを使うとFileLoadExceptionがスローされる問題 (Mixed mode assembly is built against version 'v2.0.50727' of the runtime and cannot be loaded in the 4.0 runtime without additional configuration information.)

このブログでも何度か取り上げましたが、独立して記事にしておきます。
VS2010などで、ランタイムバージョンが2.0のアセンブリを参照して使うと、次のような例外が出ます。

FileLoadException.PNG


FileLoadExceptionです。
ようは、アセンブリのバージョンが違うのでだめですよと言っているのです。

この問題を解決するには、.configファイルを使います。
バージョン2も使いますよと教えてあげるのです。

App.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup useLegacyV2RuntimeActivationPolicy="true">
    <supportedRuntime version="v4.0"/>
  </startup>
</configuration>

このファイルを作って、プログファムを実行するとさっきのような例外はスローされなくなるはずです。



SlimDXでは

さて、私がこの問題に直面したのはSlimDXを使っている時でした。
SlimDXとはDirectXのC#用ラッパーライブラリーです。

このSlimDXのアセンブリを参照に追加しようとしている時です。
どうも一見SlimDXにはランタイムバージョン4.0のものが存在しないように見えたのです。
あたかも2.0のアセンブリだけであるかのように。

IsSlimDXOnlySupportV2.0.png

クリックして上の画像を拡大してみてください。
SlimDXにはランタイムバージョンが2.0しか無いように、一見見えます。
ですから私はそのままこのアセンブリをv4.0のプロジェクトに追加して、FileLoadExceptionに悩まされました。
冒頭のようにApp.configを追加しなければならなかったのです。

そんなわけで「なんでSlimDXはv4.0対応していないのだろう?不親切だなー。これさえなければいいライブラリなのに・・・」と思ったものです。
が、違うのです。
v4.0のものもちゃんと有ります。

思い出すだけで恥ずかしいのですが、これはRuntimeというところをクリックすると、v4.0のアセンブリが出てくるのです。
v4.0のものが先頭に出てきてからSlimDXで検索すると、きちんと出てきます。

SlimDXAlsoSupportsV4.0.png

ですから、実はSlimDXを使うときにApp.configファイルを作る必要など全くなかったのです。
うーん反省です。












拍手[2回]


        
  • 1
  • 2