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組合器

異步函數

async和await關鍵字可以讓你寫出和同步代碼一樣簡潔且結構相同的異步代碼

await

  1. await關鍵字簡化了附加continuation的過程
  2. 其結構如下:
    var result=await expression;
    statement(s);
  1. 它的作用相當於:
var awaiter=expression.GetAwaiter();
awaiter.OnCompleted(()=>
{
    var result=awaiter.GetResult();
    statement(s);
});

例子

static async Task Main(string[] args)
{

}
//使用await的函數一定要async修飾
//await不能調用無返回值的函數
static async Task DisplayPrimesCountAsync()
{
    int result = await GetPrimesCountAsync(2, 1000000);
    Console.WriteLine(result);
}

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)));
}

async修飾符

  1. async修飾符會讓編譯器把await當作關鍵字而不是標識符(C# 5 以前可能會使用await作為標識符)
  2. async修飾符只能應用於方法(包括lambda表達式)
    該方法可返回void、Task、Task
  3. async修飾符對方法的簽名或public元數據沒有影響(和unsafe一樣),它只會影響方法內部
    在接口內使用async是沒有意義的
    使用async來重載非async的方法卻是合法的(只要方法簽名一致)
  4. 使用了async修飾符的方法就是“異步函數”

異步方法如何執行

  1. 遇到await表達式,執行(正常情況下)會返回調用者
    就像iterator里面的yield return
    在返回前,運行時會附加一個continuation到await的task
       為了保證task結束時,執行會跳回原方法,從停止的地方繼續執行
    如果發生故障,那么異常就會被重新拋出
    如果一切正常,那么它的返回值就會賦值給await表達式

例子

static async Task Main(string[] args)
{

}
//兩種方法作用相同
static void DisplayPrimesCount()
{
    var awaiter = GetPrimesCountAsync(2, 1000000).GetAwaiter();
    awaiter.OnCompleted(() =>
    {
        int result = awaiter.GetResult();
        Console.WriteLine(result);
    });
}

static async Task DisplayPrimesCountAsync()
{
    int result = await GetPrimesCountAsync(2, 1000000);
    Console.WriteLine(result);
}

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)));
}

可以await什么?

  1. 你await的表達式通常是一個task
  2. 也可以滿足下列條件的任意對象:
    有GetAwaiter方法,它返回一個awaiter(實現了INotifyCompletion.OnCompleted接口)
    返回適當類型的GetResult方法
    一個bool類型的IsCompleted屬性

捕獲本地狀態

  1. await表達式最牛之處就是它幾乎可以出現在任何地方
  2. 特別的,在異步方法內,await表達式可以替換任何表達式,除了lock表達式和unsafe上下文

例子

static async Task Main(string[] args)
{

}
static async void DisplayPrimeCounts()
{
    for (int i = 0; i < 10; i++)
    {
        Console.WriteLine(await GetPrimesCountAsync(i * 1000000 + 2, 1000000));
    }
}
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)));
}

await之后在哪個線程上執行

  1. 在await表達式之后,編譯器依賴於continuation(通過awaiter模式)來繼續執行
  2. 如果在富客戶端的UI線程上,同步上下文會保證后續是在原線程上執行
  3. 否則,就會在task結束的線程上繼續執行

UI上的await

  1. 例子,建議這樣寫異步函數
public MainWindow()
{
    InitializeComponent();

}

async void Go()
{
    this.Button1.IsEnabled = false;

    for (int i = 1; i < 5; i++)
    {
        this.TextMessage.Text += await this.GetPrimesCountAsync(i * 1000000, 1000000) + " primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 - 1) + Environment.NewLine;
    }

    this.Button1.IsEnabled = true;
}

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)));
}

private void Button1_Click(object sender, RoutedEventArgs e)
{
    this.TextMessage.Text = null;
    this.Go();
}
  1. 本例中,只有GetPeimesCountAsync中的代碼在worker線程上運行
  2. Go中的代碼會“租用”UI線程上的時間
  3. 可以說,Go是在消息循環中“偽並發”的執行
    也就是說:它和UI線程處理的其它時間是穿插執行的
    因為這種偽並發,唯一能發生“搶占”的時刻就是在await期間,這其實簡化了線程安全,防止重新進入即可
  4. 這種並發發生在調用棧較淺的地方(Task.Run調用的代碼里)
  5. 為了從該模型獲益,真正的並發代碼要避免訪問共享狀態或UI控件

例子

async void Go()
{
    this.Button1.IsEnabled = false;

    string[] urls = "www.bing.com www.baidu.com www.cnblogs.com".Split();
    int totalLength = 0;
    try
    {
        foreach (string url in urls)
        {
            var uri = new Uri("http://" + url);
            byte[] data = await new WebClient().DownloadDataTaskAsync(uri);
            this.TextMessage.Text += "Length of " + url + " is " + data.Length + Environment.NewLine;
            totalLength += data.Length;
        }
        this.TextMessage.Text += "Total length " + totalLength;
    }
    catch (WebException e)
    {
        this.TextMessage.Text += "Error:" + e.Message;
    }
    finally
    {
        this.Button1.IsEnabled = true;
    }

}

private void Button1_Click(object sender, RoutedEventArgs e)
{
    this.TextMessage.Text = null;
    this.Go();
}

偽代碼:

為本線程設置同步上下文(WPF)
while(!程序結束)
{
    等着消息隊列中發生一些事情
    發生了事情,是哪種消息?
    鍵盤/鼠標消息->觸發event handler
    用戶BeginInvoke/Invoke 消息->執行委托
}

   附加到UI元素的event handler通過消息循環執行
   因為在UI線程上await,continuation將消息發送到同步上下文上,該同步上下文通過消息循環執行,來保證整個Go方法偽並發的在UI線程上執行

與粗粒度的並發相比

1、例如使用BackgroundWorker,不推薦這樣寫異步函數

void Go()
{
    for (int i = 1; i < 5; i++)
    {
        int result = this.GetPrimesCount(i * 1000000, 1000000);
        this.Dispatcher.BeginInvoke(new Action(() =>
        this.TextMessage.Text += result + " primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 - 1) + Environment.NewLine));
    }
    this.Dispatcher.BeginInvoke(new Action(() => this.Button1.IsEnabled = true));
}

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));
}

private async void Button1_Click(object sender, RoutedEventArgs e)
{
    this.TextMessage.Text = null;
    this.Button1.IsEnabled = false;

    Task.Run(() => this.Go());
}
  1. 整個同步調用圖都在worker線程上
  2. 必須在代碼中到處使用Dispatcher.BeginInvoke
  3. 循環本身在worker線程上
  4. 引入了race condition
  5. 若實現取消和過程報告,會使得線程安全問題更任意發生,在方法中新添加任何的代碼也是同樣的效果

編寫異步函數

  1. 對於任何異步函數,你可以使用Task替代void作為返回類型,讓該方法成為更有效的異步(可以進行await)
    例子
static async Task Main(string[] args)
{
    //不加await關鍵字就是並行,不會等待
    await PrintAnswerToLife();
}
static async Task PrintAnswerToLife()
{
    await Task.Delay(5000);
    int answer = 21 * 2;
    Console.WriteLine(answer);
}
  1. 並不需要在方法體中顯式的返回Task。編譯器會生成一個Task(當方法完成或發生異常時),這使得創建異步的調用鏈非常方便
    例子
static async Task Main(string[] args)
{
}
static async Task Go()
{
    await PrintAnswerToLife();
    Console.WriteLine("Done");
}
static async Task PrintAnswerToLife()
{
    await Task.Delay(5000);
    int answer = 21 * 2;
    Console.WriteLine(answer);
}
  1. 編譯器會對返回Task的異步函數進行擴展,使其成為當發送信號或發生故障時使用TaskCompletionSource來創建Task的代碼
    大致代碼
static Task PrintAnswerToLide()
{
    var tcs = new TaskCompletionSource<object>();
    var awaiter = Task.Delay(5000).GetAwaiter();
    awaiter.OnCompleted(() =>
    {
        try
        {
            awaiter.GetResult();
            int answer = 21 * 2;
            Console.WriteLine(answer);
            tcs.SetResult(null);
        }
        catch (Exception e)
        {
            tcs.SetException(e);
        }
    });
    return tcs.Task;
}
  1. 因此,當返回Task的異步方法結束的時候,執行就會跳回到對它進行await的地方(通過continuation)

編寫異步函數,富客戶端場景下

  1. 富客戶端場景下,執行在此刻會跳回到UI線程(如果目前不在UI線程的話)
  2. 否則,就在continuation返回的任意線程上繼續執行
  3. 這意味着,在異步調用圖中向上冒泡的時候,不會發生延遲成本,除非是UI線程啟動的第一次“反彈”

返回Task

  1. 如果方法體返回TResult,那么異步方法就可以返回Task
    例子
static async Task Main(string[] args)
{

}
static async Task<int> GetAnswerToLiife()
{
    await Task.Delay(5000);
    int answer = 21 * 2;
    return answer;
}
  1. 其原理就是給TaskCompletion發送的信號帶有值,而不是null
    例子
static async Task PrintAnswerToLife()
{
    int answer = await GetAnswerToLife();
    Console.WriteLine(answer);
}
static async Task<int> GetAnswerToLife()
{
    await Task.Delay(5000);
    int answer = 21 * 2;
    return answer;
}
  1. 與同步編程很相似,是故意這樣設計的
    同步版本
static void Main(string[] args)
{

}
static void Go()
{
    PrintAnswerToLife();
    Console.WriteLine("Done");
}
static void PrintAnswerToLife()
{
    int answer = GetAnswerToLife();
    Console.WriteLine(answer);
}
static int GetAnswerToLife()
{
    Thread.Sleep(5000);
    int answer = 21 * 2;
    return answer;
}

C#中如何設計異步函數

  1. 以同步的方式編寫方法
  2. 使用異步調用來替代同步調用,並且進行await
  3. 除了頂層方法外(UI控件的event handler,因為沒有await調用),把你方法的返回類型升級為Task或Task ,這樣它們就可以進行await了

編譯器能對異步函數生成Task意味着什么?

  1. 大多數情況下,你只需要在初始化IO-Bound並發的底層方法里顯式的初始化TaskCompletionSource,這種情況很少見
  2. 針對初始化Compute-Bound的並發方法,你可以使用Task.Run來創建Task

異步調用圖執行

例子

static async Task Main(string[] args)
{
    //Main Thread
    await Go();
}
static async Task Go()
{
    var task = PrintAnswerToLife();
    await task;
    Console.WriteLine("Done");
}
static async Task PrintAnswerToLife()
{
    var task = GetAnswerToLife();
    int answer = await task;
    Console.WriteLine(answer);
}
static async Task<int> GetAnswerToLife()
{
    var task = Task.Delay(5000);
    await task;
    int answer = 21 * 2;
    return answer;
}
  1. 整個執行與之前同步例子中調用圖的執行順序是一樣的,因為我們對每個異步函數的調用都進行了await
  2. 在調用圖中創建了一個沒有並行和重疊的連續流
  3. 每個await在執行中都創建了一個間隙,在間隙后,程序可以從中斷處恢復執

並行(Parallelism)

  1. 不使用await來調用異步函數會導致並行執行的發生
  2. 例如:_button.Click+=(sender,args)=>Go();
    主線程仍然在執行,GO()也在執行
    確實也能滿足保持UI響應的並發要求
  3. 同樣,可以並行跑兩個操作:
    var task1=PrintAnswerToLife();
    var task2=PrintAnswerToLife();
    await task1;
    await task2;

異步Lambda表達式

  1. 匿名方法(包括Lambda表達式),通過使用async也可以變成異步方法
  2. 調用方式也一樣
static async Task Main(string[] args)
{
    Func<Task> unnamed = async () =>
    {
        await Task.Delay(1000);
        Console.WriteLine("Foo");
    };

    await NamedMethod();
    await unnamed();
}
static async Task NamedMethod()
{
    await Task.Delay(1000);
    Console.WriteLine("Foo");
}
  1. 附加event handler的時候也可以使用異步Lambda表達式

例子

myButton.Click+=async (sender,args)=>
{
    await Task.Delay(1000);
    myButton.Content="Done";
}

相當於

myButton.Click+=ButtonHandler;

async void ButtonHandler(object sender,EventArgs args)
{
    await Task.Delay(1000);
    myButton.Content="Done";
}
  1. 也可以返回Task
static async Task Main(string[] args)
{
    Func<Task<int>> unnamed = async () =>
    {
        await Task.Delay(1000);
        return 123;
    };
    int answer = await unnamed();
}

異步函數 結束


免責聲明!

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



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