#TL;DR
await前 | await | awai`後 | |
---|---|---|---|
動作スレッド | asyncメソッドの呼び出し元のスレッド | 別スレッド |
SynchronizationContext.Current のスレッド |
呼び出し時点から見た同期状態 | 同期 | 非同期 | 非同期 |
一般的なユースケースであろう、FormやUnityのUIスレッドではSynchronizationContext.current
にUIスレッドが設定されているので、UIスレッドからの呼び出しについては
await前 | await | await後 | |
---|---|---|---|
動作スレッド | UIスレッド | 別スレッド | UIスレッド |
呼び出し時点から見た同期状態 | 同期 | 非同期 | 非同期(後回し) |
public void OnClick (object sender, System.EventArgs e)
{
// Thread#0
HeavyAsync();
// Thread#0
Console.ReadKey(); // Thread#0
}
static async Task HeavyAsync()
{
// Thread#0
await Task.Delay(1000); // Thread#1
// Thread#0
await Task.Delay(1000); // Thread#1
}
await
すると何が起こるか
await Task.Delay(300);
Console.WriteLine("After");
というように書いたとします。すると、意味としては次のようになるようです
var context = SynchronizationContext.Current; //Thread#a
Task.Delay(300).ContinueWith(
_=>context.Post(_=> // contextのThread、Thread#aにタスクをさせる
Console.WriteLine("After")
)
);
つまり、await
もその後もその場で実行されることはありません。最初のawaitが出てきた時点でasyncメソッドの呼び出し元に戻ります。
しかし、SynchronizationContext.Current
に呼び出だし元のスレッドが入っている場合、awaitの後は呼び出し元に戻ります。戻るといっても即時実行されるわけではなく、タスクとして後回しにされます。
使い道
どうせ後回しにするなら、「別スレッドでそのまま処理した方がいいじゃない」と思いそうですが、UIスレッドでしかアクセスできないクラスが結構あります。(スレッドセーフでないため)そう考えると、言語仕様は次のような使われ方を想定してるんだなと合点がいきます。
Label label; // UIのコントロールで、UIスレッドでしかアクセス不可
public aysnc Task OnClick (object sender, System.EventArgs e)
{
var content = await LoadFromFileAsync();
label.text = content; // ここは後回しとは言えどUIスレッドなのでOK
}
これをawait
使わずに書こうとすると…
Label label; // UIのコントロールで、UIスレッドでしかアクセス不可
public void OnClick (object sender, System.EventArgs e)
{
var context = SynchronizationContext.Current;
LoadFromFileAsync().ContinueWith(
content=>context.Post(content=>
label.text = content;
// ポイントは、「LoadFromFileの直後に回される」のではなく、
// 「LoadFromFileの直後に『UIスレッドが暇なときに回される』という処理を回す」
// ということで、コードの雰囲気とは若干タイミングがずれている
));
}
await
の方が処理の一見みやすい気がしますが、ここまでのことをちゃんと理解していないと、実行順序が思わぬものになってしまいますね。この例のような単純なものでは全く意識せずともうまくいくと思いますが、メンバの呼び出し順序が厳しいクラスを使っていたり、なまじ昔ながらのスレッドをかじっていると混乱しそうです。(しました)
コンソールアプリなどの場合
自分で設定しない限りSynchronizationContext.Current
はnull
なので、呼び出し元スレッドに後回しで戻そうにも戻れません。他のスレッドに回されます。
コンソールアプリでは、メインスレッドでないと出来ないようなことはGUIアプリほどないので、awaitの旨味もあまりないですね。
検証
Formアプリ
void OnClic(EventArgs e){
Console.WriteLine("1) "+Thread.CurrentThread.ManagedThreadId);
HeavyAsync();
Console.WriteLine("Hello World!");
Console.ReadKey();
}
static async Task HeavyAsync()
{
int c = 2;
for(int i=0;i<3;i++){
Console.WriteLine(c++ +") "+Thread.CurrentThread.ManagedThreadId);
long time = DateTime.Now.Ticks;
while(DateTime.Now.Ticks < time + 3000000) { }
Console.WriteLine(c++ +") "+Thread.CurrentThread.ManagedThreadId);
await Task.Delay(300);
}
Console.WriteLine("Heavy end!");
}
1) 1
2) 1
3) 1
Hello World!
<- ReadKeyの入力のEnter(押すまでブロックされてる)
<- OnClickは抜けた
4) 1 <- 後回しにされてる
5) 1 <- 後回しにされてる
6) 1 <- 後回しにされてる
7) 1 <- 後回しにされてる
Heavy end! <- 後回しにされてる
コンソールアプリ
static void Main(string[] args)
{
Console.WriteLine("1) "+Thread.CurrentThread.ManagedThreadId);
HeavyAsync();
Console.WriteLine("Hello World!");
Console.ReadKey();
}
static async Task HeavyAsync()
{
int c = 2;
for(int i=0;i<3;i++){
Console.WriteLine(c++ +") "+Thread.CurrentThread.ManagedThreadId);
long time = DateTime.Now.Ticks;
while(DateTime.Now.Ticks < time + 3000000) { }
Console.WriteLine(c++ +") "+Thread.CurrentThread.ManagedThreadId);
await Task.Delay(300);
}
Console.WriteLine("Heavy end!");
}
1) 1
2) 1
3) 1
Hello World!
4) 4 <- Mainはブロックされたままだが、別スレッドで実行されてる
5) 4
6) 5
7) 5
Heavy end!
<- ReadKeyはまだブロッキングを続けている
FormアプリではUIスレッドがブロックされている間、await
の後の処理がされていないのに対し、コンソールアプリではされています。SynchronizationContext.Current
がUIスレッドになっているか、null
になっているかの違いです。
asyncメソッド呼び出し時、すぐにスレッドが切り替わって欲しい場合
Task.Run(HeavyAsync);
1) 1
Hello World!
2) 3 <- ReadKeyしてるけど関係ない
3) 3
4) 4
5) 4
6) 4
7) 4
Heav end!
<- まだReadKeyがブロックしてる
即座に切り替わっています。最早、呼び出しスレッドは関係なくなります。