はじめに
今更すぎて、この記事自体存在価値がなさそう…
C#のスレッド(非同期処理)は、他の言語では見ない類稀な構文であるため、かなり難しいと思います。さらにawait
/ async
を使うと、プログラムに書かれていない部分にも意識を回す必要があり、初心者には厳しい関門でしょう。スレッド自体の考え方やできることは言語によらずあまり変わらないのですが、C#での表現方法(文法)が独特であるのと、GUIプログラミングという鬼門の分野があるからでしょう。
スレッドを理解するために、押さえておきたいポイント
文法やプログラムの例から始めようと思ったのですが、スレッドを理解するには、その考え方や注意点などプログラムの裏側にあるものを知らなければならないと思うので、まず理解する上で押さえておくべきポイントをを上げたいと思います。
ポイント1:スレッドをどう起動するのか?
ポイント2:スレッドの終了をどう知るのか?
ポイント3:スレッドの実行結果をどう受け取るのか?
この3つはいいと思います。ただ私は、次の4つ目のポイントがスレッドを使えるようになる上で一番重要であるように思います。
ポイント4:どのスレッドで処理が実行されているのか?
どのスレッドで実行しているのかが良く分からないので、とりあえずスレッドの処理が終わるまで待機しておけばいいとか(スレッドを分かっている人から怒られる)、不用意なミスをするとか(デッドロック)、してしまうように思います。この体験が「スレッドには近寄らない方がいい」という気持ちが発生し、ますますスレッドが苦手、分からないという悪循環になっているように思います。
ポイント1:スレッドをどう起動するのか?
まず、なぜスレッドを使いたいのでしょうか? おそらく時間が掛かる処理を別スレッドで実行して、メインスレッドの処理をじゃましないようにするためでしょう。たとえば、ボタンを押したらアプリケーションがフリーズしないように、子スレッドに処理をさせてメインスレッドは即リターンする、などでしょうか。つまりスレッドで実行したいのは処理であり、そしてプログラムで ~処理を見たら、ほぼメソッドで表現されます。ですから、スレッドで処理をするメソッドをまず作りましょう。例として 1 から 100 まで足した結果(5050)を返すメソッドにします。
int Calculate()
{
int total = 0;
for (int i=1; i<=100; ++i)
total += i;
Thread.Sleep(4560); // 何か重い処理をしている...
return total
}
100回足し算をするだけでは今のコンピューターは一瞬で処理が終わってしまうので、何か時間が掛かる処理として Thread.Sleep()
を入れておきます。スレッドの説明であるあるです。
これを子スレッドで実行したい訳ですから、こんな風にメソッドを引数にとる書き方ができるとよさそうです。(「メソッドを引数にとる」という言い方は本当は正しくなく、実際には Action
や Func<>
が引数です。)
子スレッドで実行する(Calculate);
C#ではこんな風に1行で書ける構文があるのでしょうか? それがあるんですね。それがTask.Run()
です。
Task<int> task = Task.Run<int>(new Func<int>(Calculate));
残念ながらC#はメソッド名をそのまま書けないので、Func<int>
で包んであげます(追記:多分一番良いのは、 Task.Run(() => Calculate())
とラムダ式で書くことだと思います)。また、今は説明のため型を明記していますが、実際のプログラムでは型推論が働き、型を省略(Run<int>
の<int>
の部分)できます。戻り値の Task<int> task =
の部分はすぐ後で説明します。
C#の場合、メソッドをインライン化(=ラムダ式)できるので、こう書くこともできます。よく見る例はこっちかもしれません。
Task<int> task = Task.Run<int>(() => {
int total = 0;
for (int i=1; i<=100; ++i)
total += i;
Thread.Sleep(4560); // 何か重い処理をしている...
return total
});
いずれにせよ、これでスレッドを起動し、処理を開始させることができました。
ポイント2:スレッドの終了をどう知るのか?、ポイント3:スレッドの実行結果をどう受け取るのか?
C#ではこの2つはほぼ同時にプログラムで表現されるので、1つにまとめました。
さて、ポイント1でスレッドを起動したのですから、次はスレッドの処理の結果をもらうやり方です。Run()
の戻り値がスレッドの処理の結果 …ではないです。スレッドの処理の戻り値は、スレッドに投入したメソッドの戻り値のことですから、int
です。しかしint
がもらえるのはスレッドの処理が完了した後なので、すぐに int
がもらえるとおかしいのです。だから Run()
の戻り値は、スレッドの処理が完了したときにもらえる予定の Task<int>
になるのです。いわゆる引換券みたいなものです。
では、実際のスレッドの戻り値の取り方はどうやるのでしょうか。それは Task
の Result
プロパティで取得できます。Result
プロパティを使うと、スレッドの処理が「完了」するまで待って、結果を取得できます。
Task<int> task = Task.Run<int>(new Func<int>(Calculate));
int result = task.Result; // スレッドの終了まで「待つ」
ただしこう書くと、スレッドが終了するまで待つことになります。Run()
で子スレッドに処理を投入したのに、すぐ次の行でスレッドの処理が終了するまで待ってしまっては、非同期処理の利点を完全に殺していることになります。つまり、まったくの無駄です。非同期処理なのですから、メインスレッドはRun()
で処理を子スレッドに投入した後、スレッドの完了を待たずさっさと自分の処理に戻りたいのです。そんな都合の良い構文がC#にはあるのでしょうか? まあこう書いている地点であるんですけどね。
Task<int> task = Task.Run<int>(new Func<int>(Calculate));
int result = await task; // スレッドの処理の結果を「待ち受け」する
Task
にawait
を付けると、メインスレッドは処理を即リターンして、子スレッドの処理が終了すると、int result
以降の処理が、おもむろに再開します。何かすごくご都合主義を感じますが、こう書けてしまうのです。await
の動作は、他の記事でも書かれている通り、yield return
に似ています。また、await
について1点注意しておきたいのは、await
はスレッドの処理が終わるまで待たないで待ち受けをしている、ということです。
ちなみにこの記述は2行に分けなければならない訳ではなく、1行にできます。
int result = await Task.Run<int>(new Func<int>(Calculate));
これでスレッドの終了通知(await
を書いておけば勝手にやってくれる)とスレッドの実行結果(await
でTask
から取り出せる)は出来た訳ですが、今までコード断片だったので、もう少しちゃんと書きましょう。このスレッドへの投入を、ボタンクリックで実行するとしましょう。つまりボタンのイベントハンドラで呼び出すとします。スレッドの結果は TextBox
コントロールの text
へ設定することにします。
button1.Click += button1_ClickedAsync;
public async void button1_ClickedAsync(object sender, EventArgs e)
{
Task<int> task = Task.Run<int>(() => {
int total = 0;
for (int i=1; i<=100; ++i)
total += i;
Thread.Sleep(4560); // 何か重い処理をしている...
return total
});
int result = await task;
this.text.Text = $"{result}"; // 雑なstring変換
}
メソッド内にawait
があるということは、すなわちスレッド実行があるということであり、すなわち非同期処理をするということになります。C#では非同期処理をするメソッドに、async
キーワードを付けるというルールがあります。async
があると、呼び出し側はawait
を付けなければならないという、暗黙ルールを伝えることになります。今回の例は、イベントハンドラにしちゃったので await
が不要ですが… 正直例としてはイマイチでした。またメソッド名は末尾に Async
を付けるという慣習があります(ルールじゃない)。
ポイント4:どのスレッドで処理が実行されているのか?
await
/ async
の文法だけならポイント3までで完了ですが、ポイント4もちゃんと見ておきましょう。
例として、GUIアプリケーションでボタンを押したらスレッドを起動するプログラムにしてみます。単にスレッドを起動して、結果をawait
で待ち受けてテキストボックスに設定するだけです。どのスレッドで実行しているかは、Thread.CurrentThread.ManagedThreadId
でスレッドIDが取れるので、これで分かります。
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private async void button1_Click(object sender, EventArgs e)
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: button1_Click START");
Task<int> task = Task.Run<int>(new Func<int>(Calculate));
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: button1_Click Before");
int result = await task;
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: button1_Click After");
this.textBox1.Text = $"{result}";
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: button1_Click END");
}
private int Calculate()
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: Calculate START");
int total = 0;
for (int i=0; i<=100; ++i)
total += i;
Thread.Sleep(4560);
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: Calculate END");
return total;
}
}
実行結果
9: button1_Click START
9: button1_Click Before
10: Calculate START
10: Calculate END
9: button1_Click After
9: button1_Click END
await
後の処理は、親スレッド(ID=9)で実行されているのが分かると思います。絵で表すと次のようになります。(オレンジが親スレッド、緑が子スレッド)
await
後の処理をするスレッドは、そのメソッドを実行したスレッド、すなわちこの例の場合親スレッドで実行されるというルールがあります。子スレッドのままではありません。一般的な説明では、await
後の処理は、メソッドを実行した同期コンテキスト(synchronization context)で実行される、とか同期コンテキストが切り替わる、とか書かれていると思います。
この「親スレッドに切り替わって処理される」というは、GUIプログラムでは重要で、それはUIの更新はメインスレッド(UIスレッド)でしか出来ない、という縛りがあるからです。理由は割愛しますが(長いので)、まぁそういうのがあるんです。
参考サイト:async/awaitと同時実行制御 (++C++; // 未確認飛行 C ブログ)
説明があっさりしていますが、信頼と実績の定番サイトです。
- デッドロックについて
さて、ポイント4は何が重要なのでしょうか? 試しに、Task
をawait
で待ち受けするのではなく、Result
でスレッドが終了するまで待ってみましょう。
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private async void button1_Click(object sender, EventArgs e)
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: button1_Click START");
Task<int> task = Task.Run<int>(new Func<int>(Calculate));
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: button1_Click Before");
int result = task.Result; // ← await ⇒ Resultにしてみた
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: button1_Click After");
this.textBox1.Text = $"{result}";
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: button1_Click END");
}
private int Calculate()
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: Calculate START");
int total = 0;
for (int i=0; i<=100; ++i)
total += i;
Thread.Sleep(4560);
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: Calculate END");
return total;
}
}
こうすると、メインスレッドではResult
で子スレッドの終了を待ち、子スレッドは処理が終了後メインスレッドに処理を復帰させようとするが、メインスレッドの処理が(Result
で待ち続けているため)終わらないので、処理がストップしてしまいます。この状態をデッドロックと呼びます。絵で書くと次のような状態になっています。
これが、よく他のサイトで書かれている Task
をResult
やWait()
で「待って」はいけない理由です。
その他、説明できなかったこと
スレッドに引数を渡す
ポイント1でスレッドを実行する方法を書きましたが、スレッドに引数を渡す方法は説明していませんでした。一番簡単なのは、Run()
の引数をラムダ式でインライン化してしまえば、直接渡せます。
private async void button1_ClickedAsync()
{
int m = 999;
Task<int> task = Task.Run(() => {
Thread.Sleep(3000);
return m * 2;
});
int result = await task; // -> 1998
this.textBox1.Text = $"{result}";
}
ラムダ式の内部の値は、いつ評価されるのでしょうか。具体的には、次の場合、task
の結果は 1998
でしょうか、200
でしょうか。
private async void button1_ClickedAsync()
{
int m = 999;
Task<int> task = Task.Run(() => {
Thread.Sleep(3000);
return m * 2;
});
m = 100;
int result = await task; // -> いくつ?
this.textBox1.Text = $"{result}";
}
C#の場合、200
になります。ラムダ式の中で、変数が評価されるタイミングで、そのときの値を参照します。ちなみにこの動作は当たり前ではなく、(ラムダ式がある)言語によってマチマチです。C#は変数を評価するタイミングで、値を参照する、というルールになっているという話です。
それでは、ラムダ式を定義したときの値を使いたい、文字通りスレッドに引数を渡したい場合はどうすればいいでしょうか。それはTask
コンストラクタの第2引数で値を渡せます。残念ながら、Run()
メソッドには引数を渡すオーバーロードがありませんでした。Task
をnew
した場合は、Start()
でスレッドを開始できます。
private async void button1_ClickedAsync()
{
int m = 999;
Task<int> task = new Task(x => {
Thread.Sleep(3000);
return x * 2;
}, m);
task.Start();
m = 100;
int result = await task; // -> 1998
this.textBox1.Text = $"{result}";
}
非同期メソッドの戻り型(async void?、 async Task?)
非同期にしたいメソッドの戻り値がvoid
であるとき、スレッドの実行結果をもらう必要がありません。この場合、async Task HogeAsync()
ではなくasync void HogeAsync()
ではいけないのでしょうか?
async void
はTask
を返さないので、async Task
と比べて次のことが出来なくなります。
-
await
で待ち受けできなくなり、そのためスレッドの終了を知ることが出来ない、投げっぱなし(fire and forget)になる。 - 例外を捕獲できない
async void
にすること自体には問題ありません(例外を処理できないことを除いて)。 しかし、async Task
で出来ることがasync void
で出来なくなり、その逆は無いため、async void
を積極的に使う理由がありません。そのため、基本的には指向停止でasync Task
を使えば良いでしょう。async void
を使いたいときは、もともと非同期にしたいメソッドがvoid
でなければならない、つまりイベントハンドラ―ということになります。
参考サイト:Async/Await - Best Practices in Asynchronous Programming (英語です)
追記:ただしASP.NETの場合は、async void
の場合は常に誤りになります。私はASP.NETは詳しくないので理由がよく分かりませんが、次のページを読んでくれたら、と思います。
参考サイト:asyncの落とし穴Part3, async voidを避けるべき100億の理由
ASP.NETでaysnc void
を使っていはいけない話
コンソールアプリケーションでの同期コンテキスト
ポイント4の話の続き。今までわざとらしくGUIアプリケーションにしていましたが、それはコンソールアプリケーションでは await
後で実行されるスレッドがGUIと異なっているからです。
class Program
{
static void Main(string[] args)
{
var task = CalculateAsync();
var result = task.Result;
Console.ReadLine();
}
static async Task<int> CalculateAsync()
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: CalculateAsync START");
var task = Task.Run(new Func<int>(Calculate));
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: CalculateAsync Before");
var result = await task;
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: CalculateAsync After");
return result;
}
static int Calculate()
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: Calculate START");
int total = 0;
for (int i = 1; i <= 100; ++i)
total += i;
Thread.Sleep(2345);
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: Calculate END");
return total;
}
}
9: CalculateAsync START
9: CalculateAsync Before
10: Calculate START
10: Calculate END
10: CalculateAsync After
子スレッドのまま(ID=10)です。これは、コンソールアプリケーションの場合、(規定の)同期コンテキストがないため、戻れないからそのまま子スレッドで実行されます。これは、同じプログラムでも実行されるアプリケーションによって動作が異なることを意味しています。ライブラリ作成者はどうするんでしょうか。ASP.NET でも動作が異なるようなのですが、私はASP.NETは全く変わらないので、すみませんがこの記事では書きません(書けません)。
まとめ
結構いろいろ端折ったのですが、それでもやはりスレッドの話は長くなってしまいました。await
/ async
についてはこの位でいいのですが、スレッド全体としてはまだまだ不十分です。スレッドプールやスレッドローカル(変数)、スレッドの中断なんていうのもありますが、もう一つ(2つ)大きなテーマとして、スレッド間の同期を取る同期オブジェクトとシグナルの話があります。同期オブジェクトとシグナルの話は、また別の機会に書きたいと思います。
最後にポイントをまとめておきます。
-
Task.Run()
でスレッド実行。 -
Run()
の戻り型はTask<T>
で、これはスレッドの終了結果をもらう予約(まだもらえてない)。 -
Task
はawait
で「待ち受け」する。待ち受けしている間は、メソッドを即リターンして別の処理を行っている。 - スレッドが終了すると、勝手に
await
の後ろから処理が再開する。そのとき実行するスレッドは親スレッドになる。 - スレッドが終了すると処理は親スレッドに移るので、
Task
をResult
やWait()
で「待って」はいけない。 非同期処理をするメソッドにはasync
をつける。これは呼び出し側がawait
がいる、という目印になる。(厳密には、async
が付いているメソッドは戻り型がTask<T>
(C# 6.0以前)で、Task<T>
はawait
で「待ち受け」するのでawait
がいる)- 追記:戻り値が
Task<T>
であれば、awaitを付けて「待ち受け」をする。安易にResult
やWait()
で「待って」しまうと、デッドロックする場合がある。