[C#][VB.NET]非同期処理の制御(async, await, Task)

[C#][VB.NET]非同期処理の制御(async, await, Task)

非同期制御について

C#(VB.NET)で非同期処理の制御を行う場合、 async/await を使用するのが現在の主流です。
async/await を使用することでUIの応答を止めることなく処理を行えるので、ユーザビリティが向上します。

スレッドとスレッドプール

スレッド(thread:糸、筋)とは一連の処理の流れを意味します。スレッドが一つ(シングルスレッド)の場合、複数の処理を上から順に実行していきます。これに対し、スレッドが複数(マルチスレッド)の場合、複数のスレッドで処理が同時並行的に進みます。

スレッドの生成や切り替えは処理コストが大きくパフォーマンスに影響があります。そこでスレッドプールという仕組みで、1度作ったスレッドを使いまわします。スレッドは効率よく実行できるスレッド数で調整されます。

タスクとは

タスク(Task)の概念は、非同期の処理がいつ完了したのかを知り、戻り値を取得できるようにするために導入されました。Taskを使って非同期処理の制御が簡単にできるようになります。
Taskクラスはスレッドプールを使います。

async/await と Task.Run の使い方

TaskクラスのRunメソッドを使用することで、簡単に非同期処理(引数に渡したデリゲート)のタスクを作成・実行ができます。
また、async/await 修飾子を使い、非同期処理を同期処理と同じような書き方で記述できます。

Taskに対して await を使うと一度別スレッドに制御を移し、Taskの完了後に続きの処理を再開します。
await を使った非同期的な待機は、async 修飾子を指定したメソッドでのみ使用可能になります。

注意点として次のとおりです。

  • 非同期処理中の別スレッドからはUIを触れない
  • ここで紹介している async/await は.NET 4.5以降で使えます。

[C#][VB.NET]非同期処理の基本サンプル

次の例では、Task.Runメソッドで作成したタスクで重たい処理を実行し、待機しています。

C#

public async Task Sample1Async()
{
    // 別スレッドで処理
    var task = Task.Run(() =>
    {
        // 重たい処理 ...
        for (int i = 0; i < int.MaxValue; i++) { } ;
    });
    // タスクが終わるまで待機
    await task;
    Console.WriteLine("タスク完了 ...");
}

VB.NET

public Async Function Sample1Async() As Task
    Dim task As Task = Task.Run(
        Sub()
            ' 重たい処理
            For index = 0 To Integer.MaxValue - 1
            Next
        End Sub
    )
    Await task
    Console.WriteLine("タスク完了 ...")
End Function

[C#][VB.NET]複数のタスクをまとめて待機

Task.WhenAllメソッドは引数で指定したタスクすべて完了するまで待機する新しいTaskを作成して返します。
よって、この戻り値のTaskを await で待機することで複数の非同期なタスクがすべて完了するまで待機できます。

C#

public async Task Sample2Async()
{
    var task1 = Task.Run(() =>
    {
        // 重たい処理1 ...
        for (int i = 0; i < int.MaxValue; i++) { };
    });
    var task2 = Task.Run(() =>
    {
        // 重たい処理2 ...
        for (int i = 0; i < int.MaxValue; i++) { };
    });
    // task1, task2の処理を待機
    await Task.WhenAll(task1, task2);
    Console.WriteLine("全タスク完了 ...");
}

VB.NET

public Async Function Sample2Async() As Task
    Dim task1 As Task = Task.Run(
        Sub()
            ' 重たい処理
            For index = 0 To Integer.MaxValue - 1
            Next
        End Sub
    )
    Dim task2 As Task = Task.Run(
        Sub()
            ' 重たい処理
            For index = 0 To Integer.MaxValue - 1
            Next
        End Sub
    )
    Await Task.WhenAll(task1, task2)
    Console.WriteLine("タスク完了 ...")
End Sub

[C#][VB.NET]非同期処理(Task.Run)の戻り値

Task.Runの引数のデリゲートに戻り値を返す Func<T> を指定することで、戻り値が Task<T> となり、awaitすることで T の型の戻り値を取得できます。

C#

public async Task Sample3Async()
{
    // 非同期処理を実行し、戻り値を受け取る
    var result = await Task.Run(new Func<int>(() =>
    {
        // 重たい処理と適当な戻り値
        for (int i = 0; i < int.MaxValue; i++) { };
        return 1;
    }));
    Console.WriteLine("タスク完了:{0}", result);
}

VB.NET

Private Async Function Sample3Async() As Task
    Dim result As Integer = Await Task.Run(
        Function() As Integer
            For index = 0 To Integer.MaxValue - 1
            Next
            Return 1
        End Function
    )
    Console.WriteLine("タスク完了:{0}", result)
End Function

[C#][VB.NET]多数のデータに対し非同期で処理を行う

LINQを使えば複数のTaskを並列で起動し、非同期的に集計等の処理を行えます。実際にはすべての処理が平行して実行されるわけではなく、スレッドプールで使える状態のスレッドが使いまわされるようです。

C#

public async Task Sample4Async()
{
    var list = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    var tasks = list.Select(n => Task.Run(() =>
    {
        // 5秒待機
        Thread.Sleep(5000);
        Console.WriteLine($"処理完了:{n}");
        return n * 2;
    }));
    var result = await Task.WhenAll(tasks);
    Console.WriteLine("処理結果:{0}", string.Join(",", result));
}

VB.NET

Public Async Function Sample4Async() As Task
    Dim list As New List(Of Integer) From {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    Dim tasks As IEnumerable(Of Task(Of Integer)) =
        list.Select(Function(n) (
            Task.Run(Function()
                         Thread.Sleep(5000)
                         Console.WriteLine($"処理完了:{n}")
                         Return n * 2
                     End Function)
        ))
    Dim result As IEnumerable(Of Integer) = Await Task.WhenAll(tasks)
    Console.WriteLine("処理結果:{0}", String.Join(",", result))
End Function

[C#][VB.NET]非同期処理とUI制御

例えば重たい処理を非同期で実行し、その進捗をUI側に表示させたい場合、直接別スレッド(Task.Runの処理)からUIを触るとエラーになります。Taskの戻り値もしくはその外でUIを触れば問題ありません。

ボタンを押下すると、適当な処理をしてその進捗をラベルに表示させる処理の例です。

private async void button1_Click(object sender, EventArgs e)
{
    var list = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    var count = list.Count;
    var now = 0;

    label1.Text = "開始";
    foreach (var n in list)
    {
        await Task.Run(() =>
        {
            // ワーカースレッド内ではUIを触れない
            Thread.Sleep(1000);
        });
        label1.Text = $"{now++} / {count}";
    }
    label1.Text = "完了";
}

VB.NET

Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    Dim list As New List(Of Integer) From {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    Dim count As Integer = list.Count
    Dim now As Integer = 0

    Label1.Text = "開始"
    For Each n As Integer In list
        Await Task.Run(Sub()
                           Thread.Sleep(1000)
                           now += 1
                       End Sub)
        Label1.Text = $"{now} / {count}"
    Next
    Label1.Text = "完了"
End Sub

参考URL

C#カテゴリの最新記事