非同步 TAP

C# 非同步程式設計概述

邱俞凱 2019/11/05 22:45:47
89

在沒有非同步的程式設計裡,同步的程式執行都是使用同一個執行緒並且由上而下的順序在執行,例如:

一個人要做一份早餐,早餐菜單上有烤吐司夾火腿蛋,並且配上一杯咖啡,同步的程式會這樣寫

static void Main(string[] args)
        {
            var watch = Stopwatch.StartNew();
            Console.WriteLine("開始早餐製作...");
            平底鍋預熱();
            烤吐司();
            煎火腿蛋();
            抹果醬();
            倒咖啡();
            吐司夾火腿蛋();
            擺盤完成開始吃早餐();
            watch.Stop();

            Console.WriteLine($"同步烹煮早餐共花費:{watch.Elapsed.Seconds} 秒");
            Console.ReadKey();
        }

        static void 平底鍋預熱()
        {
            Console.WriteLine($"預熱平底鍋 5 秒鐘 [Thread ID:{Thread.CurrentThread.ManagedThreadId}]");
            Thread.Sleep(5000);
            Console.WriteLine($"預熱完成");
        }

        static void 烤吐司()
        {
            Console.WriteLine($"烤吐司 10 秒鐘 [Thread ID:{Thread.CurrentThread.ManagedThreadId}]");
            Thread.Sleep(10000);
            Console.WriteLine($"烤吐司完成");
        }

        static void 煎火腿蛋()
        {
            Console.WriteLine($"煎火腿蛋 20 秒鐘 [Thread ID:{Thread.CurrentThread.ManagedThreadId}]");
            Thread.Sleep(20000);
            Console.WriteLine($"煎火腿蛋完成");
        }

        static void 抹果醬()
        {
            Console.WriteLine($"抹果醬需要 2 秒鐘 [Thread ID:{Thread.CurrentThread.ManagedThreadId}]");
            Thread.Sleep(2000);
            Console.WriteLine($"抹果醬完成");
        }

        static void 倒咖啡()
        {
            Console.WriteLine($"倒咖啡需要 5 秒鐘 [Thread ID:{Thread.CurrentThread.ManagedThreadId}]");
            Thread.Sleep(5000);
            Console.WriteLine($"倒咖啡完成");
        }
        static void 吐司夾火腿蛋()
        {
            Console.WriteLine($"吐司夾火腿蛋需要 3 秒鐘 [Thread ID:{Thread.CurrentThread.ManagedThreadId}]");
            Thread.Sleep(3000);
            Console.WriteLine($"吐司火腿蛋完成");
        }

        static void 擺盤完成開始吃早餐()
        {
            Console.WriteLine($"擺盤上桌需要 2 秒鐘 [Thread ID:{Thread.CurrentThread.ManagedThreadId}]");
            Thread.Sleep(2000);
            Console.WriteLine($"早餐可以吃了");
        }

 

同步的程式執行結果如下

 

 

執行完後我們發現每,因為都是使用同一個執行緒,所以每個步驟都必須執行完後,才能進到下一個步驟去,因此做這份早餐,總共花費了 47秒。

但現實生活中,我們做早餐並不會一個步驟做完後才會做下一個步驟, 例如: 當平底鍋在預熱的時候,會同時把吐司丟進烤箱烤,

鍋子預熱好了,把火腿蛋下鍋煎,這時候吐司已經烤好的話,會先把吐司抹上果醬後,再回頭把火腿蛋煎好起煎夾進吐司,

倒好咖啡擺上盤子,便可以享用這份豐富的早餐。

由上所描述做早餐同時會做多個步驟,就是一種非同步的表現。

在把上面程式改成非同步的方式前,先來看看幾個非同步程式的觀念。

 

什麼是非同步程式設計

  • 一種並行 Concurrent 處理
  • 當呼叫非同步方法之後,將會立即返回 (return)  呼叫端
  • 承諾未來將一定會完成這個方法並且得到結果
  • 不一定需要透過執行緒才能夠做到非同步作業

 

非同步有四個重要名詞

 

非同步開發的歷史

  • .NET Framework 1.1 - 以 APM 進行非同步開發
  • .NET Framework 2.0/3.5 - 以 EAP 在不同執行緒上執行作業
  • .NET Framework 4.0 - 以 TAP  進行以工作為基礎的非同步開發
  • .NET Framework 4.5(C# 5.0+) / .NET Core 1.0+ - 透過 async / await 語法糖使用 TAP 非同步模式

 

什麼是「工作」(Task)

  • Task 類別代表單一作業不會傳回值,通常以非同步方式執行
  • 最推薦的非同步開發方法
  • 使用單一方法表示同步作業啟始完成

 

把開頭做早餐的例子使用 Task 方法簡單的讓做早餐這件事有了非同步的效果,並且看看非同步帶了什麼樣的效益。

 

static void Main(string[] args)
        {
            var watch = Stopwatch.StartNew();
            Console.WriteLine("開始烹煮早餐...");
            var 預熱 = Task.Run(() => 平底鍋預熱());
            var 香烤吐司 = Task.Run(() => 烤吐司());

            預熱.Wait();
            var 香煎火腿蛋 = Task.Run(() => 煎火腿蛋());

            香烤吐司.Wait();
            抹果醬();
            倒咖啡();

            香煎火腿蛋.Wait();
            吐司夾火腿蛋();
            擺盤完成開始吃早餐();
            watch.Stop();

            Console.WriteLine($"非同步烹煮早餐共花費:{watch.Elapsed.Seconds} 秒");
            Console.ReadKey();
        }

        static void 平底鍋預熱()
        {
            Console.WriteLine($"預熱平底鍋 5 秒鐘 [Thread ID:{Thread.CurrentThread.ManagedThreadId}]");
            Thread.Sleep(5000);
            Console.WriteLine($"預熱完成");
        }

        static void 烤吐司()
        {
            Console.WriteLine($"烤吐司 10 秒鐘 [Thread ID:{Thread.CurrentThread.ManagedThreadId}]");
            Thread.Sleep(10000);
            Console.WriteLine($"烤吐司完成");
        }

        static void 煎火腿蛋()
        {
            Console.WriteLine($"煎火腿蛋 20 秒鐘 [Thread ID:{Thread.CurrentThread.ManagedThreadId}]");
            Thread.Sleep(20000);
            Console.WriteLine($"煎火腿蛋完成");
        }

        static void 抹果醬()
        {
            Console.WriteLine($"抹果醬需要 2 秒鐘 [Thread ID:{Thread.CurrentThread.ManagedThreadId}]");
            Thread.Sleep(2000);
            Console.WriteLine($"抹果醬完成");
        }

        static void 倒咖啡()
        {
            Console.WriteLine($"倒咖啡需要 5 秒鐘 [Thread ID:{Thread.CurrentThread.ManagedThreadId}]");
            Thread.Sleep(5000);
            Console.WriteLine($"倒咖啡完成");
        }
        static void 吐司夾火腿蛋()
        {
            Console.WriteLine($"吐司夾火腿蛋需要 3 秒鐘 [Thread ID:{Thread.CurrentThread.ManagedThreadId}]");
            Thread.Sleep(3000);
            Console.WriteLine($"吐司火腿蛋完成");
        }

        static void 擺盤完成開始吃早餐()
        {
            Console.WriteLine($"擺盤上桌需要 2 秒鐘 [Thread ID:{Thread.CurrentThread.ManagedThreadId}]");
            Thread.Sleep(2000);
            Console.WriteLine($"早餐可以吃了");
        }

 

執行步驟的寫法不變,但是呼叫步驟方法改用 Task 進行非同步的程式執行。

 

這次使用了非同步的方式做早餐,在遇到可以非同步進行的程式裡,可以看到使用了不同的執行緒在進行,

因此只花費了30秒的時間,幾乎一樣的程式,但執行的結果卻相差 17 秒之多,這就是非同步的魅力。

邱俞凱