此入門教程是記錄下方參考資料視頻的過程
開發工具:Visual Studio 2019
目錄
C# 異步編程基礎(六)Continuation 繼續/延續 、TaskCompletionSource、實現 Task.Delay
C# 異步編程基礎(十) 取消(cancellation)、進度報告、TAP(Task-Based Asynchronous Pattern)、Task組合器
同步和異步
- 同步操作會在返回調用者之前完成它的工作
- 異步操作會在返回調用者之后去做它的(大部分)工作
異步發方法更為少見,會啟用並發,因為它的工作會與調用者並行執行
異步的方法通常很快(立即)就返回到調用者,所以叫非阻塞方法 - 目前見到的大部分的異步方法都是通用目的的:
Thread.Start
Task.Run
可以將Continuation附加到Task的方法
...
什么是異步編程
- 異步編程的原則是將長時間運行的函數寫成異步的
- 傳統的做法是將長時間運行的函數寫出同步的,然后從新的線程或Task進行調用,從而按需引入並發
- 上述異步方式的不同之處在於,它是從長時間運行函數的內部啟動並發。這有兩點好處:
IO-Bound並發可不使用線程來實現(用回調)。可提高可擴展和執行效率
富客戶端在worker線程會使用更少的代碼,簡化了線程安全性
異步編程的兩種用途
- 編寫高效處理大量並發IO的應用程序(典型的:服務端應用)
挑戰的並不是線程安全(因為共享狀態通常是最小化的),而是執行效率
特別的,每個網絡請求並不會消耗一個線程 - 在富客戶端應用里簡化線程安全
如果call graph(調用圖)中任何一個操作是長時間運行的,那么整個call graph必須運行在worker線程上,以保證UI響應
得到一個橫跨多個方法的單一並發操作(粗粒度)
需要為call graph中的每個方法考慮線程安全
異步的call graph,直到需要的時候才開啟一個線程,通常較淺(IO-Bound操作完全不需要)
其它的方法可以在UI線程執行,線程安全簡化
並發的粒度適中
一連串小的並發操作,操作之間會彈回到UI線程
經驗之談
- 為了獲得上述好處,下列操作建議異步編寫:
IO-Bound和Complute-Bound操作
執行超過50毫秒的操作 - 另一方面過細的粒度會損害性能,因為異步操作也有開銷
異步編程和Continuation 以及語言的支持
- Task非常適合異步編程,因為它們支持Continuation(它對異步非常重要)
第十六講里面TaskCompletionSource的例子
TaskCompletionSource是實現底層IO-Bound異步方法的一種標准方式 - 對於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)));
}
結果
語言對異步的支持非常重要
- 上述例子
- 需要對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)));
}
結果