C# 異步編程基礎(七)異步原理


此入門教程是記錄下方參考資料視頻的過程
開發工具:Visual Studio 2019

參考資料:https://www.bilibili.com/video/BV1Zf4y117fs

目錄

C# 異步編程基礎(一)線程和阻塞

C# 異步編程基礎(二)線程安全、向線程傳遞數據和異常處理

C# 異步編程基礎(三)線程優先級、信號和線程池

C# 異步編程基礎(四) 富客戶端應用程序的線程 和 同步上下文 Synchronization Contexts

C# 異步編程基礎(五)Task

C# 異步編程基礎(六)Continuation 繼續/延續 、TaskCompletionSource、實現 Task.Delay

C# 異步編程基礎(七)異步原理

C# 異步編程基礎(八) 異步函數

C# 異步編程基礎(九) 異步中的同步上下文、ValueTask

C# 異步編程基礎(十) 取消(cancellation)、進度報告、TAP(Task-Based Asynchronous Pattern)、Task組合器

同步和異步

  1. 同步操作會在返回調用者之前完成它的工作
  2. 異步操作會在返回調用者之后去做它的(大部分)工作
    異步發方法更為少見,會啟用並發,因為它的工作會與調用者並行執行
    異步的方法通常很快(立即)就返回到調用者,所以叫非阻塞方法
  3. 目前見到的大部分的異步方法都是通用目的的:
    Thread.Start
    Task.Run
    可以將Continuation附加到Task的方法
    ...

什么是異步編程

  1. 異步編程的原則是將長時間運行的函數寫成異步的
  2. 傳統的做法是將長時間運行的函數寫出同步的,然后從新的線程或Task進行調用,從而按需引入並發
  3. 上述異步方式的不同之處在於,它是從長時間運行函數的內部啟動並發。這有兩點好處:
    IO-Bound並發可不使用線程來實現(用回調)。可提高可擴展和執行效率
    富客戶端在worker線程會使用更少的代碼,簡化了線程安全性

異步編程的兩種用途

  1. 編寫高效處理大量並發IO的應用程序(典型的:服務端應用)
    挑戰的並不是線程安全(因為共享狀態通常是最小化的),而是執行效率
       特別的,每個網絡請求並不會消耗一個線程
  2. 在富客戶端應用里簡化線程安全
    如果call graph(調用圖)中任何一個操作是長時間運行的,那么整個call graph必須運行在worker線程上,以保證UI響應
       得到一個橫跨多個方法的單一並發操作(粗粒度)
       需要為call graph中的每個方法考慮線程安全
    異步的call graph,直到需要的時候才開啟一個線程,通常較淺(IO-Bound操作完全不需要)
       其它的方法可以在UI線程執行,線程安全簡化
       並發的粒度適中
        一連串小的並發操作,操作之間會彈回到UI線程

經驗之談

  1. 為了獲得上述好處,下列操作建議異步編寫:
    IO-Bound和Complute-Bound操作
    執行超過50毫秒的操作
  2. 另一方面過細的粒度會損害性能,因為異步操作也有開銷

異步編程和Continuation 以及語言的支持

  1. Task非常適合異步編程,因為它們支持Continuation(它對異步非常重要)
    第十六講里面TaskCompletionSource的例子
    TaskCompletionSource是實現底層IO-Bound異步方法的一種標准方式
  2. 對於Compute-Bound方法,Task.Run會初始化綁定線程的並發
    把task返回調用者,創建異步方法
    異步編程的區別:目標是在調用圖的較低位置來這樣做
       富客戶端應用中,高級方法可以保留在UI線程和訪問控制以及共享狀態上,不會出現線程安全問題

同步版

static void Main(string[] args)
{
    DisplayPrimeCounts();

    //粗粒度調用
    //Task.Run(() => DisplayPrimeCounts());
}
//相當於UI線程
static void DisplayPrimeCounts()
{
    for (int i = 0; i < 10; i++)
    {
        Console.WriteLine(GetPrimesCount(i * 1000000 + 2, 1000000) +
        " primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 - 1));
    }
    Console.WriteLine("Done!");
}

static int GetPrimesCount(int start, int count)
{
    return ParallelEnumerable.Range(start, count).Count(n =>
         Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0));
}

結果

異步版,但是輸出不是我們想要的結果

static void Main(string[] args)
{
    DisplayPrimeCounts();
    //需要阻塞線程
    Console.ReadKey();
}
//相當於UI線程
static void DisplayPrimeCounts()
{
    for (int i = 0; i < 10; i++)
    {
        var awaiter = GetPrimesCountAsync(i * 1000000 + 2, 1000000).GetAwaiter();
        awaiter.OnCompleted(() => Console.WriteLine(awaiter.GetResult() + " primes between ... "));
    }

    Console.WriteLine("Done!");
}

static Task<int> GetPrimesCountAsync(int start, int count)
{
    return Task.Run(() => ParallelEnumerable.Range(start, count).Count(n =>
           Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
}

結果

語言對異步的支持非常重要

  1. 上述例子
  2. 需要對task的執行序列化
    例如Task B依賴於Task A的執行結果
    上述例子為此,必須在continuation內部觸發下一次循環

同步版

static void Main(string[] args)
{
    DisplayPrimeCounts();
    //需要阻塞線程
    Console.ReadKey();
}

static void DisplayPrimeCounts()
{
    DisplayPrimeCountsFrom(0);
}

static void DisplayPrimeCountsFrom(int i)
{
    var awaiter = GetPrimesCountAsync(i * 1000000 + 2, 1000000).GetAwaiter();
    //內部異步調用
    awaiter.OnCompleted(() =>
    {
        Console.WriteLine(awaiter.GetResult() + " primes between ... ");
        if (++i < 10)
        {
            DisplayPrimeCountsFrom(i);
        }
        else
        {
            Console.WriteLine("Done");
        }
    });
}

static Task<int> GetPrimesCountAsync(int start, int count)
{
    return Task.Run(() => ParallelEnumerable.Range(start, count).Count(n =>
            Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
}

結果

異步版,利用狀態機,太麻煩

class Program
{
    static void Main(string[] args)
    {
        DisplayPrimeCountsAsync();
        //需要阻塞線程
        Console.ReadKey();
    }

    static Task DisplayPrimeCountsAsync()
    {
        var machine = new PrimesStateMachine();
        machine.DisplayPrimeCountsFrom(0);
        return machine.Task;
    }

    public static Task<int> GetPrimesCountAsync(int start, int count)
    {
        return Task.Run(() => ParallelEnumerable.Range(start, count).Count(n =>
               Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
    }
}

class PrimesStateMachine
{
    TaskCompletionSource<object> _tcs = new TaskCompletionSource<object>();
    public Task Task
    {
        get { return _tcs.Task; }
    }

    public void DisplayPrimeCountsFrom(int i)
    {
        var awaiter = Program.GetPrimesCountAsync(i * 1000000 + 2, 1000000).GetAwaiter();
        awaiter.OnCompleted(() =>
        {
            Console.WriteLine(awaiter.GetResult());
            if (++i < 10)
            {
                DisplayPrimeCountsFrom(i);
            }
            else
            {
                Console.WriteLine("Done");
                _tcs.SetResult(null);
            }
        });
    }
}

結果

3、async和await
對於不想復雜的實現異步非常重要
原理:狀態機
   異步編程時,異步函數被編譯為包含狀態機的形式,使得代碼在某處停留,等待某個任務完成

修改后的例子

static async Task Main(string[] args)
{
    await DisplayPrimeCountsAsync();
}
async static Task DisplayPrimeCountsAsync()
{
    for (int i = 0; i < 10; i++)
    {
        Console.WriteLine(await GetPrimesCountAsync(i * 1000000 + 2, 1000000) + " primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 - 1));
    }
    Console.WriteLine("Done");
}
//凡是返回Task的方法都可以用await進行調用
static Task<int> GetPrimesCountAsync(int start, int count)
{
    return Task.Run(() => ParallelEnumerable.Range(start, count).Count(n =>
            Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
}

結果

異步原理 結束


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM