歡迎來到學習擺脫又加深內卷篇
下面是學習非同步程式設計的應用
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表示式完成,極其簡潔,初學者可以多多嘗試,不要避而遠之。