相信大家看過不少講C# async await的文章,部落格園就能搜到很多,但還是有很多C#程式設計師不明白。
如果搞不明白,其實也不影響使用。但有人就會疑惑,為什麼要用非同步?我感覺它更慢了,跟同步有啥區別?
有的人研究深入,比如去研究狀態機,可能會明白其中的原理。但深入研究的畢竟少數。有的人寫一些範例,有的專業有的不是很專業,但看的人仍有不明白的。
所以我寫了一個範例,儘量簡單,讓大家形象地理解一下C#的非同步。
就是一個範例,沒有什麼理論講解,大家自己測試自己思考。如果大家看理論講解,就能明白,我也就沒必要寫這個範例了。
先直接測試,看結果,下面再放程式碼
其它部落格也寫過範例,是控制檯範例,輸出的主執行緒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次點選是並行的,沒有增加耗時時長
https://gitee.com/s0611163/AsyncAwaitDemo2
最後,我畫了兩個流程圖,但這兩個流程圖可能畫的很不專業,所以僅供參考
button1點選5次的流程圖:
button2點選5次的流程圖:
我之前說過,非同步的本質是回撥,這句話是否正確我也不知道,主要是為了大家理解非同步。