此入門教程是記錄下方參考資料視頻的過程
開發工具:Visual Studio 2019
目錄
C# 異步編程基礎(六)Continuation 繼續/延續 、TaskCompletionSource、實現 Task.Delay
C# 異步編程基礎(十) 取消(cancellation)、進度報告、TAP(Task-Based Asynchronous Pattern)、Task組合器
異步函數
async和await關鍵字可以讓你寫出和同步代碼一樣簡潔且結構相同的異步代碼
await
- await關鍵字簡化了附加continuation的過程
- 其結構如下:
var result=await expression;
statement(s);
- 它的作用相當於:
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修飾符
- async修飾符會讓編譯器把await當作關鍵字而不是標識符(C# 5 以前可能會使用await作為標識符)
- async修飾符只能應用於方法(包括lambda表達式)
該方法可返回void、Task、Task - async修飾符對方法的簽名或public元數據沒有影響(和unsafe一樣),它只會影響方法內部
在接口內使用async是沒有意義的
使用async來重載非async的方法卻是合法的(只要方法簽名一致) - 使用了async修飾符的方法就是“異步函數”
異步方法如何執行
- 遇到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什么?
- 你await的表達式通常是一個task
- 也可以滿足下列條件的任意對象:
有GetAwaiter方法,它返回一個awaiter(實現了INotifyCompletion.OnCompleted接口)
返回適當類型的GetResult方法
一個bool類型的IsCompleted屬性
捕獲本地狀態
- await表達式最牛之處就是它幾乎可以出現在任何地方
- 特別的,在異步方法內,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之后在哪個線程上執行
- 在await表達式之后,編譯器依賴於continuation(通過awaiter模式)來繼續執行
- 如果在富客戶端的UI線程上,同步上下文會保證后續是在原線程上執行
- 否則,就會在task結束的線程上繼續執行
UI上的await
- 例子,建議這樣寫異步函數
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();
}
- 本例中,只有GetPeimesCountAsync中的代碼在worker線程上運行
- Go中的代碼會“租用”UI線程上的時間
- 可以說,Go是在消息循環中“偽並發”的執行
也就是說:它和UI線程處理的其它時間是穿插執行的
因為這種偽並發,唯一能發生“搶占”的時刻就是在await期間,這其實簡化了線程安全,防止重新進入即可 - 這種並發發生在調用棧較淺的地方(Task.Run調用的代碼里)
- 為了從該模型獲益,真正的並發代碼要避免訪問共享狀態或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());
}
- 整個同步調用圖都在worker線程上
- 必須在代碼中到處使用Dispatcher.BeginInvoke
- 循環本身在worker線程上
- 引入了race condition
- 若實現取消和過程報告,會使得線程安全問題更任意發生,在方法中新添加任何的代碼也是同樣的效果
編寫異步函數
- 對於任何異步函數,你可以使用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);
}
- 並不需要在方法體中顯式的返回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);
}
- 編譯器會對返回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;
}
- 因此,當返回Task的異步方法結束的時候,執行就會跳回到對它進行await的地方(通過continuation)
編寫異步函數,富客戶端場景下
- 富客戶端場景下,執行在此刻會跳回到UI線程(如果目前不在UI線程的話)
- 否則,就在continuation返回的任意線程上繼續執行
- 這意味着,在異步調用圖中向上冒泡的時候,不會發生延遲成本,除非是UI線程啟動的第一次“反彈”
返回Task
- 如果方法體返回TResult,那么異步方法就可以返回Task
例子
static async Task Main(string[] args)
{
}
static async Task<int> GetAnswerToLiife()
{
await Task.Delay(5000);
int answer = 21 * 2;
return answer;
}
- 其原理就是給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;
}
- 與同步編程很相似,是故意這樣設計的
同步版本
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#中如何設計異步函數
- 以同步的方式編寫方法
- 使用異步調用來替代同步調用,並且進行await
- 除了頂層方法外(UI控件的event handler,因為沒有await調用),把你方法的返回類型升級為Task或Task
,這樣它們就可以進行await了
編譯器能對異步函數生成Task意味着什么?
- 大多數情況下,你只需要在初始化IO-Bound並發的底層方法里顯式的初始化TaskCompletionSource,這種情況很少見
- 針對初始化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;
}
- 整個執行與之前同步例子中調用圖的執行順序是一樣的,因為我們對每個異步函數的調用都進行了await
- 在調用圖中創建了一個沒有並行和重疊的連續流
- 每個await在執行中都創建了一個間隙,在間隙后,程序可以從中斷處恢復執
並行(Parallelism)
- 不使用await來調用異步函數會導致並行執行的發生
- 例如:
_button.Click+=(sender,args)=>Go();
主線程仍然在執行,GO()也在執行
確實也能滿足保持UI響應的並發要求 - 同樣,可以並行跑兩個操作:
var task1=PrintAnswerToLife();
var task2=PrintAnswerToLife();
await task1;
await task2;
異步Lambda表達式
- 匿名方法(包括Lambda表達式),通過使用async也可以變成異步方法
- 調用方式也一樣
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");
}
- 附加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";
}
- 也可以返回Task
static async Task Main(string[] args)
{
Func<Task<int>> unnamed = async () =>
{
await Task.Delay(1000);
return 123;
};
int answer = await unnamed();
}