c#使用task執行並行任務的原理和詳細舉例

c#使用task執行並行任務的原理和詳細舉例

在c#中,使用task可以很方便地執行並行任務。

最后更新 2023/3/28 下午8:17
沙漠尽头的狼
预计阅读 13 分钟
分类
.NET
标签
.NET C#

本文通過與chatgpt的連續對話完成,代碼都經過驗證。

在c#中,使用task可以很方便地執行並行任務。task是一個表示異步操作的類,它提供了一種簡單、輕量級的方式來創建多線程應用程式。

一、task執行並行任務的原理

使用task執行並行任務的原理是將任務分成多個小塊,每個小塊都可以在不同的線程上運行。然後,使用task.run方法將這些小塊作為不同的任務提交給線程池。線程池會自動管理線程的創建和銷毀,並根據系統資源的可用情況來自動調整線程數量,從而實現最大化利用cpu資源的效果。

二、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的示例是計算斐波那契數列。我們可以將斐波那契數列的每一項看成一個任務,然後使用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數組來存儲所有的任務。如果需要計算的是前兩項,則直接使用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的示例是異步讀取文件。在這個示例中,我們使用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.readalltext方法讀取文件內容並將其作為結果返回。在等待任務完成後,我們可以通過調用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關鍵字時,還有一些細節需要注意,再給出兩個示例。

示例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()。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";
}

現在,我們想要同時調用這兩個方法,並將它們的結果合併成一個字符串。我們可以像下面這樣編寫代碼:

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線程上運行。由於我們要等待1秒鐘才能從methodaasync()中返回結果,因此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線程不會被阻塞,並且異步操作可以在一個新的線程池線程上運行。

四、總結

在使用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/2/7

aot使用經驗總結

從項目創建伊始,就應養成良好的習慣,即只要添加了新功能或使用了較新的語法,就及時進行 aot 發布測試。

继续阅读