0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C#の非同期処理を理解したい

0
Posted at

「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文のループが並列して行われています。しかし、何か警告が出ています。image.png
この警告は「長い処理中にエラーが出たらとらえようが無いよ」とうい意味だそうです。
そこで、

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コントロールだとします。

Form1.cs
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メソッドを使います。

Form1.cs
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

  1. Main関数を使わずに、最上位ステートメントを用いて「いきなり」処理を書いても、asyncを持ちTaskを返すMain関数に相当する動きをするそうです。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?