C#進階——從應用上理解非同步程式設計的作用(async / await)

v朋朋發表於2022-01-08

歡迎來到學習擺脫又加深內卷篇

下面是學習非同步程式設計的應用

 

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表示式完成,極其簡潔,初學者可以多多嘗試,不要避而遠之。

 

相關文章