はじめに
C#においてawait以降の処理は「どのスレッドで」「Taskが完了してからどのくらい遅延して」実行されるのか?
インターネット上の情報をもとに実験して得た、自分なりの理解をまとめてみる。
これはあくまで「このような挙動になったことを確認したもの」であり、言語仕様としてどう定義されているかではない。
※インターネット上の情報としてもっとも理解の助けになったページ:
async/awaitと同時実行制御 | ++C++; // 未確認飛行 C ブログ
実験手順
- Visual Studio 2017で「Windows フォーム アプリケーション (.NET Framework)」プロジェクトを新規作成する
- Form1にボタンコントロール「button1」とそのクリックイベントを追加する
- Form1.csを以下のように実装する
- 「デバッグの開始」をする
- 「button1」をクリックする
- デバッグ出力を確認する
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以降の処理は「非同期」にも「後回し」にもされずに「そのままの流れで同期的に」実行される。
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前に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スレッドのメッセージキューに追加される」、そんなイメージ。