.Net Core異步程式設計
一. 用async關鍵字修飾的方法
- 異步方法的傳回值一般是Task,T是真正的傳回值類型,Task.慣例:異步方法名字最好以Async結尾,這樣友善我們隻要是async
- 即使方法沒有傳回值,也最好吧傳回值聲明為非泛型的Task.
- 調用泛型方法時,一般在方法前加上await關鍵字,這樣拿到的傳回值就是泛型指定的T類型;
- 異步方法的"傳染性": 一個方法中如果await調用,則這個方法也必須修飾為async
static async Task Main(string[] args)
{
string fileName = "d:/1.txt";
File.Delete(fileName);
File.WriteAllTextAsync(fileName,"hello async");
string s = await File.ReadAllTextAsync(fileName);
Console.WriteLine(s);
}
static async Task Main(string[] args)
{
/*string fileName = @"F:\a\1.txt";
File.WriteAllText(fileName, "hello");
string s = File.ReadAllText(fileName);
Console.WriteLine(s);*/
string fileName = @"F:\a\1.txt";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++)
{
sb.AppendLine("hello");
}
//這裡如果不加await,會顯示異常 因為還沒寫入完成就開始讀取,讀取是獨占的
await File.WriteAllTextAsync(fileName, sb.ToString());
string s = await File.ReadAllTextAsync(fileName);
//Task<string> t = File.ReadAllTextAsync(fileName);
//string s = await t;
Console.WriteLine(s);
}
如果同樣的功能,既有同步方法,又有異步方法,那麼首先使用異步方法. .Net5中,很多架構的方法也都支援異步:Main,WinForm時間處理函數.
對于不支援異步方法該怎麼辦?
Wait()(無傳回值); Result(有傳回值) 風險:死鎖,盡量不用
static async Task Main(string[] args)
{
int l = await DownloadHtmlAsync("https://www.youzack.com", @"F:\a\1.txt");
Console.WriteLine("ok" + l);
}
/*static async Task DownloadHtmlAsync(string url, string fileame)
{
using (HttpClient httpClient = new HttpClient())
{
string html = await httpClient.GetStringAsync(url);
await File.WriteAllTextAsync(fileame, html);
}
}*/
static async Task<int> DownloadHtmlAsync(string url, string fileame)
{
using (HttpClient httpClient = new HttpClient())
{
string html = await httpClient.GetStringAsync(url);
await File.WriteAllTextAsync(fileame, html);
return html.Length;
}
}
二. await關鍵字修飾的方法
await調用的等待期間,.NET會把目前的線程傳回給線程池,等異步方法調用執行完畢後,架構會從線程池在取出來一個線程執行後續的代碼.
Thread.CurrentThread.ManageThreadId 獲得目前線程Id
驗證:在耗時異步(寫入大字元串)操作前後分别列印線程Id
static async Task Main(string[] args)
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
sb.Append("XXXXXXXXXXXXXXXXX");
}
await File.WriteAllTextAsync(@"F:\a\1.txt", sb.ToString());
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
}
優化:到要等待的時候,如果發現已經執行結束了,那就沒必要再切換線程了,剩下的代碼就繼續在之前的線程上就行執行了
三.異步方法不等于多線程
static async Task Main(string[] args)
{
Console.WriteLine("之前" + Thread.CurrentThread.ManagedThreadId);
double r = await CalcAsync(5000);
Console.WriteLine($"r = {r}");
Console.WriteLine("之後" + Thread.CurrentThread.ManagedThreadId);
}
public static async Task<double> CalcAsync(int n)
{
Console.WriteLine("CalcAsync," + Thread.CurrentThread.ManagedThreadId);
double result = 0;
Random rand = new Random();
for (int i = 0; i < n*n; i++)
{
result += rand.NextDouble();
}
return result;
}
結論: 異步方法的代碼并不會自動在新線程中執行,除非把代碼放到新線程中執行.
把要執行的代碼以委托的形式傳遞給Task().Run().這樣就會從線程池中出去一個線程執行我們的委托
await Task.Run(()=>{
//耗時操作代碼,可以傳回return傳回值
})
四. 沒有async的異步方法
async方法缺點:
- 異步方法會生成一個類,運作效率沒有普通方法高
- 可能占用非常多的線程
static async Task Main(string[] args)
{
string s = await ReadAysnc(1);
Console.WriteLine(s);
}
/*static async Task<string> ReadAysnc(int num)
{
if (num == 1)
{
string s = await File.ReadAllTextAsync(@"F:\a\1.txt");
return s;
}else if (num == 2)
{
string s = await File.ReadAllTextAsync(@"F:\a\2.txt");
return s;
}
else
{
throw new ArgumentException();
}
}*/
static Task<string> ReadAysnc(int num)
{
if (num == 1)
{
return File.ReadAllTextAsync(@"F:\a\1.txt");
}
else if (num == 2)
{
return File.ReadAllTextAsync(@"F:\a\2.txt");
}
else
{
throw new ArgumentException();
}
}
從上面代碼分析來看,隻甩手Task,不"拆完了再裝"反編譯上面的代碼: 隻是普通方法調用
優點: 運作效率更高,不會造成線程浪費
傳回值為Task的不一定都要标注async,标注async隻是讓我們可以更友善的await而已
如果一個異步方法隻是對别的異步方法調用的轉發,并沒有太多複雜的邏輯(比如等待A的結果,再調用B;把A調用的傳回值拿到内部做一些處理再傳回),那麼就可以去掉async關鍵字.
五.暫停調用方法使用
如果想在異步方法中暫停一段時間,不要用Thread.Sleep(),因為它會阻塞調用線程,而要用await Task.Delay().
舉例:下載下傳一個網址,3秒後下載下傳另一個
namespace 異步休息
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private async void button1_Click(object sender, EventArgs e)
{
using (HttpClient httpClient = new HttpClient())
{
string s1 = await httpClient.GetStringAsync("https://www.youzack.com");
textBox1.Text = s1.Substring(0, 100);
//Thread.Sleep(3000);
await Task.Delay(3000);
string s2 = await httpClient.GetStringAsync("https://www.baidu.com");
textBox1.Text = s2.Substring(0, 100);
}
}
}
}
在控制台中沒看到差別,但是放到WinForm程式中就看到差別了,ASP.NET Core中也看到差別,但是Sleep()會降低并發.
六.CancellationToken參數
有時需要提前終止任務,比如:請求逾時,使用者取消請求.
很多異步方法都有CancellationToken參數,使用者獲得提前終止執行的信号.
CancellationToken結構體
none:空
bool isCancellationRequested 是否取消
(*)Register(Action callback) 注冊取消監聽
ThrowCancellationRequested() 如果任務被取消,執行到這句話就抛異常
CancellationTokenSource
CancelAfter()逾時後發出取消信号
Cancel()發出取消信号
CancellationToken Token
為"下載下傳一個網址N次"的方法增加取消功能,分别用GetStringAsync + isCancellationRequested,GetStringAsync + ThrowIfCancellationRequested(),帶 CancellationToken的GetStringAsync ()分别實作.取消分用逾時,使用者敲按鍵(不能await)實作.
class Program
{
static async Task Main1(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(5000);
await Download2sAsync("https://www.baidu.com", 100, cts.Token);
while (Console.ReadLine() != "q")
{
}
cts.Cancel();
Console.ReadLine();
}
static async Task Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(5000);
await Download2sAsync("https://www.baidu.com", 100,cts.Token);
}
/*static async Task Download1sAsync(string url, int n)
{
using (HttpClient httpClient = new HttpClient())
{
for (int i = 0; i < n; i++)
{
string html = await httpClient.GetStringAsync(url);
Console.WriteLine($"{DateTime.Now}:{html}");
}
}
}*/
static async Task Download2sAsync(string url, int n,CancellationToken cancellationToken)
{
using (HttpClient httpClient = new HttpClient())
{
for (int i = 0; i < n; i++)
{
string html = await httpClient.GetStringAsync(url);
Console.WriteLine($"{DateTime.Now}:{html}");
await Task.Delay(500);
if (cancellationToken.IsCancellationRequested)
{
Console.WriteLine("請求被取消");
break;
}
}
}
}
}
ASP.NET Core開發中,一般不需要自己處理CancellationToken,CancellationTokenSource這些,隻要做到"能轉發CancellationToken就轉發即可".ASP.NET Core會對于使用者請求中斷進行處理.
七.Task類的重要方法
- Task WhenAny(IEnumerable tasks)等,任何一個Task完成,Task就完成
- Task<TResult[]> WhenAll(params Task[] tasks)等,所有Task完成,Task才完成,用于等待多個任務執行結束,但是不在乎他們的執行順序.
- FromResult()建立普通數值的Task對象.
八.其他問題
1.接口中的異步方法:
async是提示編譯器為異步方法中的await代碼進行分段處理的,而一個異步方法是否修飾了async對于方法的調用者來講沒差別的,是以對于接口中的方法或者抽象方法不能修飾為async
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
interface ITest
{
Task<int> GetCharCount(string file);
}
class Test : ITest
{
public async Task<int> GetCharCount(string file)
{
string s = await File.ReadAllTextAsync(file);
return s.Length;
}
}
}
2.異步與yield
回顧:yield return不僅能夠簡化資料的傳回,而且可以讓資料處理"流水線化",提升性能
static IEnumerable<string> Test1()
{
List<string> list = new List<string>();
list.Add("hello1");
list.Add("hello2");
list.Add("hello3");
return list;
}
static IEnumerable<string> Test2()
{
yield return "hello1";
yield return "hello2";
yield return "hello3";
}
效果是一樣的.
在舊版C#中,async方法中不能用yield.從C# 8.0開始,吧傳回值聲明為IAsyncEnumerable(不要帶Task),然後周遊的時候await foreach()即可
static async Task Main(string[] args)
{
await foreach (var s in Test3())
{
Console.WriteLine(s);
}
}
static async IAsyncEnumerable<string> Test3()
{
yield return "hello1";
yield return "hello2";
yield return "hello3";
}
ASP.NET Core和控制台項目中沒有SynchronizationContext,是以不用管ConfigureAwait(false)等.不要同步,異步混用