Archive for 4月 2012
非同期処理とディスパッチャー
24日・25日とWDDに行ってたわけですが。
講演者の皆様、UIスレッドとディスパッチャーの話で苦労されてた印象。この辺りの仕組み、どうなんだろうなーとか、少し書いておこうかと。
UIスレッドに紐付いたクラス
まず前提。
UIスレッド
まず、GUIがらみのクラスは、単一スレッドからしかアクセスできないように作ってあります。スレッド安全に作ろうとするとパフォーマンスが出ないので、いっそのこと、UIスレッド以外からアクセスがあったら例外を出して止まるように作ってあります。
この、GUIコンポーネントと紐付いているスレッドがUIスレッドです。
ディスパッチャー
エンド ユーザーからの入力なんかを受け付けているのもこのUIスレッドで、UIスレッド上で時間がかかる処理をすると、UIがフリーズします。
なので、時間がかかる処理をするときは、一度別スレッドで処理して、結果をUIスレッドに戻すというフローが必要です。
WPFやSilverlightでは、それを担うのがディスパッチャー。たいていのUI要素は、DependencyObjectというクラスから派生していて、このクラスのDispatcherプロパティを介することで、UIスレッド上に制御を戻すことができます。
Task.Run(() =>
{
//時間がかかる処理を別スレッドで
return someValue;
})
.ContinueWith(t =>
{
this.Dispatcher.BeginInvoke(new
Action(() =>
{
//UIスレッドで実行すべき処理
}));
});
同期コンテキスト
で、この手の「UIスレッドに制御を戻す仕組み」は、フレームワークによってやり方が異なります。
上記の通り、WPFやSilverlightではDispatcherを使いますが、例えばWindowsフォームの場合はControlクラスのInvokeメソッドを使います。
この差を吸収するためのものが同期コンテキストです。SynchronizationContextを使います。現在のコンテキストを、Current静的プロパティで取得して、それにPostすることで、所望のスレッドに制御を移します。
var syncContext = SynchronizationContext.Current;
Task.Run(() =>
{
//時間がかかる処理を別スレッドで
return someValue;
})
.ContinueWith(t =>
{
syncContext.Post(state =>
{
//UIスレッドで実行すべき処理
}, null);
});
Taskクラスの場合、スケジューラーを使う方が自然かもしれないです。「同期コンテキストに制御を移すスケジューラー」というのも作れて、以下のように書きます。
Task.Run(() =>
{
//時間がかかる処理を別スレッドで
return someValue;
})
.ContinueWith(t =>
{
//UIスレッドで実行すべき処理
}, TaskScheduler.FromCurrentSynchronizationContext());
正直、静的なもの(SynchronizationContext.Current)に依存して実行結果が変わるとか、割とダメな仕様なんですが、他に手があるかというと無理っぽく、悩ましいところ。
意識させたら負けかな
lockステートメントとか、このディスパッチャーやら同期コンテキストやらの仕組みとか、どこかに隠してしまって、ライブラリ利用者からは見えないようにした方がいいのではないかと。学習コストも高ければ、利用方法誤った時のヤバさも半端ないので。
ということで、さっきの、↓みたいなコードはいまいちかなぁと。
Task.Run(() =>
{
//時間がかかる処理を別スレッドで
return someValue;
})
.ContinueWith(t =>
{
this.Dispatcher.BeginInvoke(new
Action(() =>
{
//UIスレッドで実行すべき処理
}));
});
async/await
で、C# 5.0のasync/awaitは同期コンテキストの仕組みを完全に内部に隠してしまったと。
↓のようなコードで、ContinueWithとSynchronizationContext.Current.Postの組み合わせ相当のコードが生成されます。
async
Task RunAsync()
{
var value = await
Task.Run(() =>
{
//時間がかかる処理を別スレッドで
return someValue;
});
//UIスレッドで実行すべき処理
}
一応、Taskクラス自身がこの仕組みを担っているのではなく、Awaitable/Awaiterというパターンの別の型を介していて、この辺りを自作すれば、同期コンテキストを使うかどうかとかも選択可能です。不要なコストになりかねず、必ずしも同期コンテキスト使いたくないので。
awaitですべて解決!とはいかず
awaitが解決するのは、「メソッド呼び出し→1つの値が返ってくる」というフローだけだったりします。
それ以外、例えばセンサーAPIとか、ローミングAPIとかがそうなんですが、常に非同期に、複数の値が飛んできます。ストリーム的なイベント発生。
これは、以下のように、相変わらずイベント ベースで書くことになります。
void InitHandlers()
{
Windows.Storage.ApplicationData.Current.DataChanged += DataChangeHandler;
}
void DataChangeHandler(Windows.Storage.ApplicationData appData, object o)
{
//データのリフレッシュ
}
WDDで多くの講演者の方が悩まされてたのはこちら。UIの更新は、Dispatcherを介さないと実行時エラーに。
EAP(Event-based Asynchronous Pattern)と同期コンテキスト
.NET 2.0の頃に流行った、イベント ベースの非同期パターンでは、実は、内部で同期コンテキストを介してUIスレッドに制御を戻してくれてたりします。
var wc = new
WebClient();
wc.DownloadDataCompleted += (sender, args) =>
{
var result = args.Result;
//ここはUIスレッドで実行されてくれる
};
wc.DownloadDataAsync(uri);
これは仕組み的には、WebClientクラスが内部で頑張ってるだけ。中でSynchronizationContext.Current.Postを呼んでいるはず。WebClientクラス以外で同じような動作になってる保証は一切なし。
しかも、逆に同期コンテキストを使いたくない場合には困ってしまうという難題付き。特に、ただでさえ、静的なものに依存して動作が変わるいまいちな仕様なわけで、それを避ける方法がないというのが結構嫌な感じ。
ということでWinRTでは…
イベント ストリーム系の非同期処理では明示的なDispatcher.Invoke呼び出しが必要となりました。
void InitHandlers()
{
Windows.Storage.ApplicationData.Current.DataChanged += DataChangeHandler;
}
void DataChangeHandler(Windows.Storage.ApplicationData appData, object o)
{
Dispatcher.Invoke(CoreDispatcherPriority.Normal, (sender, args) =>
{
// UIスレッドで実行すべき処理
}
}
というか、そもそもWinRTにはSynchronizationContextクラスがない模様。やっぱり、静的なものに依存して動作が変わるのがいまいちと判断されたんですかねぇ。
うーん、awaitの方がうまく内部に隠してしまってるだけに、こちらで見えているのが残念な…
この手のイベント ストリーム処理は、やっぱRx使うべきかなぁ。
まとめ
- ディスパッチャーとか同期コンテキストとか、利用者側に意識させたら負け
- await/asyncでは割とうまく隠してる
-
イベント ベースの非同期ではダメ…
- Rx使うべきかなぁ
Null非許容
先週、「C#にもNull非許容な型が欲しい」という話をされたものの「うん、欲しいね」としか返せなかったり。要望は昔からあるけども、実際入れようと思うと結構大変。という話、説明しておいた方がいいんだろうなぁと思ったので、ブログにしてみることに。
先に結論の要旨だけ書くと、以下のような感じ。
- C#だけで完結する問題じゃなく、.NETレベルで対応するのは今からだときつい
- Code Contracts使って契約プログラミングするのがいいよ
以下詳細。
公式の見解
Anders Hejlsberg も、「もしもの話」として、1からの再設計が許されるなら C#/.NET をどうしたいかという質問に対して「Null 許容性の見直し」を挙げています。
一時期、結構頻繁にそういっていたと思います。少し検索して出てきたのでいうと、以下の Q&A インタビューの、1:00:00 からのくだり。
言っていることは以下のような話です。
-
参照型/値型の区分と、Null 許容/非許容 の概念は独立させておくべきだった
- 今、値型に関しては、Nullable<T> によって、Null 許容な値型が作れるけども、参照型は常に Null 許容になってしまう
- 参照型も、Null 非許容な参照型が欲しい
これは本当に「もしもの話」。1から作り直すのであればどうするか、逆にいうと、直したいけども今から直すのはかなり厳しいという話だったりします。
C#/.NETは、個人的な予想としておそらく最低あと10年は後方互換性を維持しながらの発展になると思うので、この「もしも」はあったとしても10年以上先の話。
C#だけの問題でなく
今、値型に対する Null 許容は、値型 T に対して、T? と書くことでできるわけです。
int? x = null;
これの逆で、参照型 T に対して、例えば、T! とか書いて、Null 非許容を表すのはどうでしょう。
T! x = new
T(); // OK
T! y = null; // エラー
インスタンスが常にnew演算子に作られる分にはこれでいいんですが、問題はメソッドをまたぐ場合。例えば、ファクトリを介するだけで破たんします。
var factory = new
TFactory();
T! x = factory.Create(); // こいつが null を返さない保証は誰がするの?
T! y = T.Parse(Console.ReadLine()); // 同上
この例の、CreateやParseの戻り値が T! なら万事解決かというとそうでもなく。2点ほど問題があります。
-
既存コードはどうする?
- 「全部を T! に書き換える、さもなくば T! は使えない」という状況になるけども
-
T! はどうコンパイルされるべき?
- C#で閉じている分にはまだしも、他の言語ではどうする?
C#だけなら、属性を付けるとか、あるいは、以下のような構造体に置き換えるようなコード生成で対処できるかもしれない。
struct
NonNullable<T>
where T : class
{
public T Value { get; private set; }
public NonNullable(T value)
{
if (value == null)
throw new ArgumentNullException();
Value = value;
}
}
でも、それだと結局、虚偽申告(実際はnullを返すクセに非null属性を付ける)ができたり、実行時チェックとかにしかできない(ビルド時のチェック効かない)ので、あまり有用にならず。
有用さがいまいちなのに中途半端な妥協実装をしても、後々足かせになる(将来的にもっといい方法があっても入れれなくなる&その他破壊的変更の原因になりかねない)という問題もあります。
Code Contracts
現状のC#で非null保証をしたければ、Code Contractsというのを使って、契約プログラミングします。Contractクラス(System.Diagnostics.Contracts名前空間)を使って、契約表明できます。
public static T Parse(string s)
{
Contract.Requires(s != null);
Contract.Ensures(Contract.Result<T>() != null);
return new T(); // 仮実装
}
これで、Parseメソッドがnullを返さないことを保証します。
さてここで、「なんだ、それで万事解決じゃない」と素直にならないところがまた… 現状のBestではあるものの、まだちょっとめんどくさい感じ。
このCode Contracts、以下のような仕組みで動いています。
-
このRequiresやEnsuresメソッドは、実際には何もしない
- Conditional属性が付いていて、コンパイル後に消える
-
静的検証
-
Visual Studio 2010に関しては、Code Contractsの別途インストールが必要
- 有償エディションにしか入らなかったはず
- その上で、コード解析オプションをOnにしないと働かない
- 静的コード解析、そこそこ時間がかかる
-
-
動的検証
-
静的解析しきれない部分を検証するために、動的検証コードを入れるオプションもある
- 契約の表明はあくまで自己申告なので、やろうと思えば虚偽申告できる
- Code Contractsに対応していないライブラリを使うと、必然的に何らかの動的検証が必要
-
この場合、(C#のコンパイル結果の)ILコードを書き換えて、検証コードを埋め込んでる
- 契約違反は実行時例外発生
- もちろん、検証コード分、実行時の負担になる
-
ちなみに、.NETの標準ライブラリには、.NET 4から、Code Contractsによる契約が表明されています。また、書いた契約をドキュメントに反映する仕組みも持っています。
まあでも、説明中に、なかなかに警戒心を抱くキーワードが見受けられるわけです。「有償エディション」「時間がかかる」「自己申告」「IL書き換え」等々。Contractクラス自体はすでに標準ライブラリに含まれているので積極的に使っておいて損はないんですが、仕組みを知らない人が「あれ、Contract使ってもチェックしてもらえないんだけど?何これ?使えねー」と言ってるのを何度も目にしたりしてます。Ensuresの書き方が結構めんどくさいのも嫌な感じ…