C#進階——從應用上理解異步編程的作用(async / await)


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

下面是學習異步編程的應用

 

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM