LoginSignup
4
3

More than 5 years have passed since last update.

async/await で書いたコードと実行されるスレッド

Last updated at Posted at 2018-12-16

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.Currentnullなので、呼び出し元スレッドに後回しで戻そうにも戻れません。他のスレッドに回されます。
コンソールアプリでは、メインスレッドでないと出来ないようなことは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!");
}
console
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!");
}
console
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);
console
1) 1
Hello World!
2) 3 <- ReadKeyしてるけど関係ない
3) 3
4) 4
5) 4
6) 4
7) 4
Heav end!
<- まだReadKeyがブロックしてる

即座に切り替わっています。最早、呼び出しスレッドは関係なくなります。

4
3
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3