「asyncとかawait使うやつ」「UIスレッドが固まらなくなるやつ」程度のゴミ理解を脱却したい
スレッド
例えば、ネットワークからデータを受信するとき、じかんがかかると「応答なし」の状態になり、UIの操作を受け付けなくなります。そこで、イベントを待機するために別のスレッドを使用します。
スレッドを使わない場合
DoLongWork();
Console.WriteLine("Long Work Done");
while (true)
{
Thread.Sleep(500);
Console.WriteLine("Hello");
}
void DoLongWork()
{
Thread.Sleep(2000);
}
長い処理をThread.Sleep(2000)(=2秒間待つ)としてみます。結果はこのようになりました。
Long Work Done
Hello
Hello
Hello
Hello
Hello
長い処理が終わってからfor分のループに入ってます。
スレッドの作成&開始
var t = new Thread(DoLongWork);
t.Start();
while (true)
{
Thread.Sleep(500);
Console.WriteLine("Hello");
}
void DoLongWork()
{
Thread.Sleep(2000);
Console.WriteLine("Done");
}
一行目のThreadのコンストラクタはThreadStartデリゲートのインスタンスを引数にとります。よほど古いバージョンのC#でなければ、このように省略表記できます。
結果はこのようになりました。
Hello
Hello
Hello
Done
Hello
Hello
Hello
長い処理が別スレッドで行われたと分かります。
Task
.NET Framework 4.0で導入された機能です。
Task.Run(DoLongWork);
while (true)
{
Thread.Sleep(500);
Console.WriteLine("Hello");
}
//長い処理
void DoLongWork()
{
Thread.Sleep(2000);
Console.WriteLine("Done");
}
このコードに対して結果はこのようになりました。
Hello
Hello
Hello
Done
Hello
長い処理とfor文のループが並列して行われています。しかし、何か警告が出ています。
この警告は「長い処理中にエラーが出たらとらえようが無いよ」とうい意味だそうです。
そこで、
var _ = Task.Run(DoLongWork)
.ContinueWith(x =>
{
if (x.IsFaulted)
{
//失敗した場合の処理
}
});
このように書いて、「長い処理の終了を待たない」ということを明確にする必要があります。(_は廃棄という意味)
asyncとawait
双子の名前みたい。英単語的にはasync=asynchronous,非同期、await=wait,待つという意味になります。こんなコードを実行してみます。1
await Task.Run(DoLongWork);
while (true)
{
Thread.Sleep(500);
Console.WriteLine("Hello");
}
//長い処理
void DoLongWork()
{
Thread.Sleep(2000);
Console.WriteLine("Done");
}
Done
Hello
Hello
英単語通り、長い処理を「待ち」ました。
いちいちTask.Runと書かなくてもいいように、DoLongWork事態を非同期メソッドにしてみます。
await DoLongWork();
while (true)
{
Thread.Sleep(500);
Console.WriteLine("Hello");
}
//長い処理
async Task DoLongWork()
{
await Task.Run(() => Thread.Sleep(2000));
Console.WriteLine("Done");
}
Done
Hello
Hello
結果は一緒ですね。「待たない」ときはawaitをのけたらいいです。
複数スレッドで共通のオブジェクト/リソースを扱う
例えば、あるうどん屋のトイレ(共通のオブジェクト/リソース)が1個だけあることを考えます。もしある人(あるスレッド)が鍵をかけずに使った場合、ほかの人(ほかのスレッド)が入ってきて良くないかと思います。
次のコードを考えてみます。
//共通のオブジェクト/リソース
var count = 0;
await DoTest();
Console.WriteLine(count);
async Task DoTest()
{
var t1 = Task.Run(Increment);//鍵をかけずに用を足してる人
var t2 = Task.Run(Increment);//鍵がかかってないトイレに入った人
await Task.WhenAll(t1,t2);//両方が終了するまで待つ
}
void Increment()
{
for(var i = 0; i < 100000; i++)
{
count++;
}
}
134346
//200000じゃないの!?
おかしな結果になりました。
これはあるスレッドがほかのスレッドの処理の完了を待たずに、countを操作したのが原因です。
対処としてlockステートメントを使います。「ステートメント」はtry-catchなどと同じ立ち位置のものです。
lockステートメントの役割については参考文献[1]より引用します。(あんま理解できてない)
ロックとは、コードをクリティカルセクションとしてマーキングすることです。これによりオブジェクトがロックされて、同期処理を行うことができるようになります。lock宣言により、ロック用のオブジェクトにロックのリクエストが出されて、続く実行文あるいはステートメントブロックの処理を行います。ステートメントブロックが終了するとロックは解除されます。
var count = 0;
var _lockObj = new object();//鍵の役割
await DoTest();
Console.WriteLine(count);
async Task DoTest()
{
var t1 = Task.Run(Increment);
var t2 = Task.Run(Increment);
await Task.WhenAll(t1,t2);
}
void Increment()
{
for(var i = 0; i < 100000; i++)
{
lock(_lockObj){
count++;
}
}
}
200000
GUIを持つ場合で気を付けること
例えばWindowsFormsで、label1がLabelコントロールだとします。
private void button1_Click(object sender, EventArgs e)
{
label1.Text = "";
var t = new Thread(DoLongWork);
t.Start();
}
void DoLongWork()
{
Thread.Sleep(2000);
label1.Text = "Done";
}
ボタンを押して2秒待つと、、、
System.InvalidOperationException: '有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドからコントロール 'label1' がアクセスされました。'
GUIは必ずメインスレッドから操作しないといけないそうです。Controlクラスが持つInvokeメソッドを使います。
private void button1_Click(object sender, EventArgs e)
{
label1.Text = "";
var t = new Thread(DoLongWork);
t.Start();
}
void DoLongWork()
{
Thread.Sleep(2000);
this.Invoke(() => label1.Text = "Done");
}
参考
[1] プログラミングC# 第5版
[2] C#プログラミングのイディオム/定石&パターン
[3] https://learn.microsoft.com/ja-jp/dotnet/csharp/tutorials/top-level-statements
[4] https://qiita.com/acple@github/items/8f63aacb13de9954c5da
-
Main関数を使わずに、最上位ステートメントを用いて「いきなり」処理を書いても、
asyncを持ちTaskを返すMain関数に相当する動きをするそうです。 ↩