並列タスクを実行するためのC#の原理と例

並列タスクを実行するためのC#の原理と例

C#では、Taskを使用すると並列タスクを簡単に実行できます。

最后更新 2023/03/28 20:17
沙漠尽头的狼
预计阅读 10 分钟
分类
.NET
标签
.NET C#

この記事はChatGPTとの継続的な対話を通じて行われ、コードは検証されます。

C#では、Taskを使用すると並列タスクを簡単に実行できます。Taskは非同期操作を表すクラスで、マルチスレッドアプリケーションを作成するためのシンプルで軽量な方法を提供します。

1.並列タスクの実行原理

Taskを使って並列タスクを実行する原理は、タスクを複数の小さなチャンクに分割し、それぞれが異なるスレッドで実行できることです。次に、Task.Runメソッドを使用して、これらの小さなブロックを別のタスクとしてスレッドプールに送信します。スレッドプールは、スレッドの作成と破棄を自動的に管理し、システムリソースの利用可能性に応じてスレッド数を自動的に調整することで、CPUリソースを最大限に活用できます。

2.5つの例を示す

例1の例

Taskを使用して並列タスクを実行する方法を示す簡単な例を次に示します。

void Task1()
{
    // 创建任务数组
    var tasks = new Task[10];

    for (var i = 0; i < tasks.Length; i++)
    {
        var taskId = i + 1;

        // 使用Task.Run方法提交任务
        tasks[i] = Task.Run(() =>
        {
            Console.WriteLine("任务 {0} 运行在线程 {1} 中", taskId, Task.CurrentId);
            // 执行任务逻辑
        });
    }

    // 等待所有任务完成
    Task.WaitAll(tasks);

    Console.WriteLine("所有任务运行完成。");
    Console.ReadKey();
}

この例では、長さ10のタスクの配列を作成し、Task.Runメソッドを使用して各タスクをスレッドプールに送信しています。タスクの実行中に、Task.CurrentIdプロパティを使用して現在のタスクのIDを取得し、表示しやすくします。最後に、Task.WaitAllメソッドを使用して、すべてのタスクが完了するのを待ち、完了メッセージを出力します。

操作の結果:

任务 3 运行在线程 11 中
任务 4 运行在线程 12 中
任务 8 运行在线程 16 中
任务 1 运行在线程 9 中
任务 2 运行在线程 10 中
任务 5 运行在线程 13 中
任务 6 运行在线程 14 中
任务 7 运行在线程 15 中
任务 9 运行在线程 17 中
任务 10 运行在线程 18 中
所有任务运行完成。

実際の開発では、並列タスクの効率性と信頼性を確保するために、タスクのサイズと数をケースバイケースで評価する必要があることは注目に値する。

例2の例

タスクを使用するもう一つの例は、フィボナッチ数列の計算です。フィボナッチ数列の各項目をタスクと考え、Task.WaitAllメソッドを使用してすべてのタスクが完了するのを待ちます。

void Task2()
{
    static long Fib(int n)
    {
        if (n is 0 or 1)
        {
            return n;
        }
        else
        {
            return Fib(n - 1) + Fib(n - 2);
        }
    }

    const int n = 10; // 计算斐波那契数列的前n项

    var tasks = new Task<long>[n];

    for (var i = 0; i < n; i++)
    {
        var index = i; // 需要在闭包内使用循环变量时需要赋值给另外一个变量

        if (i < 2)
        {
            tasks[i] = Task.FromResult((long)i);
        }
        else
        {
            tasks[i] = Task.Run(() => Fib(index));
        }
    }

    // 等待所有任务完成
    Task.WaitAll(tasks);

    // 打印结果
    for (var i = 0; i < n; i++)
    {
        Console.Write("{0} ", tasks[i].Result);
    }

    Console.ReadKey();
}

この例では、すべてのタスクを格納するためにTask配列を使用します。最初の2つの項目を計算する必要がある場合は、Task.FromResultを使用して完了タスクを直接作成します。それ以外の場合は、Task.Runメソッドを使用して非同期タスクを作成し、Fibメソッドを呼び出して結果を計算します。すべてのタスクが完了するのを待った後、Task配列を反復し、Task.Resultプロパティを使用して各タスクの結果を取得し、出力します。

操作の結果:

0 1 1 2 3 5 8 13 21 34

非同期タスクを作成する場合、ループ変数の値はクロージャ内では不確定であるため、別の変数に代入してクロージャ内でその変数を使用する必要があることに注意してください。そうしないと、すべてのタスクが同じループ変数の値を使用し、結果がエラーになります。

例3の事例

Task配列を使用してすべてのタスクを格納するだけでなく、Task.Factory.StartNewメソッドを使用して並列タスクを作成することもできます。このメソッドはTask.Runメソッドに似ており、非同期タスクを作成してスレッドプールにコミットします。

void Task3()
{
    long Factorial(int n)
    {
        if (n == 0) return 1;
        return n * Factorial(n - 1);
    }

    const int n = 5; // 计算阶乘的数

    var task = Task.Factory.StartNew(() => Factorial(n));

    Console.WriteLine("计算阶乘...");

    // 等待任务完成
    task.Wait();

    Console.WriteLine("{0}! = {1}", n, task.Result);
    Console.ReadKey();
}

この例では、Task. Factory.StartNewメソッドを使用して階乗を計算する非同期タスクを作成し、タスクが完了したら結果を出力するのを待ちます。

実行結果:

计算阶乘...
5! = 120

Task.RunメソッドとTask. Factory.StartNewメソッドはどちらも非同期タスクを作成できますが、動作は若干異なります。特に、Task.Runメソッドは常にTaskScheduler.Defaultをタスクスケジューラとして使用し、Task.Factory.StartNewメソッドはタスクスケジューラ、タスクタイプ、その他のオプションを指定できます。したがって、どの方法を使用するかを選択する際には、ケースバイケースで評価する必要があります。

例4の場合

Taskを使用するもう1つの例は、ファイルを非同期で読み込むことです。この例では、Task.FromResultメソッドを使用して完了タスクを作成し、結果としてファイルの内容を返します。

void Task4()
{
    const string filePath = "test.txt";

    var task = Task.FromResult(File.ReadAllText(filePath)); // 只是方便举例,更好的代码应该是:File.ReadAllTextAsync(filePath); 

    Console.WriteLine("读取文件内容...");

    // 等待任务完成
    task.Wait();

    Console.WriteLine("文件内容: {0}", task.Result);
    Console.ReadKey();
}

この例では、Task.FromResultメソッドを使用して完了タスクを作成し、File. Read AllTextメソッドを使用してファイルの内容を読み込み、結果として返します。タスクが完了するのを待った後、Task.Resultプロパティを呼び出してタスクの結果を取得できます。

メモ帳を自由に作成してください。テスト結果は次のとおりです。

读取文件内容...
文件内容: Dotnet9,专注ASP.NET Core网站开发、MAUI跨平台应用开发、WPF客户端开发,同时以 https://Dotnet9.com 网站分享—些 技术类文章,欢迎交流学习。

実際の開発では、大きなファイルを扱う必要がある場合や、長時間のI/O操作を行う必要がある場合は、UIスレッドをブロックしないように非同期コードを使用する必要があります。たとえば、大きなファイルを読み込む場合、非同期コードを使用してUIスレッドをブロックしないようにし、アプリケーションのパフォーマンスと応答性を向上させることができます。

例5の事例

最後の例は、Taskとasync/awaitを使用した非同期タスクの実装です。この例では、時間のかかる操作を非同期メソッドとしてカプセル化し、async/awaitキーワードを使用して操作の完了を待ちます。

async Task Task5()
{
    async Task<string> LongOperationAsync()
    {
        // 模拟耗时操作
        await Task.Delay(TimeSpan.FromSeconds(3));

        return "完成";
    }

    Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}开始耗时操作...");

    // 等待异步方法完成
    var result = await LongOperationAsync();

    Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}耗时操作完成: {result}");
    Console.ReadKey();
}

この例では、async/awaitキーワードを使用してLongOperationAsyncメソッドを非同期メソッドとして宣言し、awaitキーワードを使用してTask.Delay操作が完了するのを待ちます。メインプログラムではawaitキーワードを使用して、LongOperationAsyncが完了して結果を取得するのを待つことができます。

2023-03-28 20:54:09.111开始耗时操作...
2023-03-28 20:54:12.143耗时操作完成: 完成

async/awaitキーワードを使用する場合は、UIスレッドがブロックされる可能性がある非同期メソッド内でスレッドをブロックする操作を使用しないでください。ブロッキング操作を実行する必要がある場合は、別のスレッドで実行するか、非同期IO操作を使用してスレッドをブロックしないようにします。

async/awaitキーワードを使用する

async/awaitキーワードを使用する際に注意すべき詳細がいくつかあり、さらに2つの例を挙げます。

例1の例

サンプルコードを以下に示します。

async Task Task6()
{
    async Task<string> LongOperationAsync(int id)
    {
        // 模拟耗时操作
        await Task.Delay(TimeSpan.FromSeconds(1 + id));

        return $"{DateTime.Now:ss.fff}完成 {id}";
    }

    Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}开始耗时操作...");

    // 等待多个异步任务完成
    var task1 = LongOperationAsync(1);
    var task2 = LongOperationAsync(2);
    var task3 = LongOperationAsync(3);

    var results = await Task.WhenAll(task1, task2, task3);
    var resultStr = string.Join(",", results);

    Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}耗时操作完成: {resultStr}");
    Console.ReadKey();
}

この例では、Task.WhenAllメソッドを使用して複数の非同期タスクが完了するのを待ち、Joinメソッドを使用してすべてのタスクの結果を最終結果として連結します。

2023-03-28 21:15:42.855开始耗时操作...
2023-03-28 21:15:46.894耗时操作完成: 44.888完成 1,45.883完成 2,46.893完成 3

例2の例

另一个需要注意的问题是,在使用async/await关键字时,应该尽可能避免使用ConfigureAwait(false)方法。这个方法可以让异步操作不必恢复到原始的SynchronizationContext上,从而减少线程切换的开销和提高性能。

然而,在某些情况下,如果在异步操作完成后需要返回到原始的SynchronizationContext上,使用ConfigureAwait(false)会导致调用者无法正确处理结果。因此,建议仅在确定不需要返回到原始的SynchronizationContext上时才使用ConfigureAwait(false)方法。

コード例MethodAAsyncとMethodBAsyncという2つの非同期メソッドを持つコンソールアプリケーションがあるとします。MethodAAsyncは、1秒間待ってから文字列を返します。MethodBAsyncは、2秒間待機してから文字列を返します。コードは以下の通りです。

async Task<string> MethodAAsync()
{
    await Task.Delay(1000);
    return $"{DateTime.Now:ss.fff}>Hello";
}

async Task<string> MethodBAsync()
{
    await Task.Delay(2000);
    return $"{DateTime.Now:ss.fff}>World";
}

ここで、両方のメソッドを同時に呼び出し、その結果を1つの文字列にマージしたいと思います。以下のようにコードを書くことができます:

async Task<string> CombineResultsAAsync()
{
    var resultA = await MethodAAsync();
    var resultB = await MethodBAsync();
    return $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}: {resultA} | {resultB}";
}

このコードは非常にシンプルに見えますが、パフォーマンスの問題があります。CombineResultsAAsyncメソッドを呼び出すと、最初のawaitアクションによって実行コンテキストが元のSynchronizationContext(メインスレッド)に戻るため、非同期アクションはUIスレッドで実行されます。MethodAAsyncから結果が返されるまで1秒待たなければならないため、非同期操作が完了して結果が利用可能になるまでUIスレッドはブロックされます。

この場合、ConfigureAwait falseメソッドを使用して、現在のコンテキストを保持する必要がないスレッドの実行状態を指定し、スレッドプールスレッド上で非同期操作を実行できます。これは以下のコードで実現できます。

async Task<string> CombineResultsBAsync()
{
    var resultA = await MethodAAsync().ConfigureAwait(false);
    var resultB = await MethodBAsync().ConfigureAwait(false);
    return $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}: {resultA} | {resultB}";
}

ConfigureAwait falseメソッドを使用して、非同期操作に現在のコンテキストのスレッド実行状態を保持する必要はないことを指示します。このため、非同期操作はUIスレッドではなくスレッドプールスレッド上で実行されます。UIスレッドがブロックされず、新しいスレッドプールスレッドで非同期操作を実行できるため、潜在的なパフォーマンス問題を回避できます。

IV.サマリー

async/awaitキーワードを使用する場合は、コードの可読性、保守性、パフォーマンスを向上させるためにいくつかのベストプラクティスに従う必要があります。一般的なベストプラクティスを以下に示します。

  1. 尽可能将异步方法声明为TaskTask<TResult>类型,以便可以使用await关键字等待其完成。如果异步方法不返回任何内容,则应将其声明为Task类型。

  2. 非同期メソッド内では、ブロッキングスレッドを使用する操作は可能な限り避け、代わりにノンブロッキング操作を使用してレイテンシーをシミュレートします。ブロッキング操作を実行する必要がある場合は、別のスレッドで実行するか、非同期IO操作を使用してスレッドをブロックしないようにします。

  3. 在异步方法内部不要捕获异常并立即处理,因为这会导致代码变得复杂难以维护。应该让调用者自行处理异常。如果必须在异步方法内部捕获异常,也应该将其包装成AggregateException异常,并将其传递给调用者。

  4. 在使用ConfigureAwait(false)方法时要小心,只有在确定不需要返回到原始的SynchronizationContext上时才使用,否则可能会导致调用者无法正确处理结果。

  5. 尽量避免在异步方法中使用不安全的线程API,例如Thread.SleepThread.Join等方法,以确保代码的可移植性和稳定性。应该使用非阻塞的异步方法来模拟延迟。

  6. 在使用async/await关键字时,应该遵循一些命名约定,例如异步方法的名称应该以Async结尾,以便于区分同步和异步方法。

  7. 在需要同时等待多个异步任务完成时,可以使用Task.WhenAll方法等待所有任务完成。如果只需要等待其中一个任务完成,则可以使用Task.WhenAny方法等待任意一个任务完成。

  8. 在异步方法内部,应该将耗时的操作封装为另外的异步方法,并在需要的地方使用async/await关键字调用它们,以提高代码的可读性和可维护性。

  9. 在使用async/await关键字时,应该尽可能避免使用线程同步机制,例如lock关键字或Monitor类,因为这会导致UI线程被阻塞。而应该使用异步锁或其他非阻塞的线程同步机制。

要約すると、Taskとasync/awaitを使用することで、非同期プログラミングが大幅に簡素化され、コードの可読性、保守性、パフォーマンスが向上する。ただし、コードの正確性と安定性を確保するためには、いくつかの詳細とベストプラクティスに注意する必要があります。

Keep Exploring

延伸阅读

更多文章
同分类 / 同标签 2026/04/22

バージョン別の. NETサポート状況(250 7 0 7更新)

仮想マシンとテストマシンを使用して、各バージョンのオペレーティングシステムの. NETサポートをテストします。オペレーティングシステムのインストール後、対応するランタイムを測定し、スターダストエージェントをパスとして実行できます。

继续阅读
同分类 / 同标签 2026/02/07

AOTの使用経験

プロジェクトの最初から、新しい機能が追加されたり、新しい構文が使用されたりするたびに、AOTリリーステストを行うという良い習慣を身につける必要があります。

继续阅读