🙆

[C#5] async / await について

に公開

C# で非同期処理を手軽に利用できる仕組みである async と await についてのメモ. Windows に付属のC#コンパイラ([C#] インストール不要のC#コンパイラ参照)を使用する.

0. 概要

以下の内容について記載する.

  1. 「同期」という用語について
  2. async / await の動作
  3. サンプルプログラム
  4. まとめ

世の中には同様の解説が溢れているが, 自分なりの理解をメモしておく.

1. 「同期」という用語について

async / await は, 非同期処理のための仕組みであるが, async / await がわかりにくい原因の一つとして, 日本語の「非同期処理」という言葉がわかりにくい, というのがあるのでは, と思ったのでまずは用語の確認から始める.

1.1 何が「同期」するのか

同期処理といったときの「同期」というのは何が同期することなのか. async / await (非同期処理) の文脈で「同期」するのは前のジョブの終わりと次のジョブの開始と思われる.

つまり同期処理では前のジョブの終了と次のジョブの開始が「同期」する. そのため2つのジョブは順番に処理される. 一方, 非同期処理では前のジョブの終了と次のジョブの開始が「同期」しない(「非同期」)ため, 前のジョブの終了を待たずに次のジョブを開始することができる.

1.2 「同期」と「同時」

「同期」は「同時」という意味ではない. 「同時処理」というと複数のジョブを同時に実行するような印象だが, 「同期処理」はどちらかといえば順番に実施するイメージ. 一方, 「非同時処理」というと同時には処理せず, 順番に実施するようなイメージだが, 「非同期処理」は複数のジョブを同時に処理する.

このように, 「同期」を「同時」ととらえてしまうとまったく逆の意味になってしまう.

「同期」は「同時」ではない.

2. async / await の動作

2.1 別スレッドでの実行(Task.Run)

2つの処理を非同期的に(同時に)実行するためには, まずはスレッドを分ける必要がある. 普通に関数を実行するだけだと, 呼び出し元と同じスレッドで実行されしまう. そのため, あるタスクを別のスレッドで実行するためにはそれ用の命令が必要になる. よく使われるのは Task.Run メソッドであろう.

Task.Run(()=>{
  // some tasks
})

このほか Task.Delay メソッド, HttpClient クラスの GetStringAsync メソッドなども別スレッドで実行される.

2.2 実行の待機(await)

別スレッドでタスクを実行した後, 通常はその実行結果を入手して何か別の処理を実行する. そのためには別スレッドのタスクの実行が終了するのを待って(同期して)値の受け渡し等の処理をする必要がある. 別スレッドでの処理の終了を待つためのキーワードが await である.

await Task.Run(()=>{
  // some tasks
})

await を付けた処理を実行すると, 非同期関数(async のついた関数, 次項参照)は別スレッドで実行される処理が終わるまで待機することになる. その間非同期関数の呼び出しもとに処理が移り, 同時に別の処理を行うことができる.

なお, await の対象とするためには一定の条件がある. 詳細はこちらを参照.

2.3 非同期処理の明示

上記のような非同期処理(await)が含まれる関数を明示するためのキーワードが async である.

async void func(){
  ...
  await Task.Run(()=>{
    // some tasks
  })
  ...
}

2.4 非同期処理のイメージ

以上の流れのイメージは以下のとおり.

  • UI Thread からスタート
  • async 関数が呼ばれる
  • async 関数の中で Worker Thread の処理がスタート
  • すぐに UI Thread に処理が戻され後続の処理を実施. 一方で, async 関数は Worker Thread の処理が終了するまで待機(await)
  • Worker Thread の処理が終了すると async 関数の残りが UI Thread で処理される

これを図にすると以下のような感じになる.

3. サンプルプログラム

サンプルプログラムで実際のスレッドの動きを確認する.

3.1 サンプルプログラムの概要

ここでは以下のようなサンプルプログラムを考える.

  • Form に Button を3つ配置
  • sync ボタン(button1): 同期処理で5秒待機
  • async ボタン(button2): 非同期処理で5秒待機
  • カウントボタン(button3): ボタンを押すごとにカウントが1増える

これらの各処理を行いながらスレッドの流れを追ってゆく.

3.2 サンプルプログラム

サンプルプログラムの構成は以下のとおり.

  • 内部変数の定義
  • フォームの作成
  • ボタンを押したときのコールバック関数の定義
  • ログ(文字列, Thread ID, 時刻)を出力するための補助関数
  • メイン関数
gui.cs
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Threading;
using System.Threading.Tasks;

public class Form1 : Form
{
  // ボタンの定義
  public Button button1,button2,button3;
  // 内部カウンタ変数
  private int _count = 0;

  //フォームの設定
  public Form1()
  {
    button1 = new Button();
    button1.Location = new Point(30,30);
    button1.Text = "sync";
    this.Controls.Add(button1);
    button1.Click += new EventHandler(button1_Click);

    button2 = new Button();
    button2.Location = new Point(30,60);
    button2.Text = "async";
    this.Controls.Add(button2);
    button2.Click += new EventHandler(button2_Click);

    button3 = new Button();
    button3.Location = new Point(30,90);
    button3.Text = "0";
    this.Controls.Add(button3);
    button3.Click += new EventHandler(button3_Click);
  }

  // 各ボタンのコールバック関数

  // 同期処理で5秒待機
  private void button1_Click(object sender, EventArgs e)
  {
    dbg_log("  button1 start");
    Thread.Sleep(5000);
    dbg_log("  button1 end");
  }
  // 非同期処理で5秒待機
  private async void button2_Click(object sender, EventArgs e)
  {
    dbg_log("  button2 start");
    await Task.Run(()=>{
        dbg_log("    Task.Run start");
        Thread.Sleep(5000);
        dbg_log("    Task.Run end");
      });
    dbg_log("  button2 end");
  }
  // カウントボタン
  private void button3_Click(object sender, EventArgs e)
  {
    dbg_log("  button3 start");
    this.button3.Text = _count++.ToString();
    dbg_log("  button3 end");
  }

  // ログを出力する補助関数
  static void dbg_log(string s)
  {
    DateTime dt = DateTime.Now;
    Console.WriteLine("{0} / ThID {1} / Time {2:00}:{3:00}"
      , s
      , Thread.CurrentThread.ManagedThreadId
      , dt.Minute
      , dt.Second);
  }

  // メイン関数
  static void Main()
  {
    dbg_log("Main Start");
    Application.Run(new Form1());
    dbg_log("Main end");
  }
}
make.bat
@echo off
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe %1

コンパイル, 実行

> ./make ./gui.cs
Microsoft (R) Visual C# Compiler version 4.8.9232.0
for C# 5
Copyright (C) Microsoft Corporation. All rights reserved.

This compiler is provided as part of the Microsoft (R) .NET Framework,
but only supports language versions up to C# 5,
which is no longer the latest version.
For compilers that support newer versions of the C# programming language,
see http://go.microsoft.com/fwlink/?LinkID=533240

> ./gui.exe 

3.3 同期処理(sync ボタン)の場合

sync ボタンを押し, その後5秒以内にカウントボタンを2回押す.

結果

> ./gui.exe      
Main Start / ThID 1 / Time 07:38
  button1 start / ThID 1 / Time 07:40
  button1 end / ThID 1 / Time 07:45
  button3 start / ThID 1 / Time 07:45
  button3 end / ThID 1 / Time 07:45
  button3 start / ThID 1 / Time 07:45
  button3 end / ThID 1 / Time 07:45
Main end / ThID 1 / Time 07:47

button1 (sync ボタン) が押され, 5秒の待機が終わってから(同期処理) button3 (カウントボタン) の処理が行われている. sync ボタンによる待機中に押したカウントボタンについて, sync ボタンの処理が終了した後に処理されている. Thread ID はすべて1であり, すべて UI Thread で処理されている.

3.4 非同期処理(async ボタン)の場合

async ボタンを押し, その後5秒以内にカウントボタンを2回押す.

結果

> ./gui.exe
Main Start / ThID 1 / Time 10:29
  button2 start / ThID 1 / Time 10:31
    Task.Run start / ThID 4 / Time 10:31
  button3 start / ThID 1 / Time 10:33
  button3 end / ThID 1 / Time 10:33
  button3 start / ThID 1 / Time 10:33
  button3 end / ThID 1 / Time 10:33
    Task.Run end / ThID 4 / Time 10:36
  button2 end / ThID 1 / Time 10:36
Main end / ThID 1 / Time 10:40

Worker Thread が始まって(Task.Run start)すぐに処理が UI Thread に戻りカウントボタンの処理が行われている. button2 (async ボタン) の残りについて, Worker Thread の処理が終わった後は UI Thread に処理が戻っている(button2 end の Thread ID が1になっている).

3.5 (参考) コンソールアプリの場合

ウィンドウアプリの場合, async 関数内において, await が終了した後は UI Thread で処理が行われるが, コンソールアプリの場合は空いているスレッドで処理が行われる. これを見るために以下のようなプログラムを準備した.

プログラムの内容はフォームアプリと同様だが相違点は以下のとおり.

  • 同期関数(func1)と非同期関数(func2)が順番に実行される
  • 非同期関数(func2)の実行後, メイン関数が終了しないように5秒待機させている(待機させないと非同期処理が終わってなくてもプログラム自体は終了してしまう)
cui.cs
using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
  // 同期関数の定義
  static void func1()
  {
    dbg_log("  func1 Start");
    Thread.Sleep(5000);
    dbg_log("  func1 end");
  }

  // 非同期関数の定義
  static async void func2()
  {
    dbg_log("  func2 Start");
    await Task.Run(()=>{
        dbg_log("    Task.Run start");
        Thread.Sleep(5000);
        dbg_log("    Task.Run end");
      });
    dbg_log("  func2 end");
  }

  // ログを出力する補助関数
  static void dbg_log(string s)
  {
    DateTime dt = DateTime.Now;
    Console.WriteLine("{0} / ThID {1} / Time {2:00}:{3:00}"
      , s
      , Thread.CurrentThread.ManagedThreadId
      , dt.Minute
      , dt.Second);
  }

  // メイン関数
  static void Main()
  {
    dbg_log("Main Start");
    func1();
    func2();
    dbg_log("Main Sleep Start");
    Thread.Sleep(5000); //
    dbg_log("Main Sleep end");
    dbg_log("Main end");
  }
}

コンパイル, 実行

> ./make ./cui.cs
Microsoft (R) Visual C# Compiler version 4.8.9232.0
for C# 5
Copyright (C) Microsoft Corporation. All rights reserved.

This compiler is provided as part of the Microsoft (R) .NET Framework,
but only supports language versions up to C# 5,
which is no longer the latest version.
For compilers that support newer versions of the C# programming language,
see http://go.microsoft.com/fwlink/?LinkID=533240

> ./cui.exe 
Main Start / ThID 1 / Time 22:32
  func1 Start / ThID 1 / Time 22:32
  func1 end / ThID 1 / Time 22:37
  func2 Start / ThID 1 / Time 22:37
    Task.Run start / ThID 3 / Time 22:37
Main Sleep Start / ThID 1 / Time 22:37
    Task.Run end / ThID 3 / Time 22:42
  func2 end / ThID 3 / Time 22:42
Main Sleep end / ThID 1 / Time 22:42
Main end / ThID 1 / Time 22:42

非同期処理が始まって(Task.Run start)すぐにメイン関数の次の処理(Main Sleep start)が実行されている. 非同期処理の残り(func2 end)は Thread ID 3 で実行されていることに注意(メイン関数のスレッドに戻るわけではない).

4. まとめ

C# の非同期処理(async/await)について, 用語のイメージを確認しつつ, サンプルプログラムによってスレッドの動きを確認した.

Discussion