歡迎來到學習擺脫又加深內卷篇
下面是學習異步編程的應用
1.首先,我們建一個winfrom的項目,界面如下:
2.然后先寫一個耗時函數:
/// <summary> /// 耗時工作 /// </summary> /// <returns></returns> private string Work() { Thread.Sleep(1000); Thread.Sleep(2000); //listBox1.Items.Add("耗時任務完成"); return DateTime.Now.ToString("T") + "進入耗時函數里, 線程ID:" + Thread.CurrentThread.ManagedThreadId; //步驟7:子線程運行,不阻塞主線程 }
這里用當前線程睡眠來模擬耗時工作
3.同步實現方式:
private void button1_Click(object sender, EventArgs e) { listBox1.Items.Add(DateTime.Now.ToString("T") + "調用異步之前,線程ID:" + Thread.CurrentThread.ManagedThreadId); //步驟1:在主線程運行,阻塞主線程 TaskSync(); listBox1.Items.Add(DateTime.Now.ToString("T") + "調用異步之后,線程ID:" + Thread.CurrentThread.ManagedThreadId); //步驟2:在主線程運行,阻塞主線程 } /// <summary> /// 同步任務 /// </summary> private void TaskSync() { listBox1.Items.Add(DateTime.Now.ToString("T") + "同步任務開始,線程" + Thread.CurrentThread.ManagedThreadId); var resual = Work(); listBox1.Items.Add(resual); listBox1.Items.Add(DateTime.Now.ToString("T") + "同步任務結束,線程" + Thread.CurrentThread.ManagedThreadId); }
運行結果:
很明顯以上就是同步實現方法,在運行以上代碼時,會出現UI卡住了的現象,因為耗時工作在主線程里運行,所以UI一直刷新導致假死。
4.那么我們就會想到,可以開一個線程運行耗時函數,比如:
private void button4_Click(object sender, EventArgs e) { listBox1.Items.Add(DateTime.Now.ToString("T") + "獨立線程之前,線程" + Thread.CurrentThread.ManagedThreadId); ThreadTask(); listBox1.Items.Add(DateTime.Now.ToString("T") + "獨立線程之后,線程" + Thread.CurrentThread.ManagedThreadId); } /// <summary> /// 接收線程返回值 /// </summary> class ThreadParm { /// <summary> /// 接收返回值 /// </summary> public string resual = "耗時函數未執行完"; /// <summary> /// 線程工作 /// </summary> /// <returns></returns> public void WorkThread() { resual = Work(); } /// <summary> /// 耗時工作 /// </summary> /// <returns></returns> private string Work() { Thread.Sleep(1000); Thread.Sleep(2000); //listBox1.Items.Add("耗時任務完成"); return DateTime.Now.ToString("T") + "進入耗時函數里, 線程ID:" + Thread.CurrentThread.ManagedThreadId; //步驟7:子線程運行,不阻塞主線程 } } /// <summary> /// 獨立線程任務 /// </summary> private void ThreadTask() { listBox1.Items.Add(DateTime.Now.ToString("T") + "獨立線程任務開始,線程" + Thread.CurrentThread.ManagedThreadId); ThreadParm arg = new ThreadParm(); Thread th = new Thread(arg.WorkThread); th.Start(); //th.Join(); var resual = arg.resual; listBox1.Items.Add(resual); listBox1.Items.Add(DateTime.Now.ToString("T") + "獨立線程任務結束,線程" + Thread.CurrentThread.ManagedThreadId); }
運行結果如下
以上是開了一個線程運行耗時函數,用引用類型(類的實例)來接收線程返回值,主線程沒有被阻塞,UI也沒有假死,但結果不是我們想要的,
還沒等耗時函數返回,就直接輸出了結果,即我們沒有拿到耗時函數的處理的結果,輸出結果只是初始化的值
resual = "耗時函數未執行完";
為了得到其結果,可以用子線程阻塞主線程,等子線程運行完再繼續,如下:
th.Join();
這樣就能獲得到耗時函數的結果,正確輸出,但是在主線程掛起的時候,UI還是在假死,因此沒有起到優化的作用。
5.可以把輸出的結果在子線程(耗時函數)里輸出,那樣就主線程就不必輸出等其結果了,既能輸出正確的結果,又不會導致UI假死:
/// <summary> /// 耗時工作 /// </summary> /// <returns></returns> private void Work() { Thread.Sleep(1000); Thread.Sleep(2000); listBox1.Items.Add(("T") + "進入耗時函數里, 線程ID:" + Thread.CurrentThread.ManagedThreadId); //步驟7:子線程運行,不阻塞主線程 }
如上修改耗時函數(其他地方修改我就省略了)再運行,會報如下錯誤:
於是你會說,控件跨線程訪問,這個我熟呀!不就用在初始化時添加下面這句代碼嗎:
Control.CheckForIllegalCrossThreadCalls = false;
又或者用委托來完成。
確實可以達到目的,但是這樣不夠優雅,而且有時候非要等子線程走完拿到返回結果再運行下一步,所以就有了異步等待
6.異步實現方式:
/// <summary> /// 異步任務 /// </summary> /// <returns></returns> private async Task TaskAsync() { listBox1.Items.Add(DateTime.Now.ToString("T") + "異步任務開始,線程ID:" + Thread.CurrentThread.ManagedThreadId); //步驟3:在主線程運行,阻塞主線程 var resual = await WorkAsync(); //步驟4:在主線程運行,阻塞主線程 //以下步驟都在等待WorkAsync函數返回才執行,但在等待的過程不占用主線程,所以等待的時候不會阻塞主線程 string str = DateTime.Now.ToString("T") + resual + "當前線程:" + Thread.CurrentThread.ManagedThreadId; listBox1.Items.Add(str);//步驟10:在主線程運行,阻塞主線程 listBox1.Items.Add(DateTime.Now.ToString("T") + "異步任務結束,線程ID:" + Thread.CurrentThread.ManagedThreadId);//步驟11:在主線程運行,阻塞主線程 } /// <summary> /// 異步工作函數 /// </summary> /// <returns></returns> private async Task<string> WorkAsync() { listBox1.Items.Add(DateTime.Now.ToString("T") + "進入耗時函數前,線程" + Thread.CurrentThread.ManagedThreadId); //步驟5:在主線程運行,阻塞主線程 //拉姆達表達式開異步線程 //return await Task.Run(() => //{ // Thread.Sleep(1000); // //listBox1.Items.Add("計時開始:"); // Thread.Sleep(2000); // //listBox1.Items.Add("計時結束"); // return "耗時:" + 30; //}); //函數方式開異步現程 string str = await Task.Run(Work); //步驟6:這里開線程處理耗時工作,不阻塞主線程,主線程回到步驟3 //以下步驟都在等待Work函數返回才執行,但在等待的過程不占用主線程,所以等待的時候不會阻塞主線程 listBox1.Items.Add(DateTime.Now.ToString("T") + "出去異步函數前,線程" + Thread.CurrentThread.ManagedThreadId); //步驟9:主線程運行,阻塞主線程 return "運行時間" + str; //return await Task.Run(Work); } /// <summary> /// 耗時工作 /// </summary> /// <returns></returns> private string Work() { Thread.Sleep(1000); Thread.Sleep(2000); //listBox1.Items.Add("耗時任務完成"); return DateTime.Now.ToString("T") + "進入耗時函數里, 線程ID:" + Thread.CurrentThread.ManagedThreadId; //步驟7:子線程運行,不阻塞主線程 } private void button2_Click(object sender, EventArgs e) { listBox1.Items.Add(DateTime.Now.ToString("T") + "調用異步之前,線程" + Thread.CurrentThread.ManagedThreadId); //步驟1 TaskAsync();//步驟2:調用異步函數,阻塞主線程 listBox1.Items.Add(DateTime.Now.ToString("T") + "調用異步之后,線程" + Thread.CurrentThread.ManagedThreadId); }
運行結果如下:
以上就能滿足我們的需求,即不會卡UI,也能等待,且在等待結束后回到主線程運行。
其運行邏輯是:
網上很多人說異步是開了線程來等待完成的, 從上圖的時間軸來看,其並沒有開啟新的線程,都是同步往下執行。那為啥叫異步呢,因為執行到await時不發生阻塞,直接跳過等待去執行其他的,當await返回時,又接着執行await后面的代碼,這一系列的運行都是在主調線程中完成,並沒有開線程等待。所以如果耗時函數不開一個線程運行,一樣會阻塞,沒有完全利用異步的優勢。
那么,await是在主線程等待,那其為什么沒有阻塞主線程呢?我個人覺得其是利用委托的方式,后面再去揪原理吧!
其實異步編程很實用且優雅,特別結合lamda表達式完成,極其簡潔,初學者可以多多嘗試,不要避而遠之。