LoginSignup
1
6

More than 5 years have passed since last update.

C#においてawait以降の処理はいつどこで実行されるのか

Posted at

はじめに

C#においてawait以降の処理は「どのスレッドで」「Taskが完了してからどのくらい遅延して」実行されるのか?
インターネット上の情報をもとに実験して得た、自分なりの理解をまとめてみる。
これはあくまで「このような挙動になったことを確認したもの」であり、言語仕様としてどう定義されているかではない。

※インターネット上の情報としてもっとも理解の助けになったページ:
async/awaitと同時実行制御 | ++C++; // 未確認飛行 C ブログ

実験手順

  1. Visual Studio 2017で「Windows フォーム アプリケーション (.NET Framework)」プロジェクトを新規作成する
  2. Form1にボタンコントロール「button1」とそのクリックイベントを追加する
  3. Form1.csを以下のように実装する
  4. 「デバッグの開始」をする
  5. 「button1」をクリックする
  6. デバッグ出力を確認する
Form1.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            int startTick = System.Environment.TickCount;
            Log(startTick, "1", "UI", 0);
            A(startTick);
            Log(startTick, "6?", "UI", 500);
            System.Threading.Thread.Sleep(2000);
            Log(startTick, "14", "UI", 2500);
        }

        private async void A(int startTick)
        {
            Log(startTick, "2", "UI", 0);

            // await前にTaskが終わるケース(UIスレッドの場合)
            Task task = Task.Run(() => Sleep(startTick, "3", "4", 250, 0));
            System.Threading.Thread.Sleep(500);
            await task;
            Log(startTick, "5", "UI", 500);

            // await後にTaskが終わるケース(UIスレッドの場合)
            await Task.Run(() => B(startTick));
            Log(startTick, "15", "UIsync", 2500);
        }

        private async void B(int startTick)
        {
            Log(startTick, "7?", "work7?", 500);

            // await後にTaskが終わるケース(ワーカースレッドの場合)
            await Task.Run(() => Sleep(startTick, "8?", "9", 500, 500));
            Log(startTick, "10", "work8?>", 1000);

            // await前にTaskが終わるケース(ワーカースレッドの場合)
            Task task = Task.Run(() => Sleep(startTick, "11", "12", 250, 1000));
            System.Threading.Thread.Sleep(500);
            await task;
            Log(startTick, "13", "work8?>", 1500);
        }

        private void Sleep(int startTick, string seq1, string seq2, int sleep, int expectTaskStartTick)
        {
            string threadName = "work" + seq1;
            Log(startTick, seq1, "work" + seq1, expectTaskStartTick);
            System.Threading.Thread.Sleep(sleep);
            Log(startTick, seq2, "work" + seq1, expectTaskStartTick + sleep);
        }

        private void Log(int startTick, string seq, string threadName, int expectTick)
        {
            int threadId = System.Threading.Thread.CurrentThread.ManagedThreadId;
            int actualTick = System.Environment.TickCount - startTick;
            Console.WriteLine("({0,4}ms:{1,4}ms) [{2}:{3,-7}] {4}", expectTick, actualTick, threadId, threadName, seq);
        }
    }
}

デバッグ出力のフォーマット

(想定経過時間ms:実測経過時間ms) [スレッドID:想定スレッド識別名] 想定到達順"

「想定スレッド識別名」は以下のような規則で命名している:

  • UIスレッド(同期コンテキストを持つスレッド)で処理されるものは「UI」
  • UIスレッドの同期コンテキストで処理されるものは「UIsync」
  • ワーカースレッド(同期コンテキストを持たないスレッド)で処理されるものは「work」に「想定到達順」を付ける
  • await以降の処理がTask側のスレッドで実行される場合、Task側の想定スレッド識別名に「>」を付ける

「想定到達順」については、非同期処理のタイミングによって順番が変わり得る部分には「?」を付けている。

実験結果

(   0ms:   0ms) [1:UI     ] 1
(   0ms:   0ms) [1:UI     ] 2
(   0ms:  31ms) [3:work3  ] 3
( 250ms: 281ms) [3:work3  ] 4
( 500ms: 531ms) [1:UI     ] 5
( 500ms: 531ms) [1:UI     ] 6?
( 500ms: 531ms) [3:work7? ] 7?
( 500ms: 531ms) [4:work8? ] 8?
(1000ms:1031ms) [4:work8? ] 9
(1000ms:1031ms) [4:work8?>] 10
(1000ms:1031ms) [3:work11 ] 11
(1250ms:1281ms) [3:work11 ] 12
(1500ms:1531ms) [4:work8?>] 13
(2500ms:2531ms) [1:UI     ] 14
(2500ms:2531ms) [1:UIsync ] 15

※実際には2と3の間にdllロードの出力があるが、ここでは関係ないので取り除いている

結果から読み取れる傾向

await到達前にTaskが終わるケース

(   0ms:   0ms) [1:UI     ] 2
(   0ms:  31ms) [3:work3  ] 3
( 250ms: 281ms) [3:work3  ] 4
( 500ms: 531ms) [1:UI     ] 5
(1000ms:1031ms) [4:work8?>] 10
(1000ms:1031ms) [3:work11 ] 11
(1250ms:1281ms) [3:work11 ] 12
(1500ms:1531ms) [4:work8?>] 13

await前にTaskが終わっている場合はawaitに到達してもreturnせず、await以降の処理は「非同期」にも「後回し」にもされずに「そのままの流れで同期的に」実行される。

task_first.png

await到達後にTaskが終わるケース

ワーカースレッドの場合

( 500ms: 531ms) [3:work7? ] 7?
( 500ms: 531ms) [4:work8? ] 8?
(1000ms:1031ms) [4:work8? ] 9
(1000ms:1031ms) [4:work8?>] 10

ワーカースレッドで未完了のTaskをawaitした場合、awaitに到達したスレッドはそこで呼び出し元にreturnし、await以降の処理は「Task側のスレッドでTask側の処理を完了したあと即座に」実行される。

await_first.png

総じて(await前にTaskが終わるケースも含め)、ワーカースレッドでawaitした場合はawait以降の処理は「びりっけつで処理を終えたスレッドで実行される」、そんなイメージ。
自分の担当分を先に終えたワーカーは先に帰り(そして次の仕事を始め)、最後まで仕事をしていたワーカーが責任を持って後始末をするのである。

UIスレッドの場合

( 500ms: 531ms) [1:UI     ] 5
( 500ms: 531ms) [1:UI     ] 6?
(1500ms:1531ms) [4:work8?>] 13
(2500ms:2531ms) [1:UI     ] 14
(2500ms:2531ms) [1:UIsync ] 15

UIスレッドで未完了のTaskをawaitした場合、awaitに到達したスレッドはそこで呼び出し元にreturnし、await以降の処理は「Task完了時に高優先度のメッセージとしてUIスレッドのメッセージキューに追加される」、そんなイメージ。

ui_await.png

1
6
0

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
1
6