【譯】最大限度地降低多線程 C# 代碼的複雜性

来源:https://www.cnblogs.com/leolion/archive/2019/03/22/10581723.html
-Advertisement-
Play Games

分支或多線程編程是編程時最難最對的事情之一。這是由於它們的並行性質所致,即要求採用與使用單線程的線性編程完全不同的思維模式。對於這個問題,恰當類比就是拋接雜耍表演者,必須在空中拋接多個球,而不要讓它們相互干擾。這是一項重大挑戰。然而,通過正確的工具和思維模式,這項挑戰是能應對的。 本文將深入介紹我為 ...


分支或多線程編程是編程時最難最對的事情之一。這是由於它們的並行性質所致,即要求採用與使用單線程的線性編程完全不同的思維模式。對於這個問題,恰當類比就是拋接雜耍表演者,必須在空中拋接多個球,而不要讓它們相互干擾。這是一項重大挑戰。然而,通過正確的工具和思維模式,這項挑戰是能應對的。

本文將深入介紹我為了簡化多線程編程和避免爭用條件、死鎖等其他問題而編寫的一些工具。可以說,工具鏈以語法糖和神奇委托為依據。不過,引用偉大的爵士音樂家 Miles Davis 的話:“在音樂中,沒有聲音比有聲音更重要。” 聲音間斷就產生了奇跡。

從另一個角度來說,不一定是關乎可以編碼什麼,而是關乎可以選擇不編碼什麼,因為你希望通過間斷代碼行產生一點奇跡。引用 Bill Gates 的一句話:“根據代碼行數來衡量工作質量就像通過重量來衡量飛機質量一樣。” 因此,我希望能幫助開發人員減少編碼量,而不是教導開發人員如何編寫更多代碼。

同步挑戰

在多線程編程方面遇到的第一個問題是,同步對共用資源的訪問許可權。當兩個或多個線程共用對某個對象的訪問許可權且可能同時嘗試修改此對象時,就會出現這個問題。當 C# 首次發佈時,lock 語句實現了一種基本方法,可確保只有一個線程能訪問指定資源(如數據文件),且效果很好。C# 中的 lock 關鍵字很容易理解,它獨自顛覆了我們對這個問題的思考方式。

不過,簡單的 lock 存在一個主要缺陷:它不區分只讀訪問許可權和寫入訪問許可權。例如,可能要從共用對象中讀取 10 個不同的線程,並且通過 System.Threading 命名空間中的 ReaderWriterLockSlim 類授權這些線程同時訪問實例,而不導致問題發生。與 lock 語句不同,此類可便於指定代碼是將內容寫入對象,還是只從對象讀取內容。這樣一來,多個讀取器可以同時進入,但在其他所有讀寫線程均已完成自己的工作前,拒絕任何寫入代碼訪問。

現在的問題是:如果使用 ReaderWriterLock 類,語法就會變得很麻煩,大量的重覆代碼既降低了可讀性,又隨時間變化增加了維護複雜性,並且代碼中通常會分散有多個 try 和 finally 塊。即使是簡單的拼寫錯誤,也可能會帶來日後有時極難發現的災難性影響。 

通過將 ReaderWriterLockSlim 封裝到簡單的類中,這個問題瞬間解決,不僅重覆代碼不再會出現,而且還降低了小拼寫錯誤毀一天勞動成果的風險。圖 1 中的類完全基於 lambda 技巧。可以說,這就是對一些委托應用的語法糖(假設存在幾個介面)。最重要的是,它在很大程度上有助於實現避免重覆代碼原則 (DRY)。

圖 1:封裝 ReaderWriterLockSlim
 1 public class Synchronizer<TImpl, TIRead, TIWrite> where TImpl : TIWrite, TIRead {
 2     ReaderWriterLockSlim _lock = new ReaderWriterLockSlim ();
 3     TImpl _shared;
 4 
 5     public Synchronizer (TImpl shared) {
 6         _shared = shared;
 7     }
 8 
 9     public void Read (Action<TIRead> functor) {
10         _lock.EnterReadLock ();
11         try {
12             functor (_shared);
13         } finally {
14             _lock.ExitReadLock ();
15         }
16     }
17 
18     public void Write (Action<TIWrite> functor) {
19         _lock.EnterWriteLock ();
20         try {
21             functor (_shared);
22         } finally {
23             _lock.ExitWriteLock ();
24         }
25     }
26 }

圖 1 中只有 27 行代碼,但卻精妙簡潔地確保對象跨多個線程進行同步。此類假定類型中有讀取介面和寫入介面。如果由於某種原因而無法更改需要將訪問許可權同步到的基礎類實現,也可以重覆模板類本身三次,通過這種方式使用它。基本用法如圖 2 所示。

圖 2:使用 Synchronizer 類
 1 interface IReadFromShared {
 2     string GetValue ();
 3 }
 4 
 5 interface IWriteToShared {
 6     void SetValue (string value);
 7 }
 8 
 9 class MySharedClass : IReadFromShared, IWriteToShared {
10     string _foo;
11 
12     public string GetValue () {
13         return _foo;
14     }
15 
16     public void SetValue (string value) {
17         _foo = value;
18     }
19 }
20 
21 void Foo (Synchronizer<MySharedClass, IReadFromShared, IWriteToShared> sync) {
22     sync.Write (x => {
23         x.SetValue ("new value");
24     });
25     sync.Read (x => {
26         Console.WriteLine (x.GetValue ());
27     })
28 }

在圖 2 的代碼中,無論有多少線程在執行 Foo 方法,只要執行另一個 Read 或 Write 方法,就不會調用 Write 方法。不過,可以同時調用多個 Read 方法,而不必在代碼中分散多個 try/catch/finally 語句,也不必不斷重覆相同的代碼。我在此鄭重聲明,通過簡單字元串來使用它是沒有意義的,因為 System.String 不可變。我使用簡單的字元串對象來簡化示例。

基本思路是,必須將所有可以修改實例狀態的方法都添加到 IWriteToShared 介面中。同時,應將所有隻從實例讀取內容的方法都添加到 IReadFromShared 介面中。通過將諸如此類的問題分散到兩個不同的介面,並對基礎類型實現這兩個介面,可使用 Synchronizer 類來同步對實例的訪問許可權。這樣一來,將訪問許可權同步到代碼的做法變得更簡單,並且基本上可以通過更具聲明性的方式這樣做。

在多線程編程方面,語法糖可能會決定成敗。調試多線程代碼通常極為困難,並且創建同步對象的單元測試可能會是徒勞無功之舉。

如果需要,可以創建只包含一個泛型參數的重載類型,不僅繼承自原始 Synchronizer 類,還將它的一個泛型參數作為類型參數三次傳遞到它的基類。這樣一來,就不需要讀取介面或寫入介面了,因為可以直接使用類型的具體實現。不過,這種方法要求手動處理需要使用 Write 或 Read 方法的部分。此外,雖然它的安全性稍差一點,但確實可便於將無法更改的類包裝到 Synchronizer 實例中。

用於分支的 lambda 集合

邁出第一步來使用神奇的 lambda(或在 C# 中稱為“委托”)後,不難想象,可以利用它們完成更多操作。例如,反覆出現的常見多線程主題是,讓多個線程與其他伺服器聯繫,以提取數據並將數據返回給調用方。

最簡單的例子就是,應用程式從 20 個網頁讀取數據,併在完成後將 HTML 返回給一個根據所有網頁的內容創建某種聚合結果的線程。除非為每個檢索方法都創建一個線程,否則此代碼的運行速度比預期慢得多:99% 的所有執行時間可能會花在等待 HTTP 請求返回上。

在一個線程上運行此代碼的效率很低,並且線程創建語法非常容易出錯。隨著你支持多個線程及其助理對象,挑戰變得更嚴峻,開發人員不得不在編寫代碼時使用重覆代碼。意識到可以創建委托集合和用於包裝這些委托的類後,便能使用一個方法調用來創建所有線程。這樣一來,創建線程就輕鬆多了。

圖 3 中的一段代碼創建兩個並行運行的此類 lambda。請註意,此代碼實際上來自我的第一版 Lizzie 腳本語言的單元測試 (bit.ly/2FfH5y8)。

圖 3:創建 lambda
 1 public void ExecuteParallel_1 () {
 2     var sync = new Synchronizer<string, string, string> ("initial_");
 3 
 4     var actions = new Actions ();
 5     actions.Add (() => sync.Assign ((res) => res + "foo"));
 6     actions.Add (() => sync.Assign ((res) => res + "bar"));
 7 
 8     actions.ExecuteParallel ();
 9 
10     string result = null;
11     sync.Read (delegate (string val) { result = val; });
12     Assert.AreEqual (true, "initial_foobar" == result || result == "initial_barfoo");
13 }

仔細看看這段代碼便會發現,計算結果並未假定我的兩個 lambda 的執行存先後順序。執行順序並未明確指定,並且這些 lambda 是在不同的線程上執行。這是因為,使用圖 3 中的 Actions 類,可以向它添加委托,這樣稍後就能決定是要並行執行委托,還是按順序執行委托。

為此,必須使用首選機制創建並執行許多 lambda。在圖 3 中可以看到前面提到的 Synchronizer 類,用於同步對共用字元串資源的訪問許可權。不過,它對 Synchronizer 使用了新方法 Assign,我並未在圖 1中的列表內為 Synchronizer 類添加此方法。Assign 方法使用前面 Write 和 Read 方法中使用的相同“lambda 技巧”。

若要研究 Actions 類的實現,請務必下載 Lizzie 版本 0.1,因為我在後面推出的版本中完全重寫了代碼,使之成為獨立編程語言。

C# 中的函數式編程

大多數開發人員往往認為,C# 幾乎與面向對象的編程 (OOP) 同義或至少密切相關,事實顯然如此。不過,通過重新思考如何使用 C#,並深入瞭解它的各方面功能,解決一些問題就變得更加簡單了。目前形式的 OOP 不太易於重用,原因很多是因為它是強類型。

例如,如果重用一個類,就不得不重用初始類引用的每個類(在兩種情況下,類都是通過組合和繼承進行使用)。此外,類重用還會強制重用這些第三方類引用的所有類等。如果這些類是在不同的程式集中實現,必須添加各種各樣的程式集,才能獲得對一個類型上單個方法的訪問許可權。

我曾經看過一個可以說明這個問題的類比:“雖然想要的是香蕉,但最終得到的是手拿香蕉的大猩猩,以及大猩猩所居住的熱帶雨林。” 將這種情況與使用更動態的語言(如 JavaScript)進行重用做比較,後者並不關心類型,只要它實現函數本身使用的函數即可。通過略微寬鬆類型方法生成的代碼更靈活、更易於重用。委托可以實現這一點。

可使用 C# 來改善跨多個項目重用代碼的過程。只需要理解函數或委托也可以是對象,並且可以通過弱類型方式控制這些對象的集合。

早在 2018 年 11 月發行的《MSDN 雜誌》中,我發表過一篇標題為“使用符號委托創建你自己的腳本語言”的文章 (msdn.com/magazine/mt830373)。本文中提到的有關委托的思路是在這篇文章的基礎之上形成。本文還介紹了 Lizzie,這是我的自製腳本語言,它的存在歸功於這種以委托為中心的思維模式。如果我使用 OOP 規則創建了 Lizzie,我會認為,它在大小上可能至少大一個數量級。

當然,如今 OOP 和強類型處於主導地位,想要找到一個主要必需技能不要求它的職位描述,幾乎是不可能的。我在此鄭重聲明,我創建 OOP 代碼的時間已超過 25 年,所以,我與任何人一樣都會因為對強類型有偏見而感到內疚。然而,如今我在編碼方法上更加務實,對類層次結構的最終外觀失去興趣。

並不是我不欣賞外觀精美的類層次結構,而是收益遞減。添加到層次結構中的類越多,它就變得越臃腫,直到因不堪重壓而崩潰。有時,卓越的設計只用很少的方法、更少的類和大多數鬆散耦合的函數,這樣就可以輕鬆擴展代碼,也就不需要“引入大猩猩和熱帶雨林”了。

回到本文反覆出現的主題(從 Miles Davis 的音樂方法中獲得靈感):少即是多(“沒有聲音比有聲音更重要”)。 代碼也不例外。間斷代碼行往往會產生奇跡,最佳解決方案的衡量依據更多是不編碼什麼,而不是編碼什麼。連傻瓜也可以將喇叭吹響,但只有為數不多的人才能用喇叭吹奏出音樂。像 Miles 這樣能創造出奇跡的人就更少了。

 


原文作者:Thomas Hansen

原文地址:Minimize Complexity in Multithreaded C# Code


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

-Advertisement-
Play Games
更多相關文章
  • 一、函數引入 與數學中的函數不同,在Python中,函數不是看上去冰冷無聊的規則和公式,而是有實打實的、有自己作用的代碼。比如說當我們需要實現“列印”這個功能,我們會用到print();當我們需要實現“獲取數據長度”這個功能,我們會要到len()。這些都是設定好了,可以直接拿過來就用的功能,這就叫做 ...
  • 自上向下,優先順序越來越高 ...
  • redis是什麼? Redis 是一個高性能的key-value資料庫! 想進一步瞭解請移步搜索引擎自行查找。 編寫這個小程式的目的就是對redis進行一個簡單的小操作,對redis有一個初步的瞭解,並未有什麼高大尚的騷操作,適合小白閱讀。 程式共分為三個部分。 1.創建紅包 2.將紅包存儲到資料庫 ...
  • 這是一篇用Python畫畫的文章,更多有趣、好玩的Python應用、實戰盡在知識星球「人人都是Pythonista」。 關註公眾號「**Python專欄**」,回覆:**美隊盾牌**,獲取全套代碼! ...
  • 一、可變不可變類型 二、數字類型 1.整型 2.浮點型float 總結;數字類型是不可變類型,同時只能存一個值 三、字元串類型 msg='hello' print(msg[0],type(msg[0])) #取其第一個字元,列印其的類型 print(msg[-1]) #從最後第一項索引字元 prin ...
  • 一、逆向工程的作用 簡單來說,就是替我們生成Java代碼。 之前使用Mybatis的Mapper代理方法開發,還需要自己創建實體類,而且屬性還得和資料庫中的欄位對應。這著實是機械化的而且比較麻煩的事,而機械化的事情正是代碼所擅長的,於是Mybatis官方就提供了MyBatis Generator , ...
  • 0X00 前言 在.NET處理 Ajax應用的時候,通常序列化功能由JavaScriptSerializer類提供,它是.NET2.0之後內部實現的序列化功能的類,位於命名空間System.Web.Script.Serialization、通過System.Web.Extensions引用,讓開發者 ...
  • 0X00 前言 Java中的Fastjson曾經爆出了多個反序列化漏洞和Bypass版本,而在.Net領域也有一個Fastjson的庫,作者官宣這是一個讀寫Json效率最高的的.Net 組件,使用內置方法JSON.ToJSON可以快速序列化.Net對象。讓你輕鬆實現.Net中所有類型(對象,基本數據 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...