前言
相信大家看過不少講C# async await的文章,部落格園就能搜到很多,但還是有很多C#程式設計師不明白。
如果搞不明白,其實也不影響使用。但有人就會疑惑,為什麼要用非同步?我感覺它更慢了,跟同步有啥區別?
有的人研究深入,比如去研究狀態機,可能會明白其中的原理。但深入研究的畢竟少數。有的人寫一些示例,有的專業有的不是很專業,但看的人仍有不明白的。
所以我寫了一個示例,儘量簡單,讓大家形象地理解一下C#的非同步。
就是一個示例,沒有什麼理論講解,大家自己測試自己思考。如果大家看理論講解,就能明白,我也就沒必要寫這個示例了。
示例的測試步驟
先直接測試,看結果,下面再放程式碼
- 點選VS2022的啟動按鈕,啟動程式,它會先啟動Server工程,再啟動AsyncAwaitDemo2工程
- 快速連續點選5次button1(呼叫的是同步方法)
- 快速連續點選5次button2(呼叫的是非同步方法)
- 觀察思考輸出結果,體會非同步的好處
測試截圖
測試截圖說明
- 5次點選button1,介面是卡住的,但不影響輸出結果,5次點選button2介面不會卡住(截圖看不出來,測試過程中有體現)
- 5次點選button1,共5次同步請求,每個請求耗時2秒,一共耗時10秒,嫌慢,想要併發請求?那你可能要使用Task.Run,線上程中呼叫同步方法,這又涉及到執行緒佔用問題
- 5次點選button2,共5次非同步請求,每個請求耗時2秒,但一共耗時只有2秒,注意,程式碼中請求web api介面使用Thread類了嗎?沒有!使用Task.Run了嗎?沒有!使用了async await語法糖,是不是比透過new Thread或Task.Run要簡單多了?就像寫同步程式碼那樣,卻實現了非同步併發的效果。
一個問題
其它部落格也寫過示例,是控制檯示例,輸出的主執行緒ID,是有可能會變化的。這裡是Winform示例,輸出的主執行緒ID始終是UI執行緒的ID,即是1;
我沒有演示主執行緒ID發生變化的情況,這樣會使示例變得複雜一點,不容易看明白。
服務端
服務端和客戶端是兩個獨立的工程,測試時在一起跑,但其實可以分開部署,部署到不同的機器上
服務端是一個web api介面,用.NET 6、VS2022開發,程式碼如下:
[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
[HttpGet]
[Route("[action]")]
public async Task<string> ForPreheat()
{
return await Task.FromResult("預熱返回結果");
}
[HttpGet]
[Route("[action]")]
public async Task<string> Get()
{
await Task.Delay(2000); //模擬耗時操作
return "測試返回結果";
}
}
客戶端
大家看客戶端程式碼時,不需要關心服務端怎麼寫
客戶端是一個Winform工程,用.NET 6、VS2022開發,程式碼如下:
public partial class Form1 : Form
{
private string _urlPreheat = "http://localhost:5028/Test/ForPreheat";
private string _url = "http://localhost:5028/Test/Get";
private int _index1 = 1; //測試時請快速連續點選5次button1
private int _index2 = 1; //測試時請快速連續點選5次button2
private Stopwatch _sw1;
private Stopwatch _sw2;
public Form1()
{
InitializeComponent();
}
private async void Form1_Load(object sender, EventArgs e)
{
Request(_urlPreheat); //預熱
await RequestAsync(_urlPreheat); //預熱
Log("==== 預熱請求結束 ====================================");
}
#region Log
private void Log(string msg)
{
msg = $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {msg}\r\n";
if (this.InvokeRequired)
{
this.BeginInvoke(new Action(() =>
{
txtLog.AppendText(msg);
}));
}
else
{
txtLog.AppendText(msg);
}
}
#endregion
private async void button1_Click(object sender, EventArgs e)
{
await Task.Delay(1000); //為了能1秒內快速點選幾次,為了能寫這句程式碼button1_Click前面加了async,但後面的程式碼仍是同步呼叫;如果不加這句,第一次點選介面就卡死了,無法連續點選5次。
Log("==== 同步請求開始 ====================================");
if (_index1 == 1) _sw1 = Stopwatch.StartNew();
Request(_url);
if (_index1 == 5)
{
_sw1.Stop();
Log($"5次請求耗時:{_sw1.Elapsed.TotalSeconds:0.000}秒");
}
_index1++;
}
private async void button2_Click(object sender, EventArgs e)
{
await Task.Delay(1000); //為了和button1保持一致
Log("==== 非同步請求開始 ====================================");
if (_index2 == 1) _sw2 = Stopwatch.StartNew();
await RequestAsync(_url);
if (_index2 == 5)
{
_sw2.Stop();
Log($"5次請求耗時:{_sw2.Elapsed.TotalSeconds:0.000}秒");
}
_index2++;
}
private async Task RequestAsync(string url)
{
Stopwatch sw = Stopwatch.StartNew();
HttpClient httpClient = HttpClientFactory.GetClient();
var result = await (await httpClient.GetAsync(url)).Content.ReadAsStringAsync();
sw.Stop();
Log($"執行緒ID={Thread.CurrentThread.ManagedThreadId},請求耗時:{sw.Elapsed.TotalSeconds:0.000}秒,返回內容長度:{result.Length}");
}
private void Request(string url)
{
Stopwatch sw = Stopwatch.StartNew();
var result = HttpUtil.HttpGet(url);
sw.Stop();
Log($"執行緒ID={Thread.CurrentThread.ManagedThreadId},請求耗時:{sw.Elapsed.TotalSeconds:0.000}秒,返回內容長度:{result.Length}");
}
}
思考
把button2_Click方法中的await RequestAsync(_url);修改為兩行:
await RequestAsync(_url);
await RequestAsync(_url);
那麼5次點選,每次點選請求2次,共5×2=10次請求,請問這10次請求的總耗時大約是幾秒?
答案是4秒,為什麼?
因為每次點選的2次請求是順序的,耗時是2+2=4秒,而5次點選是併發的,沒有增加耗時時長
Demo完整程式碼
https://gitee.com/s0611163/AsyncAwaitDemo2
流程圖
最後,我畫了兩個流程圖,但這兩個流程圖可能畫的很不專業,所以僅供參考
button1點選5次的流程圖:
button2點選5次的流程圖:
我之前說過,非同步的本質是回撥,這句話是否正確我也不知道,主要是為了大家理解非同步。