Upload
-
View
26.990
Download
6
Embed Size (px)
DESCRIPTION
C# 5.0/VB 11で導入された async/await と、 その背後にある実行インフラ await 演算子の展開結果 await 演算子では解決できなその他の非同期 などについての話。
Citation preview
An other world awaits you避けては通れない非同期処理
++C++; // 岩永 信之
An other world awaits you
別世界があなたを待っています
C# 1.0• Manage
d
C# 2.0• Generic
s
C# 3.0• LINQ
C# 4.0• Dynami
c
C# 5.0• Async
これ
非同期asynchrony
※VB 7 ~ 11 の歴史でもある
C# 5.0• Async
An other world awaits you
別世界があなたを待っています こんな世界
フリーズしない世界 バッテリーの持ちがいい世界
“ 今までも、俺ならできたよ” 訓練された人しかできない 訓練された人でも超大変
見合ったコストでは“できない”
アジェンダ 非同期処理の裏側 いろいろなタイプの非同期処理 Windows 8/WinRT/.NET 4.5 時代の非同期処理
async/await前ふり
まず初めに C# 5.0/VB 11 の強力さを
C# 5.0 非同期処理
async/await
Caller Info CallerFilePath/CallerLineNumber/
CallerMemberName 属性
細かい仕様変更 / バグ修正 foreach 変数 オーバーロード解決 名前付き引数の評価順序
最大の売り本日の主役
http://ufcpp.net/study/csharp/ap_ver5.htmlhttp://msdn.microsoft.com/en-us/library/hh678682.aspx
C# 5.0 の async/await Task クラス async 修飾子 await 演算子
async Task<string> GetAsync(string url){ var client = new HttpClient(); var res = await client.GetAsync(url); var content = await res.Content.ReadAsStringAsync(); return content;}
同期処理の場合とほぼ同じフローで非同期処理
await
t.Wait();
• wait for t: 類義語は stay (とどまる)• スレッドを止めて待つ
await t;
• await t: 類義語は extpect (期待する)• スレッドを止めずにコールバックを待つ
重要 : ちゃんと非同期
複数の確認ダイアログ表示
もう少し複雑な例
確認 1チェック
確認 2チェック
確認 3チェック
No
No
Yes
Yes
Yes
確認フロー
結果表示
No
ゲームでアイテムを合成します
レア アイテムですよ?
合成強化済みですよ?
もう強化限界ですよ?
同期if (this.Check1.IsChecked ?? false){
var result = Dialog.ShowDialog(" 確認 1", "1 つ目の確認作業 ");if (!result) return false;
} if (this.Check2.IsChecked ?? false){
var result = Dialog.ShowDialog(" 確認 2", "2 つ目の確認作業 ");if (!result) return false;
} if (this.Check3.IsChecked ?? false){
var result = Dialog.ShowDialog(" 確認 3", "3 つ目の確認作業 ");if (!result) return false;
} return true;
非同期(旧) 画面に収まるように
フォント サイズ調整 4pt です ほんの 84 行ほど
ちなみに 部分部分を関数化して多少は
整理できます ダイアログ 3 つだからまだこ
の程度で済んでます
if (this.Check1.IsChecked ?? false){
Dialog.BeginShowDialog(" 確認 1", "1 つ目の確認作業 ", result =>{
if (!result){
onComplete(false);return;
}
if (this.Check2.IsChecked ?? false){
Dialog.BeginShowDialog(" 確認 2", "2 つ目の確認作業 ", result2 =>{
if (!result2){
onComplete(false);return;
}
if (this.Check3.IsChecked ?? false){
Dialog.BeginShowDialog(" 確認 3", "3 つ目の確認作業 ", result3 =>{
onComplete(result3);});
}else
onComplete(true);});
}else if (this.Check3.IsChecked ?? false){
Dialog.BeginShowDialog(" 確認 3", "3 つ目の確認作業 ", result3 =>{
onComplete(result3);});
}else
onComplete(true);});
}else if (this.Check2.IsChecked ?? false){
Dialog.BeginShowDialog(" 確認 2", "2 つ目の確認作業 ", result =>{
if (!result){
onComplete(false);return;
}
if (this.Check3.IsChecked ?? false){
Dialog.BeginShowDialog(" 確認 3", "3 つ目の確認作業 ", result3 =>{
onComplete(result);});
}else
onComplete(true);});
}else if (this.Check3.IsChecked ?? false){
Dialog.BeginShowDialog(" 確認 3", "3 つ目の確認作業 ", result3 =>{
onComplete(result3);});
}else
onComplete(true);
非同期( C# 5.0 )if (this.Check1.IsChecked ?? false){
var result = await Dialog.ShowDialogAsync(" 確認 1", "1 つ目の確認作業 ");if (!result) return false;
} if (this.Check2.IsChecked ?? false){
var result = await Dialog.ShowDialogAsync(" 確認 2", "2 つ目の確認作業 ");if (!result) return false;
} if (this.Check3.IsChecked ?? false){
var result = await Dialog.ShowDialogAsync(" 確認 3", "3 つ目の確認作業 ");if (!result) return false;
} return true; • await 演算子が増えただけ
• ダイアログが増えても平気
さて、今日の議題
中身はどうなってるの? 非同期処理の実行インフラ await の実現方法
await で万事解決? そうでもない
並列処理 イベント型の非同期 データフロー
こんなに便利な async/await ですが…
非同期処理の実行インフラスレッド プールとか I/O とか同期コンテキストとか
非同期インフラ スレッド スレッド プール I/O 完了ポート UI スレッドと同期コンテキスト
非同期インフラ スレッド スレッド プール I/O 完了ポート UI スレッドと同期コンテキスト
スレッド 非同期処理の最も低レイヤーな部分
スレッドはプリエンプティブ※なマルチタスク ハードウェア タイマーを使って強制割り込み OS が特権的にスレッド切り替えを行う
static void Main(){ var t = new Thread(Worker2); t.Start(); Worker1();}
Worker1 Worker2
CPU をシェア定期的に切り替え
※preemptive: 先売権のある
高負荷
スレッド 非同期処理の最も低レイヤーな部分
ただし、高負荷
細々とした大量の処理をこなすには向かない 切り替え(コンテキスト スイッチ)のコストが高すぎる
for (int i = 0; i < 1000; i++){ var t = new Thread(Worker); t.Start();}
1000 個の処理を同時実行
スレッドを立てるコスト スレッドに紐づいたデータ
カーネル ステート : 1kB くらい ローカル スタック : 1MB くらい
イベント発生 Thread Attached/Detached イベント
スレッド切り替えコスト カーネル モードに移行 レジスターの値を保存 lock を獲得 次に実行するスレッドの決定 lock を解放 スレッドの状態を入れ替え レジスターの値の復元 カーネル モードから復帰
※ http://blogs.msdn.com/b/larryosterman/archive/2005/01/05/347314.aspx
どれも結構時間がかかる処理
スレッド切り替えコスト削減 切り替えの原因
一定時間経過 待機ハンドル待ち( lock 獲得とか) 同期 I/O 待ち
対策 そもそもスレッドを立てない lock-free アルゴリズム利用 I/O は非同期待ち
自前実装大変スレッドは直接使わない
スレッド プールI/O 完了ポート
スレッド切り替えコスト削減 切り替えの原因
一定時間経過 待機ハンドル待ち( lock 獲得とか) 同期 I/O 待ち
対策 そもそもスレッドを立てない lock-free アルゴリズム利用 I/O は非同期待ち
自前実装大変スレッドは直接使わない
スレッド プールI/O 完了ポート
.NET でいうと• Thread クラスは使わない
• Task クラスを使う
.NET for Windows ストア アプリではついに削除された
.NET 3.5以前の場合は ThraedPool クラスや Timer クラス、IAsyncResult インターフェイス
非同期インフラ スレッド スレッド プール I/O 完了ポート UI スレッドと同期コンテキスト
2種類のマルチタスク
※cooperative
プリエンプティブ
• ハードウェア タイマーを使って強制割り込み• OS が特権的にスレッド切り替えを行う• 利点 : 公平(どんなタスクも等しく OS に制御奪われる)• 欠点 : 高負荷(切り替えコストと使用リソース量)
協調的※
• 各タスクが責任を持って終了する• 1 つのタスクが終わるまで次のタスクは始まらない• 利点 : 低負荷• 欠点 : 不公平( 1 タスクの裏切りが、全体をフリーズさせる)
スレッド プール スレッドを可能な限り使いまわす仕組み
プリエンプティブなスレッド数本の上に 協調的なタスク キューを用意
スレッド プール
キュータスク
1タスク
2
…
数本のスレッドだけ用意
空いているスレッドを探して実行(長時間空かない時だけ新規スレッド作
成)
新規タスク
タスクは一度キューに溜め
る
スレッド プールの性能的な工夫 Work Stealing Queue
lock-free 実装なローカル キュー できる限りスレッド切り替えが起きないように
ローカルキュー 1
ローカルキュー 2
スレッド 1 スレッド 2
グローバルキュー
①スレッドごとにキューを持つ ②
ローカル キューが空のとき、他のスレッドからタスクを奪取
参考 : lock-free アルゴリズム
lock (_sync){ _value = SomeOperation(_value);}
long oldValue1, oldValue2;do{ oldValue1 = _value; var newValue = SomeOperation(_value); oldValue2 = Interlocked.CompareExchange( ref _value, newValue, oldValue1);}while (oldValue1 != oldValue2);
lock ベース
lock-free ( interlocked ベース)
OS機能に頼った競合回避(カーネル モード移行あ
り)
競合してたらやり直すCPU の Interlocked命令※を
利用競合頻度が低い時に高効率
※ アトミック性を保証した CPU命令。カーネル モード移行と比べるとだいぶ低不可interlocked: 連結した、連動した
非同期インフラ スレッド スレッド プール I/O 完了ポート UI スレッドと同期コンテキスト
おさらい 待機しちゃダメ
× lock
× 同期 I/O 待ち × Thread.Sleep
○ 非同期 I/O
○ タイマー( Task.Delay )
何もしていないのにスレッド立てっぱなし• スタック( 1MB )取りっぱなし• スレッド切り替えの誘発• スレッド プール上でも新しいスレッド
が立ってしまう
I/O 完了ポート
I/O Input/Output
CPU と、 CPU の外との入出力 CPU内での計算と比べると、数ケタ遅い
ユーザー入力
通信
ストレージ
CPU
待機しちゃダメ
ハードウェア タイマー
I/O 完了ポート※
I/O を待たない コールバックを登録 I/O 完了後、スレッド プールで続きの処理を行う
スレッド プール
タスク1
タスク2…
※ I/O completion port
あるスレッドアプリ
I/O 完了ポート( OS カーネル内)
ハードウェア
I/O開始 I/O 完了
コールバック登録
コールバック登録後、すぐにスレッド上での
処理を終了
非同期 API を使いましょう 同じ非同期処理でも
Task.Run(() => req.GetResponse());
• スレッド内で同期 I/O• スレッド立てっぱなし
req.GetResponseAsync();
• I/O 完了ポートを使って非同期 I/O• コールバック登録後、スレッドを解放
Sleep もダメ(例 : Sleep Sort ) 値に比例して Sleep すればソートできるんじゃね? というネタ。
new Thread(_ => { Thread.Sleep(t); q.Enqueue(x); }).Start();
Task.Delay(t).ContinueWith(_ => q.Enqueue(x));
× スレッド立てて Sleep
○タイマー利用+ コールバック
要素数分のスレッドが立つ要素数 ×1MB のスタック確
保既定の設定だと、スレッド 1,000 個くらいでOut of Memory
ダメなものは最初から提供しない Windows 8 世代の API
WinRT ファイル操作、ネットワーク、グラフィック ランチャー、ダイアログ表示
.NET 4.5 で追加されたクラス HttpClient など
.NET for Windows ストア アプリ 同期 I/O APIや Thread クラス削除
50ミリ秒以上かかる可能性のある API は非同期 API のみ提供
非同期インフラ スレッド スレッド プール I/O 完了ポート UI スレッドと同期コンテキスト
シングル スレッド必須 スレッド安全なコードは高コスト
いっそ、単一スレッド動作を前提に 典型例は GUI
C#/.NET に限らずたいていの GUI フレームワークはシングル スレッド動作
低レイヤー API ( DirectX とか OpenGL とか)も、 1 つのスレッドからしか扱えない
典型例 : UI スレッド GUI は単一スレッド動作( UI スレッド)
ユーザーからの入力受け付け 画面の更新
UI スレッドユーザー
からの入力
OK
グラフィック
更新
他のスレッド
処理中は応答不可
他のスレッドからは更新不可
矛盾
単一スレッドからしかUI 更新でき
ないそのスレッドを止めると UI フリー
ズ
シングル スレッド推奨
マルチ スレッド推奨
OK
解決策1. スレッド プールで重たい処理
2. UI スレッドに処理を戻してから UI 更新
UI スレッド
OK
更新
他のスレッド
重たい処理
Dispatcher.Invoke
Task.Run
※ dispatcher: 配送者
戻す役割を担うのがディスパッチャー※
ディスパッチャーの利用例 WPF の場合※
Task.Run(() =>{ var result = HeavyWork();
this.Dispatcher.Invoke(() => { this.List.ItemsSource = result; });});
※ WPF 、 Silverlight 、 WinRT XAML でそれぞれ書き方が少しずつ違う
• あまり意識したくないんだけども
• 自動的にはやってくれないの?
自動化するにあたって 処理の実行場所には文脈がある
同期コンテキスト※と呼ぶ
適切な同期コンテキストを拾って実行する必要がある
文脈 要件
スレッド プール どのスレッドで実行しててもいい実行効率最優先
GUI UI の更新は、 UI スレッド上での実行が必要(ディスパッチャーを経由)
Web API どのスレッドで実行してもいいただし、どの Web リクエストに対する処理か、紐づけが必要
※ synchronization contextもちろん自作も可能
同期コンテキストの利用例
あとは、ライブラリの中で自動的に同期コンテキストを拾ってくれれば万事解決…?
var context = SynchronizationContext.Current;
Task.Run(() =>{ var result = HeavyWork();
context.Post(r => { this.List.ItemsSource = (IEnumerable<int>)r; }, result);});
文脈に応じた適切な実行方法をしてくれる• WPF の場合はディスパッチャーへの
Post
完全自動化無理でした スレッド プール中で同期コンテキストを拾うと、
スレッド プールにしか返ってこない BackgroundWorker の DoWork内で、別の
BackgroundWorker を作ると破綻
必ず UI スレッドに処理を戻すと実行効率悪い ぎりぎりまでスレッド プール上で実行したい
ネイティブ⇔ .NET をまたげない WinRT XAML UI だと自動化無理
つまるところ、 JavaScript とかでそれができているのは• 単一 UI フレームワーク• 実行効率度外視
なので、ある程度の自由を TaskSchduler の選択
Task.Run(() => HeavyWork()) .ContinueWith(t => { // スレッド プール上で実行される });
Task.Run(() => HeavyWork()) .ContinueWith(t => { // UI スレッド上で実行される }, TaskScheduler.FromCurrentSynchronizationContext());
既定動作(スレッド プール)
同期コンテキストを拾う
ちなみに、 await は 既定で同期コンテキストを拾う
await した場所で同期コンテキストを拾う (ライブラリ内でなく)利用側で なので、ネイティブ API を使っていても、ちゃんと拾え
る
var result = await Task.Run(() => HeavyWork());
既定動作(同期コンテキストを拾う)
挙動の変更(同期コンテキストを拾わない)var result = await Task.Run(() => HeavyWork()) .ConfigureAwait(false);
いろいろな非同期await も万能じゃない
await が解決するもの (1) 1往復の、 pull 型非同期処理
例 : 前述の GUI での非同期処理
UI スレッド 他のスレッド
重たい処理
await
Task.Run
var result = await Task.Run(() => HeavyWork());
await が解決するもの (2) 1往復の、 pull 型非同期処理
非同期 I/O も好例
UI スレッド I/O 完了ポート
await
Task.RunI/O開始
I/O 完了
var result = await client.GetAsync();
await が解決するもの (3) 1往復の、 pull 型非同期処理
ダイアログ ウィンドウの表示なんかも
UI スレッド
await
ダイアログ表示
OKユーザーがダイアログを操作
GUI フレームワーク
var result = await dialog.ShowAsync();
「 UI スレッドの入れ子」みたいなことができないので、一度処理を抜けなきゃいけない
await が解決しないもの 並列処理 イベント型非同期 そもそも制御フローを書きにくいもの
並列処理 マルチコア CPU を使いきりたい
Parallel クラス
Parallel LINQvar results = data.AsParallel() .Select(x => /* 並列に実行したい処理 */);
Parallel.ForEach(data, x =>{ // 並列に実行したい処理});
イベント型非同期処理 複数件、 push 型
センサー API
サーバーからの push 通知 ユーザー操作への応答も
ある意味では非同期処理
処理側ハンドラー登録
発生側
イベント発生
イベント発生
イベント発生
イベント発生
void Init(){ var sensor = Accelerometer.GetDefault(); sensor.Shaken += sensor_Shaken;}
void sensor_Shaken( Accelerometer sender, AccelerometerShakenEventArgs args){ // イベント処理}
WinRT のイベント型非同期処理 ネイティブの向こう側で起こっていること
同期コンテキストを自動的に拾えないvoid Init(){ var sensor = Accelerometer.GetDefault(); sensor.Shaken += sensor_Shaken;}
void sensor_Shaken( Accelerometer sender, AccelerometerShakenEventArgs args){ // イベント処理}
UI を更新するなら明示的なディスパッチャー利用必須
WinRT のイベント型非同期処理 Rx※ とか使うのがいいかも
var sensor = Accelerometer.GetDefault();
Observable.FromEventPattern(sensor, "Shaken") .ObserveOn(SynchronizationContext.Current) .Subscribe(args => { // イベント処理 });
※ Reactive Extensionshttp://msdn.microsoft.com/en-us/data/gg577609NuGet取得可・Windows ストア アプリでの利用可
TaskScheduler と同じ感覚
制御フローを書きにくいもの await は、同期と同じように非同期を書ける
そもそも同期でも書きにくいものは苦手 2 次元的なデータフローとか ステートマシンとか
制御フローを書きにくいもの WF とか
http://msdn.microsoft.com/en-us/vstudio/aa663328
TPL Dataflow とか http://
msdn.microsoft.com/en-us/devlabs/gg585582.aspx
how to awaitasync/await の中身
非同期処理 おさらい 待機しちゃダメ
コールバック 中断と再開
同期コンテキスト
task.ContinueWith( コールバック );
やるべきことはイテレーターと同じ
イテレーター 中断と再開
class MethodEnumerator : IEnumerator<int>{ public int Current { get; private set; } private int _state = 0; public bool MoveNext() { switch (_state) { case 0: Current = 1; _state = 1; return true; case 1: Current = 2; _state = 2; return true; case 2: default: return false; } }}
IEnumerable<int> Method(){ yield return 1;
yield return 2;
}
イテレーター 中断と再開
class MethodEnumerator : IEnumerator<int>{ public int Current { get; private set; } private int _state = 0; public bool MoveNext() { switch (_state) { case 0: Current = 1; _state = 1; return true; case 1: Current = 2; _state = 2; return true; case 2: default: return false; } }}
IEnumerable<int> Method(){ yield return 1;
yield return 2;
}
Current = 1;_state = 1;return true;case 1:
状態の記録
中断
再開用のラベル
await の展開結果(コンセプト) コンセプト的には イテレーター +
ContinueWith
async Task<int> Method(){ var x = await task1; var y = await task2;}
_state = 1;if (!task1.IsCompleted){ task1.ContinueWith(a); return;}case 1:var x = task1.Result;
中断
状態の記録
結果の受け取り
再開用のラベル
await の展開結果 実際はもう少し複雑
Awaiter というものを介していたり( Awaitableパターン)_state = 1;var awaiter1 = task1.GetAwaiter();if (!awaiter1.IsCompleted){ awaiter1.OnCompleted(a); return;}case 1:var x = awaiter1.GetResult();
• こいつが同期コンテキストを拾い上げていたりする• Awaiter を自作することで、
await の挙動を変更可能• Task以外も await可能
逆に C# 5.0 が使えなくても
イテレーターを使って await に似た非同期処理可能
C# が使えなくても IEnumerable を自作する程度の手間で await に似た
(略
そこそこめんどくさい
絶望的にめんどくさいけど、性能は出ると思う
C# 使わせてください。ほんとお願いします。
まとめ
まとめ スレッド( Thread )は直接使わない
スレッド プール( Task )を使う
await wait ( stay )と違って、 await ( expect )
イテレーター(中断と再開) + ContinueWith (コールバック)
同期コンテキスト
await にだって…できないことは…ある 並列処理、イベント型非同期 そもそも同期で書きにくいもの