『審慎』.Net4.6 Task 非同步函數 比 同步函數 慢5倍 踩坑經歷

来源:https://www.cnblogs.com/shuxiaolong/archive/2018/07/24/DotNet_Task_BUG.html
-Advertisement-
Play Games

近來,有項目需要使用到 DotNetty 這個非同步Socket框架。 這個框架是 微軟團隊 移植的 Java的 Netty —— 而且還能與 Java 現有的 Netty 對接。 Netty 如何的牛逼 我就不多介紹了。 DotNetty 基於 .Net 4.3 (實際至少需要 .Net 4.... ...


非同步Task簡單介紹

本標題有點 嘩眾取寵,各位都別介意(不排除個人技術能力問題) —— 接下來:我將會用一個小Demo 把 本文思想闡述清楚。

.Net 4.0 就有了 Task 函數 —— 非同步編程模型

.Net 4.6 給 Task 增加了好幾個 特別實用的方法,而且引入了 await async 語法糖

當然,這是非常不錯的技術,奈何我有自己的線程隊列封裝,也就沒有著急使用這個東西。

終究入局 Task非同步函數

近來,有項目需要使用到 DotNetty 這個非同步Socket框架。

這個框架是 微軟團隊 移植的 Java的 Netty —— 而且還能與 Java 現有的 Netty 對接。

Netty 如何的牛逼 我就不多介紹了。

DotNetty 基於 .Net 4.3 (實際至少需要 .Net 4.5) —— 是的,你沒有看錯,是 .Net 4.3

好了,跟著我一起踩坑,一起學些 非同步Task函數的 使用規範。

先看一個最簡單的 Demo,領教一下 Task 的非同步威力

 1     static void Main(string[] args)
 2     {
 3         //模擬一個業務需求: 有 200 個字元串需要處理
 4         List<string> list = new List<string>();
 5         for (int i = 0; i < 200; i++) list.Add("AAAA" + i);
 6 
 7 
 8 
 9         DateTime time0 = DateTime.Now;
10         
11         //用多個Task 處理這些字元串
12         List<Task> listTask = new List<Task>();
13         foreach (string item in list)
14         {
15             Task task = Task.Run(() =>
16             {
17                 Handle(item); //執行一個方法, 處理這200多個字元串
18             });
19             listTask.Add(task);
20         }
21         Task.WaitAll(listTask.ToArray()); //等待200個字元串 都處理完成
22 
23         DateTime time1 = DateTime.Now;
24 
25 
26 
27         Console.WriteLine("200個字元串處理完成, 同步執行需要200秒, 實際Task執行耗時: " + (time1 - time0).TotalSeconds + "");
28 
29         
30 
31     }
32 
33 
34     public static void Handle(string item)
35     {
36         Thread.Sleep(1000); //處理耗時1秒
37         Console.WriteLine("處理 " + item);
38     }

 

業務的處理邏輯沒這麼簡單

實際上,我們有 AAAA0 ~ AAAA199 總計 200 個字元串

但是,實際處理字元串 需要一個 StrHandler 類。

並且,StrHandler 有個屬性 Type, 如果 StrHandler.Type==1,則這個 StrHandler 就只能處理 AAAA1 AAAA11 .... AAAA191 這些以 1 結尾的字元串

那麼:200個 字元串 就需要 最少10個 StrHandler 來處理。 理論:200個 字元串,創建 200個 StrHandler 來處理 不就得了? 但是:StrHandler 的 構造函數有一些 初始化操作,非常耗時,需要 5秒。

我們先看一下 new 200 個 StrHandler 會有多慢。 如果使用同步函數,那就是 (1+5)*200 = 1200 秒

 1 class Program
 2 {
 3     static void Main(string[] args)
 4     {
 5         //模擬一個業務需求: 有 200 個字元串需要處理
 6         List<string> list = new List<string>();
 7         for (int i = 0; i < 200; i++) list.Add("AAAA" + i);
 8 
 9 
10 
11         DateTime time0 = DateTime.Now;
12         
13         //用多個Task 處理這些字元串
14         List<Task> listTask = new List<Task>();
15         foreach (string item in list)
16         {
17             Task task = Task.Run(() =>
18             {
19                 Handle(item); //執行一個方法, 處理這200多個字元串
20             });
21             listTask.Add(task);
22         }
23         Task.WaitAll(listTask.ToArray()); //等待200個字元串 都處理完成
24 
25         DateTime time1 = DateTime.Now;
26 
27 
28 
29         Console.WriteLine("200個字元串處理完成, 同步執行需要200秒, 實際Task執行耗時: " + (time1 - time0).TotalSeconds + "");
30 
31         
32 
33     }
34 
35 
36     public static void Handle(string item)
37     {
38         //字元串最末位的數字 就是 StrHandler 的 Type 值
39         int temp = Convert.ToInt32(item.Substring(item.Length - 1));            
40 
41         StrHandler handler = new StrHandler(temp);
42         handler.Handle(item);
43     }
44 
45 }
46 
47 //字元串的 處理類
48 public class StrHandler
49 {
50     public StrHandler(int type)
51     {
52         Type = type;
53         Thread.Sleep(5000); //創建一個 StrHandler 需要5秒
54     }
55 
56     public int Type { get; set; }
57 
58     
59     public void Handle(string item)
60     {
61         Thread.Sleep(1000);  //函數本身調用需要 1秒鐘
62         Console.WriteLine("處理器 {0} 處理字元串 {1}", Type, item);
63     }
64 }

復用 StrHandler 減少開銷

因為 StrHandler 需要創建 Tcp 通訊通道,開闢多個將占用不必要的網路埠。 200個字元串,最少需要 10個 StrHandler —— 所以:我們就只創建 10個 StrHandler。

我們修改 static Handle() 函數如下

 1     //public static void Handle(string item)
 2     //{
 3     //    int temp = Convert.ToInt32(item.Substring(item.Length - 1)); //字元串最末位的數字 就是 StrHandler 的 Type 值
 4 
 5     //    StrHandler handler = new StrHandler(temp);
 6     //    handler.Handle(item);
 7     //}
 8 
 9     public static void Handle(string item)
10     {
11         StrHandler handler = GetHandler(item);
12         handler.Handle(item);
13     }
14 
15     private static Hashtable hash = Hashtable.Synchronized(new Hashtable());
16 
17     //不同的字元串有不同的 StrHandler
18     //StrHandler 是一種昂貴資源, 初始化 StrHandler 需要5秒, 所以需要 對 GetHandler 進行緩存
19     private static StrHandler GetHandler(string item)
20     {
21         //lock (hash) //加不加lock 不影響 本文最終理論
22         {
23             int temp = Convert.ToInt32(item.Substring(item.Length - 1));
24 
25             StrHandler handler = hash[temp] as StrHandler;
26             if (handler != null) return handler;
27 
28             //如果沒有緩存, 則創建 StrHandler
29             handler = new StrHandler(temp);
30             hash[temp] = handler;
31             return handler;
32         }
33     }

 

我們使用 Hashtable 緩存了 StrHanlder 類 —— 再看一下性能

StrHandler 初始化 DotNetty 通訊

上面的幾次演變,把性能逐步提高了不少。 業務要求: StrHandler 需要和 DotNetty 通訊,在 調用 StrHandler 的 Handle(string) 之前,就必須讓 DotNetty 完成初始化。

看一下改進後的 StrHandler

 1 //字元串的 處理類
 2 public class StrHandler
 3 {
 4     public StrHandler(int type)
 5     {
 6         Type = type;
 7         DotNetty = new DotNetty();
 8 
 9         //Thread.Sleep(5000);
10         DotNetty.Start();  //耗時的 5秒, 其實就是 DotNetty 的時間消耗 //調用的是假設的同步方法
11     }
12 
13     public int Type { get; set; }
14     public DotNetty DotNetty { get; set; }  //增加了 DotNetty 的類(這可是一個重量級對象,可不是 說new就new 的)
15 
16     //函數本身調用需要 1秒鐘
17     public void Handle(string item)
18     {
19         if (!DotNetty.Active)
20             throw new Exception(string.Format("{0} DotNetty 沒有激活, 無法執行 Handle", Type));
21 
22         Thread.Sleep(1000);
23         Console.WriteLine("處理器 {0} 處理字元串 {1}", Type, item);
24     }
25 }
26 我們再看一下 DotNetty 的定義(模擬定義)
27 
28 //以下代碼模擬 DotNetty 框架 —— 這個框架 只提供了 非同步Task 方法 StartAsync();
29 //所以: StartAsync() 定義不能修改
30 public class DotNetty
31 {
32     //DotNetty 只提供了 非同步函數
33     public async Task StartAsync()
34     {
35         await Task.Run(() =>
36         {
37             //DotNetty 是一個著名的通訊框架, 正常情況下 初始化只需要1秒。
38             //但 特殊情況下,初始化需要 5秒 (比如 目標的 IP埠 壓根不存在)
39             Thread.Sleep(5000);
40         }); 
41 
42         Active = true; //DotNetty 初始化完成還有, 將 DotNetty 置為激活狀態
43     }
44     
45     //假設給 DotNetty 提供一個 同步的 Start() 方法
46     //實際上: DotNetty 沒有這個同步方法
47     public void Start()
48     {
49         Thread.Sleep(5000);
50         Active = true; //DotNetty 初始化完成還有, 將 DotNetty 置為激活狀態
51     }
52 
53     public bool Active { get; set; }
54 }

 

DotNetty 不提供 Start() 方法,我們假設增加一個 同步方法 Start()

—— 這次測試的是 假設有個 同步函數 Start() 的性能。

DotNetty 只提供非同步Task方法 StartAsync()

我們上面也說了,DotNetty 只提供 StartAsync() 這個方法。

我們剛纔模擬的 Start() 是不存在的。

這時候,有經驗的小伙伴 一定能指出來:

沒有提供同步函數,我們可以把 非同步Task 函數 封裝成 同步函數啊!

說的很對,我們可以給 DotNetty 擴展一個 Start() 方法,

1 public static class Extend
2 {
3     public static void Start(this DotNetty dotNetty)
4     {
5         Task task = dotNetty.StartAsync();
6         task.Wait(); //讓 非同步Task 等待完成, 這不就是一個 同步方法了麽?
7     }
8 }

為了證實猜想,我還特意 寫了個 測試代碼。

1     static void Main(string[] args)
2     {
3         DotNetty dotNetty = new DotNetty();
4         dotNetty.Start();
5         Console.WriteLine("DotNetty.Active : " + dotNetty.Active);
6     }

增加了擴展方法之後,程式編譯通過了

正式運行

本文總結

本文,通過一個簡單的 Demo,演示了 如何將 Task非同步編程 搞死的案例。

終究得出瞭如下結論:

Task非同步函數 通過 Wait() 封裝的 偽同步函數 是靠不住的。

Task.WaitAll() 函數 是最大的坑 (這是 .Net 4.6 新增加的函數?)

DotNetty 不提供 同步函數 Start(),只提供 StartAsync() 是不厚道的。

建議:所有底層庫,你可以有 Task函數,但請保留 同步函數。

絕不小心求證、只管大膽胡說

這個段落,可以當作開玩笑 —— 各位不要較真。

PS. 
以前我們寫函數, 會準備 同步函數、回調函數
.Net 4.5 後, 引入了 非同步函數模型

上面的案例中, 我們看到: 一個非同步Task 方法, 既能當 回調函數用, 又能當同步函數用
—— 你或許覺得: 非同步Task方法 好強大


但是, 警告建議:

無數網友(包括大神) 非同步Task 踩坑經歷, 包括自己這次的 踩坑, 
都得出了一個結論:  非同步Task只能一條道走到黑

即: 你一個地方使用了 非同步Task, 其他引用的地方 往上, 都得改成 非同步.
你反駁: 我可以把 非同步Task 封裝成 一個同步函數啊, task.Wait(); return task.Result;
—— 這就是你踩坑的開始: 你可能會看到 線程飆升到 900個, 但是 CPU利用率為 0% 
—— 於是最後, 你就會回到最開始的建議: 非同步Task只能一條道走到黑
—— 900個線程, CPU 0%, 如何查錯就是問題了: 每一個函數都看起來沒問題(是真的沒問題), 串在一起運行後 就假死
—— 你以為是 死鎖? 可能幾分鐘後 900個線程 全部瞬間又運行起來了 
—— 並沒有死鎖, 似乎就是 Task 內核實現的一種資源分配 BUG(為了不導致死鎖,所以才飆升900個線程的資源分配方案)


Task非同步函數 是強大的, 但請不要濫用

同步函數 封裝成 非同步函數 不會有任何問題
但是Task非同步函數 封裝成 同步函數(就是偽同步函數) —— 這會是你噩夢的開始

有個直覺猜想(可能不正確):
A() 是個內核非同步Task函數 開闢了10個Task做事(做完就全部釋放),
B() 是個底層非同步Task函數, 因為某些原因, B() 調用 A()的偽同步函數, B開闢了10個Task做事  
C() 是個上層調用函數, 他調用了 B()的偽同步函數, C開闢了10個Task做事
—— 最後,一旦假死發生, 線程數或許會等於  10*10*10 = 1000 個, 或者 2000 個
—— 假死發生時的 線程數, 和非同步Task的偽同步函數 嵌套層次 有關係
—— 設想一下, A() 引用了 NLog 這種更內核的庫, 哪天 NLog 作者將自己的代碼改成了非同步Task, A() 為了代碼改動最小, 封裝了一個 NLog 的 偽同步函數(保持了之前代碼調用的一致性), 假設NLog開闢了5個Task
5*10*10*10 = 5000 個線程 估計是逃不掉了  【個人亂猜,都別介意】


日月輪轉、滄海桑田 —— 可以提供 Task非同步函數, 但儘量同時保留 同步函數(尤其是底層框架)

 

 

寫在最後的話

總有程式員,能理解 同步函數、勉強理解 回調函數,完全不懂 非同步函數 —— 完全是在模仿著寫 await async。

作為一個底層 架構師,如果你的底層 都是高大上 的非同步函數 —— 會不會讓使用你的框架的 開發人員 也遇到今天這樣的 BUG呢?

同步函數 就像 親力親為, 回調函數 就像 軟體外包, 非同步函數 就像 管理一家大公司 —— 人多力量大的同時,如果管理不好,就可能發生 一件小事 幾個人做,結果還扯皮 的尷尬 ~

2014年開始, NoSQL 甚囂塵上, 我也在那年發表了一個開源工具 Laura.SqlForever。

幾年過去了,SQL Server、Oracle、MySql 毫無波動 —— 終究:Sql Forever。

當年,好多公司,招聘的時候都各種 NoSQL 技術。

甚至有人 提出:將現有業務 全部遷移到 NoSQL —— 如今、成功的有幾個呢?還是說:盲目的改資料庫 最後給自己帶來了 一地雞毛?

“非同步函數” 這個概念 會不會也一樣呢?


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言 隨著Spring Boot2.0正式發佈,Spring WebFlux正式來到了Spring Boot大家族裡面。由於Spring WebFlux可以通過更少的線程去實現更高的併發和使用更少的硬體資源去實現擴展,我對此很感興趣。同時Angular6也發佈了,也想試試自己Angular的功底,便 ...
  • 前言 在 "上一篇" 中我們學習了單例模式,介紹了單例模式創建的幾種方法以及最優的方法。本篇則介紹設計模式中的工廠模式,主要分為簡單工廠模式、工廠方法和抽象工廠模式。 簡單工廠模式 簡單工廠模式是屬於創建型模式,又叫做靜態工廠方法模式。簡單工廠模式是由一個工廠對象決定創建出哪一種產品類的實例。調用只 ...
  • 裝飾模式 裝飾模式之前的面向對象原則介紹: 單一職責原則:就一個類而言,應該僅有一個引起它變化的原因。也就是說功能要單一。 優點: 靈活性,可復用性。 如果一個類承擔的職責太多,就等於把這些職責耦合在一起,一個職責的變化可能會削弱或者阻礙其他職責能力,這種耦合會導致脆弱的設計,當變化發生時,設計會發 ...
  • 軟體設計中由一些所謂的理念都沒有一個明確的定義,比如之前流行的SOA和現在炒的火熱的微服務(Micro Service)和無伺服器(Serverless),我們都不能通過一個明確的“內涵”給它們一個準確地定義,只能從“外延”上描述這些架構設計應該具有怎樣的特性。正因為無法給出一個明確的界定,造成了人... ...
  • 文章翻譯集錦 序 在閱讀部分英文文檔的時候,有幾點妨礙因素: 1.伺服器在國外,水土不服; 2.軟體翻譯有時候失敗; 3.載入速度有點慢、有時候不是一般的慢; 4.網站經常打不開; ... ... 系列 IdentityServer4系列(目前已經整理完畢):http://nidie.com.cn/ ...
  • WCF作為.NET Framework3.0就被引入的用於構建面向服務的框架在眾多項目中發揮著重大作用。時至今日,雖然已有更新的技術可以替代它,但對於那些既存項目或產品,使用新框架重構的代價未必能找到人願意買單。 而在.NET Core平臺環境中,WCF也並沒有被完全列入遷移目標。WCF的服務端被擱 ...
  • 據瞭解,目前武漢軟體開發市場關於PC端桌面開發的技術主要有兩塊:winform和wpf。wpf是微軟既winform之後推出的一套新的桌面開發技術。採用數據驅動的方式可以輕鬆編寫出非常炫的界面。 ...
  • /// /// API接收Base64轉圖片 /// /// 圖片位元組 /// 儲存地址 /// public IHttpActionResult Index(String Img, String Path) { //轉圖片 byte[] bit = Convert.FromBase64String... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...