Archive for 11月 2012
async/awaitと同時実行制御
C# 5.0のasync/awaitを使うと、多くの場面ではシングル スレッド的な動作になるし、多くの場面ではlock不要(結果的に、デッドロックが起こりようなくなる)になったりします。
ただし、「多くの場面で」。「必ず」ではないのがはまりどころ。いくつかの場面では、同時実行制御が必要です(普通にマルチスレッドの平行実行になるので、同時に同じデータにアクセスされる可能性を考慮しないとバグります)。
前提知識
いくつか、C# 5.0世代の非同期処理についての前提知識は、以下のスライド(先月末の.NETラボでの発表)を参考にしてください。
- 5~12ページ: async/awaitの書き方
- 17~22ページ: スレッドとそのコスト
- 24~26ページ: スレッド プール
- 29~32ページ: I/O完了待ちと非同期API
- 36~40ページ: UIスレッドとディスパッチャー
- 41~45ページ: 同期コンテキスト
await演算子と同期コンテキスト
今日の主題となるのは以下の2点。
- 文脈なんて普段から意識するものではなく、同期コンテキスト(synchronization context: 同時実行における文脈)も、できるだけ意識させたくない
- await演算子は同期コンテキストを自動的に拾う
awaitは同期コンテキストを拾うというのは、例えば、以下のようなコードを書いたとするとしたときに、
static async void Run()
{
await Run1Async();
// 続きの処理
}
以下のようなコードと同じような処理手順になるということです。
static async void Run()
{
var context = SynchronizationContext.Current;
Run1Async().ContinueWith(t =>
{
context.Post(state =>
{
// 続きの処理
}, null);
});
}
(実際にawait演算子がやっていることはもう少し複雑ですが、「同期コンテキストを拾う」という点に焦点を当てるとこんな感じになるという例です。)
Fire & Forget
非同期メソッドには、Task(もしくはTask<T>)を返す、await演算子かWait()メソッドで「待てる」(awaitable)メソッドと、voidの「待てない」(fire & forget)メソッドの2種類定義できます。
awaitableは「待てる」という名前通り、awaitする前提です。一方のfire & forgetは「呼ぶだけ呼んであとは放置」という意味で、以下のような使い方になります。
さて、この図の例でいうと、await someTask; した後、次の行がどう実行されるかは、実行環境(の持つ同期コンテキストの種類)次第となります。
GUI上で
async void、fire & forget型の非同期メソッドの用途はイベント ハンドラーです。というよりむしろ、それ以外の状況での使い道は乏しいし、はまりどころになるだけなので非推奨です。
例えば、以下のようなコードを考えてみましょう。ThreadUnsafeの部分には、スレッド安全でない(マルチスレッド実行するならlockが必要な)処理が入っているものとしましょう。
HttpClient _client = new HttpClient();
private async void Button_Click_1(object sender, RoutedEventArgs e)
{
await _client.GetAsync(_url1);
ThreadUnsafe();
await _client.GetAsync(_url2);
ThreadUnsafe();
await _client.GetAsync(_url3);
ThreadUnsafe();
}
このButton_Click_1を、ボタンのClickイベントのハンドラーにしましょう(というか、このメソッド名はVisual Studio上でClickイベントのハンドラーを自動生成した結果得られるものです)。
<Window x:Class=”WpfApplication1.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″>
<Grid>
<Button Content=”click here” Click=”Button_Click_1″ />
</Grid>
</Window>
この状況で、そのボタンを連打しても(lockしなくて)平気なの?というのが疑問として浮かびます。(答えだけ先に言ってしまうなら不要です。)
まず、ボタンを1回だけ押したときの挙動。GUIアプリ(WPFでもSilverlightでもWindowsストア アプリでも)の同期コンテキストは、制御フローをUIスレッドに返すようになっています。その結果、先ほどのコードは以下のように実行されます。
ThreadUnsafeの部分が実行されているのは常に同じスレッドで、同時に実行される可能性はありません。これは、ボタンを連打しても変わらず、連打時の挙動は以下のようになります。
非同期処理なので実行される順序やタイミングまでは保証できませんが、ThreadUnsafeの部分がすべてUIスレッド上で実行されることは保証されます。
ちなみに、UIスレッドに処理を戻す際には、一度メッセージがキューに溜められるので、「処理中に帰ってきた応答が処理されない」というようなこともありません。
つまり、以下のような場合には常にlock不要。
- UIスレッド上から非同期処理開始
- I/O待ち
- awaitする
GUIのイベント ハンドラーとして使う限りにおいては、fire & forgetでも何の問題もなかったり。
スレッド プール実行
さて、I/O待ちでなく、「重たい処理をUIスレッドから逃がしたい」みたいな場合にはスレッド プール上での実行(Task.Run)が必要になります。
private async void Button_Click_1(object sender, RoutedEventArgs e)
{
PreWork();
await Task.Run(() =>
{
HeavyWork();
});
PostWork();
}
ここで覚えておくべきことは、Task.Runの内側と外側で同期コンテキストが変わること。
Task.Runの外側はUIのコンテキストになります。awaitした後にはUIスレッドに処理が戻されているので、後半もUIスレッド上です。
一方、内側は、スレッド プール上での実行になりますが、スレッド プール上のコンテキストではawaitしてもスレッド プール上のどこかで実行されるだけで、決してUIスレッドには戻らなくなります。
ということで、問題となる書き方の例。Task.Runでスレッド プール実行に移った後のawait。
private async void Button_Click_1(object sender, RoutedEventArgs e)
{
await Task.Run(async () =>
{
await someTask;
await someTask;
});
}
Task.Runの中のawaitでは、単一スレッド実行にはならないので、lockが必要になることがあります。
コンソール アプリ
前述の通り、GUI上でI/O待ちする分には割と問題がでないんですが。一方、コンソール アプリだとどうでしょう。
コンソール アプリは同期コンテキストを持っていません。SynchronizationContext.Currentはnullです。
using System;
using System.Threading;
class Program
{
static void Main()
{
Console.WriteLine(
SynchronizationContext.Current == null); // true
}
}
この場合、「awaitは既定の同期コンテキストを使う」ということになっていて、その既定の同期コンテキストはスレッド プールを使います。
GUIの場合と同じようなコードをコンソール アプリでも書いてみましょう。
static HttpClient _client = new HttpClient();
static async Task RunAsync()
{
await _client.GetAsync(_url1);
ThreadUnsafe();
await _client.GetAsync(_url2);
ThreadUnsafe();
await _client.GetAsync(_url3);
ThreadUnsafe();
}
以下のように、どこのスレッドでThreadUnsafeが実行されているかの保証がなくなります。
ただし、この時点では、まあ、複数のThreadUnsafeが同時に実行されることはありません。
問題が出るのは、これをfire & forgetで複数呼んだときや、Task.WhenAllでfork/joinした場合。
static async Task RunDoubeAsync()
{
await Task.WhenAll(
RunAsync(),
RunAsync());
}
以下のように、どのスレッドで実行されるかも、タイミングも保証がないので、ThreadUnsafeの部分が複数のスレッドで同時に実行される可能性があります。
当然、lockが必要です。この状況下でlockが必要なのは当然なんですが、問題は、全く同じコードなのに、状況によってlockの必要性が変わること。
ASP.NET
コンソール アプリの場合「同期コンテキストを持っていない」というので、ある意味わかりやすいのもの、むしろ問題となるのはASP.NETだったり。
ASP.NETは同期コンテキストを持っています。
var t = System.Threading.SynchronizationContext.Current.GetType();
var name = t.Name;
とか書くと、AspNetSynchronizationContextという名前が帰ってきます。
ここで勘違いしてはいけないのは、GUIの同期コンテキストとは目的からして全く違うということ。
GUIでは、UIスレッド上で実行しないといけないことが多いので、UIスレッドに処理を戻す必要がありました。これがGUIにおける文脈であり、同期コンテキストがやっていることです。
が、こういう制約はWebにはありません。Webにあるのは、どのWebリクエストと紐づいた処理かという文脈です。なので、AspNetSynchronizationContextは、
- やらない: 単一スレッド実行
- やる: HttpContext等の保持
- やる: リクエストが来てから開始した非同期処理が全部終わるまで、レスポンスを返さないよう、処理の監視
というものです。単一スレッド実行しない。awaitの挙動的には、コンソール アプリの場合と同じ注意が必要です。
オーバーヘッド
まあ、同期コンテキスト越しの処理というのはそれなりにオーバーヘッドが生じます。特に、GUIの場合、UIスレッドに制御フローを戻すのが、結構な負担。なので、勝手にUIスレッドに戻っているというのも、問題になることあり。
例えばということで、以下のような一連の流れを考えてみましょう。
- IOAsync: 非同期I/O待ち
- HeavyWork: 1.の結果を使って、何かしらの重たい処理を行う(スレッド プール上で実行したい)
- UpdateUI: 2.の結果をUIに反映(UIスレッド上での実行必須)
もちろん、いくつかの書き方があります。何パターンか示していきましょう。ダメなものから順に(なので、最後のやつ推奨)。
まずはわかりやすいダメな例。全部同期。UIスレッドが止まるので、いわゆるフリーズ。
PreWork(); |
次、非同期I/Oのところだけawait。HeavyWorkのところでフリーズ。
PreWork(); |
なのでまあ、HeavyWorkをTask.Run。ただ、単純にそこをawaitで書こうとすると、一度UIスレッドを経由しちゃうので非効率。
PreWork(); |
そこで実際はどうするべきかというと、ContinueWithでつなぐこと。同期コンテキストを拾いたいならawaitに頼る、そうでないならawaitに頼らない。
PreWork(); |
ConfigureAwait
さて。awaitの挙動は、Awaitableパターンというものを満たすクラスを作ることで、自由にカスタマイズできます。
「同期コンテキストを拾う」という挙動も、単にTaskに対する既定の実装がそうしているというだけで、拾わないようにすることも可能です。
それをやっているのがConfigureAwait拡張メソッド。ConfigureAwaitの引数にtrueを渡すと同期コンテキストを拾う、falseを渡すと拾わないという仕様になっています。
います、が…。またダメな例。以下のようなコードを書いたとします。
async void RunAsync()
{
PreWork();
await HeavyWorkAsync()
.ConfigureAwait(false);
await AnotherWorkAsync()
.ConfigureAwait(true);
PostWork();
}
2個目のConfigureAwaitはtrueなので、同期コンテキストを拾ってUIスレッドに戻ってほしい。けども、実際はそうなりません。
Task.Runの中でawaitしてもUIスレッドには戻れないのと同じ理屈。一度ConfigureAwait(false)してしまうとUIスレッドに戻れなくなる。
なので、前述の例を、こいつを使って書いてしまうとバグります。スレッド プール上でUIを更新しようとして、実行時例外で落ちます。
PreWork(); |
解消法は簡単というか、もうすでに例を示している通り、ContinueWithを使えばOK。
まとめ
特定の状況下、例えば、
- UIスレッド上から非同期処理開始
- I/O待ち
- awaitする
であれば、非同期処理といえども、挙動はシングル スレッド的で、同時実行制御(lockとか)は不要です。
一方で、
- コンソール アプリは同期コンテキストを持っていない
- ASP.NETの同期コンテキストはマルチスレッド動作
- Task.Runの中でさらにawaitしても、スレッド プールの同期コンテキストになる
- ConfigureAwaitにも注意
- 同期コンテキストを介するのはそれなりのオーバーヘッド
などの注意が必要です。
for Desktop, for Phone, for…?
商標問題で Metro が使えなくなって久しいですが。「My アプリ for Windows Store」も嫌だし、そもそも商標問題なくても「for Metro」も微妙よなぁという話。
Windows Phone 8 の方が Windows 8 とコアの共通化したのでなおのこと。
Metro 改め
ちなみにまあ、商標的なところに使わなければ割と平気で、開発者通称的には Metro で通せなくもなさそう(アプリ名とかに使えないことに変わりはないですが)。
商標的なところにおいてどうなったかというと、
- Metro アプリ → Windows ストア アプリ
- Metro デザイン → Microsoft Designe Language
でしたっけ。
for Windows Store
おい、お前、デスクトップ アプリも Windows ストア上に並べてるくせに(リンクのみ。ストア上からの[インストール]ボタンはないけども)。
あと、長い。却下。
for Store
おい、Windows Phone Store もストアだろ。
あと、Azure Store も //build/ でアナウンスあったけども、それもストアだろ。
(どっちも元々マーケットだったけども、たぶんブランド名そろえるためにストアに変更。)
ってことで、嫌なんですよねぇ、この単語。
for Metro
仮に商標問題なくても嫌。
これも、理由は似たようなものなんですが、Windows Phone の方も Metro デザインだから。
あと、Metro デザイン風に作ったデスクトップ アプリもありますし、最近多くの Web サイトが Metro デザインっぽくなっちゃってるし。
for Modern UI
5年・10年とモダンという名前を使い続けるのか。
ナウでヤングだな、おい。
new ‘new iPad’ じゃあるまいし。
for WinRT
恐れていた事態がすでに起きてしまっています。
en-us ロケールの bing で、「WinRT」で検索すると、以下のページがトップです。
OS の方の、いわゆる「ARM 版 Windows 8」のことの、「Windows RT」の公式サイト。
Windows Runtime、略して WinRT とはなんだったのか。
というので、この名前が嫌というのもありますが、Windows Phone 8 の方も WinRT の亜種なんですよねぇ。Metro と一緒で、「Phone の方もだから」という理由でも棄却対象。
.NET も大概だけども
.NET も当初、
- 実行エンジンとしての .NET (CLR: common language runtime のこと)
- 共通型システムとしての .NET (CTS: common type system のこと)
- ライブラリ プロファイル(どのクラスを使えるか)としての .NET (.NET Client Profile とか .NET for Windows Store とか)
の混乱はなはだしかったですが、今の WinRT はまさにそれと同じで、何をさして WinRT って呼んでるんだかさっぱり。
for WinRT なんて名前つけようものなら、
- winmd 参照して使ってるデスクトップ アプリどうなるんだ(共通型システムとしての WinRT は普通にデスクトップでも使える)
- Windows Phone 8 の方、Windows Phone RT なんて呼ばれたりもするけども、中身は WinRT よね(差が出るのはプロファイルの部分)
等々。
for Tablet
これがまだいい気もしつつ。
一応、「マウス/キーボードでもタッチでも使える Windows 8」、「ストア アプリ = touch-first ≠ touch-only」ということもあって抵抗感あり。
個人的にも、タブレットにもノート PC にもなるタイプの分離型端末に期待してるので、あんまり。
for Surface
ということでいっそ、for Surface とでもしとこうかなぁ。
とか思うわけです。
これこそ、他社製品どうすんだ感漂ってますが。
元祖 Surface どうすんだ感も漂ってますが。
まあ、「touch-first UI 向け」くらいの意味合いで、surface(固有名詞じゃなく、一般単語の surface、表面)って単語使うのも悪くはないかなぁ。
アプリの内部コード名はこれで行こうとか思う次第。
としても、ストア上に公開するアプリ名としては意味通じてくれないよなぁ。
審査的には?
そもそも審査通るのかな?
基本的に、自社コンテンツであれば堂々と「my アプリ」の部分だけ出して、「for なんとか」なんて付ける必要ないというか、今ストアに並んでる大半のアプリは「for なんとか」とか付けていませんが。
ファン コンテンツとして、「このアプリは○○公式ではなく、ファンが○○を Windows ストア向けに移植したものです」的なものはどうしたものか(アプリの説明にその旨の説明がちゃんと目立つようにあれば、審査通るはず)。
そういや、Skype は、Windows ストア アプリ版が「Skype」で、これまでのデスクトップ向けの奴が「デスクトップ用 Skype」になっちゃったんですよねぇ。ふざけてる。
non-nullable reference type
何度かこの話だしてるものの。C# に、参照型だけども null が絶対入っていないことを保証する型(non-nullable)はなぜないの?という話。
それなりにしっかりと説明してるブログ記事を見つけたのでリンク:
- Difficulties with non-nullable types (part 1)
- Difficulties with non-nullable types (part 2)
- Difficulties with non-nullable types (part 3)
- Difficulties with non-nullable types (part 4)
以下、簡単な日本語解説。
non-nullable?
.NET は元々、値と参照の区別しか持っていなくて、
- 値型は常に 非 null (non-nullable)
- 参照型は null を許容(nullable)
でした。
で、.NET 2.0で、Nullable<T> が導入されて、
- 値型に nullable が増えて、値型は null を許すかどうかを選べるようになった
- 参照型はやっぱり nullable のみ
という状態。
もちろん、この時点で non-nullable 参照型の導入も検討はされた(上記ブログはその時代の記事)けども、相当厳しいという話。
既定で null
.NET は、パフォーマンス上の理由で、既定値を 0/null/既定値の入れ子 に限定しています。
値型とか配列を初期化した時に、C でいうところの memset(0) するだけでいいように。
C++ なんかだと、デフォルト コンストラクターを持ってる型の配列を作ったりすると、要素の数だけコンストラクターが呼ばれるという仕様があったり。逆に、デフォルト コンストラクターを呼べない型の配列は作れない。バイト配列を確保しておいて、placement new するとか、このコストを避ける方法はあるにはあるんですが。
初期化順序の泥沼
で、non-nullable 参照型を作ろうとしたときに問題になるのが、この既定値問題。上記理由でいったんは必ず null 初期化されてしまう参照型に対して、「null じゃなくなった」っていうことを保証する仕組みが必要で、これが非常に難しい。
- 親クラスのコンストラクター内で non-nullable 参照型のフィールドを参照とどうなるの?
- C# の仕様では、自分のフィールド初期化 → 親のフィールド初期化 → 親のコンストラクター → 自分のコンストラクター の順で呼ばれる。
- 自分のコンストラクター内での初期化では、親のコンストラクターの時点での非 null 保証ができない
- 静的メンバーの循環参照があるときどうするの?
- non-nullable な静的フィールドの 非 null 保証しようと思ったら、静的コンストラクター内で初期化するしかないけども。
- 静的コンストラクターが呼ばれるタイミングは、そのクラスが最初に参照された瞬間。循環参照してしまうと、片方は非 null 保証できない。
- 配列どうしよう?
- 先に領域を確保したうえで、for ループで要素の初期化するような用途の場合どうしたものか。
- 配列の new[] の時点で、要素初期化子みたいなデリゲートを渡すしかないけど、そういう仕様が .NET にはない。
という感じ。
なので、やるなら最初の(.NET 1.0の)時点でやらなきゃいけなかったし、それをやってたとしても結構変な制約がかかったり、パフォーマンスに影響したりしそう。