如何最大限度地降低多線程C#代碼的復(fù)雜性
分支或多線程編程是編程時(shí)最難最對(duì)的事情之一。這是由于它們的并行性質(zhì)所致,即要求采用與使用單線程的線性編程完全不同的思維模式。對(duì)于這個(gè)問題,恰當(dāng)類比就是拋接雜耍表演者,必須在空中拋接多個(gè)球,而不要讓它們相互干擾。這是一項(xiàng)重大挑戰(zhàn)。然而,通過正確的工具和思維模式,這項(xiàng)挑戰(zhàn)是能應(yīng)對(duì)的。
本文將深入介紹我為了簡(jiǎn)化多線程編程和避免爭(zhēng)用條件、死鎖等其他問題而編寫的一些工具??梢哉f,工具鏈以語(yǔ)法糖和神奇委托為依據(jù)。不過,引用偉大的爵士音樂家 Miles Davis 的話:“在音樂中,沒有聲音比有聲音更重要?!?聲音間斷就產(chǎn)生了奇跡。
從另一個(gè)角度來說,不一定是關(guān)乎可以編碼什么,而是關(guān)乎可以選擇不編碼什么,因?yàn)槟阆Mㄟ^間斷代碼行產(chǎn)生一點(diǎn)奇跡。引用 Bill Gates 的一句話:“根據(jù)代碼行數(shù)來衡量工作質(zhì)量就像通過重量來衡量飛機(jī)質(zhì)量一樣。” 因此,我希望能幫助開發(fā)人員減少編碼量,而不是教導(dǎo)開發(fā)人員如何編寫更多代碼。
同步挑戰(zhàn)
在多線程編程方面遇到的第一個(gè)問題是,同步對(duì)共享資源的訪問權(quán)限。當(dāng)兩個(gè)或多個(gè)線程共享對(duì)某個(gè)對(duì)象的訪問權(quán)限且可能同時(shí)嘗試修改此對(duì)象時(shí),就會(huì)出現(xiàn)這個(gè)問題。當(dāng) C# 首次發(fā)布時(shí),lock 語(yǔ)句實(shí)現(xiàn)了一種基本方法,可確保只有一個(gè)線程能訪問指定資源(如數(shù)據(jù)文件),且效果很好。C# 中的 lock 關(guān)鍵字很容易理解,它獨(dú)自顛覆了我們對(duì)這個(gè)問題的思考方式。
不過,簡(jiǎn)單的 lock 存在一個(gè)主要缺陷:它不區(qū)分只讀訪問權(quán)限和寫入訪問權(quán)限。例如,可能要從共享對(duì)象中讀取 10 個(gè)不同的線程,并且通過 System.Threading 命名空間中的 ReaderWriterLockSlim 類授權(quán)這些線程同時(shí)訪問實(shí)例,而不導(dǎo)致問題發(fā)生。與 lock 語(yǔ)句不同,此類可便于指定代碼是將內(nèi)容寫入對(duì)象,還是只從對(duì)象讀取內(nèi)容。這樣一來,多個(gè)讀取器可以同時(shí)進(jìn)入,但在其他所有讀寫線程均已完成自己的工作前,拒絕任何寫入代碼訪問。
現(xiàn)在的問題是:如果使用 ReaderWriterLock 類,語(yǔ)法就會(huì)變得很麻煩,大量的重復(fù)代碼既降低了可讀性,又隨時(shí)間變化增加了維護(hù)復(fù)雜性,并且代碼中通常會(huì)分散有多個(gè) try 和 finally 塊。即使是簡(jiǎn)單的拼寫錯(cuò)誤,也可能會(huì)帶來日后有時(shí)極難發(fā)現(xiàn)的災(zāi)難性影響。
通過將 ReaderWriterLockSlim 封裝到簡(jiǎn)單的類中,這個(gè)問題瞬間解決,不僅重復(fù)代碼不再會(huì)出現(xiàn),而且還降低了小拼寫錯(cuò)誤毀一天勞動(dòng)成果的風(fēng)險(xiǎn)。圖 1 中的類完全基于 lambda 技巧。可以說,這就是對(duì)一些委托應(yīng)用的語(yǔ)法糖(假設(shè)存在幾個(gè)接口)。最重要的是,它在很大程度上有助于實(shí)現(xiàn)避免重復(fù)代碼原則 (DRY)。
1:封裝 ReaderWriterLockSlim
public class Synchronizer<TImpl, TIRead, TIWrite> where TImpl : TIWrite, TIRead { ReaderWriterLockSlim _lock = new ReaderWriterLockSlim (); TImpl _shared; public Synchronizer (TImpl shared) { _shared = shared; } public void Read (Action<TIRead> functor) { _lock.EnterReadLock (); try { functor (_shared); } finally { _lock.ExitReadLock (); } } public void Write (Action<TIWrite> functor) { _lock.EnterWriteLock (); try { functor (_shared); } finally { _lock.ExitWriteLock (); } } }
1 中只有 27 行代碼,但卻精妙簡(jiǎn)潔地確保對(duì)象跨多個(gè)線程進(jìn)行同步。此類假定類型中有讀取接口和寫入接口。如果由于某種原因而無法更改需要將訪問權(quán)限同步到的基礎(chǔ)類實(shí)現(xiàn),也可以重復(fù)模板類本身三次,通過這種方式使用它。基本用法如 2 所示。
2:使用 Synchronizer 類
interface IReadFromShared { string GetValue (); } interface IWriteToShared { void SetValue (string value); } class MySharedClass : IReadFromShared, IWriteToShared { string _foo; public string GetValue () { return _foo; } public void SetValue (string value) { _foo = value; } } void Foo (Synchronizer<MySharedClass, IReadFromShared, IWriteToShared> sync) { sync.Write (x => { x.SetValue ("new value"); }); sync.Read (x => { Console.WriteLine (x.GetValue ()); }) }
在 2 的代碼中,無論有多少線程在執(zhí)行 Foo 方法,只要執(zhí)行另一個(gè) Read 或 Write 方法,就不會(huì)調(diào)用 Write 方法。不過,可以同時(shí)調(diào)用多個(gè) Read 方法,而不必在代碼中分散多個(gè) try/catch/finally 語(yǔ)句,也不必不斷重復(fù)相同的代碼。我在此鄭重聲明,通過簡(jiǎn)單字符串來使用它是沒有意義的,因?yàn)?System.String 不可變。我使用簡(jiǎn)單的字符串對(duì)象來簡(jiǎn)化示例。
基本思路是,必須將所有可以修改實(shí)例狀態(tài)的方法都添加到 IWriteToShared 接口中。同時(shí),應(yīng)將所有只從實(shí)例讀取內(nèi)容的方法都添加到 IReadFromShared 接口中。通過將諸如此類的問題分散到兩個(gè)不同的接口,并對(duì)基礎(chǔ)類型實(shí)現(xiàn)這兩個(gè)接口,可使用 Synchronizer 類來同步對(duì)實(shí)例的訪問權(quán)限。這樣一來,將訪問權(quán)限同步到代碼的做法變得更簡(jiǎn)單,并且基本上可以通過更具聲明性的方式這樣做。
在多線程編程方面,語(yǔ)法糖可能會(huì)決定成敗。調(diào)試多線程代碼通常極為困難,并且創(chuàng)建同步對(duì)象的單元測(cè)試可能會(huì)是徒勞無功之舉。
如果需要,可以創(chuàng)建只包含一個(gè)泛型參數(shù)的重載類型,不僅繼承自原始 Synchronizer 類,還將它的一個(gè)泛型參數(shù)作為類型參數(shù)三次傳遞到它的基類。這樣一來,就不需要讀取接口或?qū)懭虢涌诹?,因?yàn)榭梢灾苯邮褂妙愋偷木唧w實(shí)現(xiàn)。不過,這種方法要求手動(dòng)處理需要使用 Write 或 Read 方法的部分。此外,雖然它的安全性稍差一點(diǎn),但確實(shí)可便于將無法更改的類包裝到 Synchronizer 實(shí)例中。
用于分支的 lambda 集合
邁出第一步來使用神奇的 lambda(或在 C# 中稱為“委托”)后,不難想象,可以利用它們完成更多操作。例如,反復(fù)出現(xiàn)的常見多線程主題是,讓多個(gè)線程與其他服務(wù)器聯(lián)系,以提取數(shù)據(jù)并將數(shù)據(jù)返回給調(diào)用方。
最簡(jiǎn)單的例子就是,應(yīng)用程序從 20 個(gè)網(wǎng)頁(yè)讀取數(shù)據(jù),并在完成后將 HTML 返回給一個(gè)根據(jù)所有網(wǎng)頁(yè)的內(nèi)容創(chuàng)建某種聚合結(jié)果的線程。除非為每個(gè)檢索方法都創(chuàng)建一個(gè)線程,否則此代碼的運(yùn)行速度比預(yù)期慢得多:99% 的所有執(zhí)行時(shí)間可能會(huì)花在等待 HTTP 請(qǐng)求返回上。
在一個(gè)線程上運(yùn)行此代碼的效率很低,并且線程創(chuàng)建語(yǔ)法非常容易出錯(cuò)。隨著你支持多個(gè)線程及其助理對(duì)象,挑戰(zhàn)變得更嚴(yán)峻,開發(fā)人員不得不在編寫代碼時(shí)使用重復(fù)代碼。意識(shí)到可以創(chuàng)建委托集合和用于包裝這些委托的類后,便能使用一個(gè)方法調(diào)用來創(chuàng)建所有線程。這樣一來,創(chuàng)建線程就輕松多了。
3 中的一段代碼創(chuàng)建兩個(gè)并行運(yùn)行的此類 lambda。請(qǐng)注意,此代碼實(shí)際上來自我的第一版 Lizzie 腳本語(yǔ)言的單元測(cè)試 (bit.ly/2FfH5y8)。
3:創(chuàng)建 lambda
public void ExecuteParallel_1 () { var sync = new Synchronizer<string, string, string> ("initial_"); var actions = new Actions (); actions.Add (() => sync.Assign ((res) => res + "foo")); actions.Add (() => sync.Assign ((res) => res + "bar")); actions.ExecuteParallel (); string result = null; sync.Read (delegate (string val) { result = val; }); Assert.AreEqual (true, "initial_foobar" == result || result == "initial_barfoo"); }
仔細(xì)看看這段代碼便會(huì)發(fā)現(xiàn),計(jì)算結(jié)果并未假定我的兩個(gè) lambda 的執(zhí)行存先后順序。執(zhí)行順序并未明確指定,并且這些 lambda 是在不同的線程上執(zhí)行。這是因?yàn)?,使用圖 3 中的 Actions 類,可以向它添加委托,這樣稍后就能決定是要并行執(zhí)行委托,還是按順序執(zhí)行委托。
為此,必須使用首選機(jī)制創(chuàng)建并執(zhí)行許多 lambda。在圖 3 中可以看到前面提到的 Synchronizer 類,用于同步對(duì)共享字符串資源的訪問權(quán)限。不過,它對(duì) Synchronizer 使用了新方法 Assign,我并未在圖 1中的列表內(nèi)為 Synchronizer 類添加此方法。Assign 方法使用前面 Write 和 Read 方法中使用的相同“l(fā)ambda 技巧”。
若要研究 Actions 類的實(shí)現(xiàn),請(qǐng)務(wù)必下載 Lizzie 版本 0.1,因?yàn)槲以诤竺嫱瞥龅陌姹局型耆貙懥舜a,使之成為獨(dú)立編程語(yǔ)言。
C# 中的函數(shù)式編程
大多數(shù)開發(fā)人員往往認(rèn)為,C# 幾乎與面向?qū)ο蟮木幊?(OOP) 同義或至少密切相關(guān),事實(shí)顯然如此。不過,通過重新思考如何使用 C#,并深入了解它的各方面功能,解決一些問題就變得更加簡(jiǎn)單了。目前形式的 OOP 不太易于重用,原因很多是因?yàn)樗菑?qiáng)類型。
例如,如果重用一個(gè)類,就不得不重用初始類引用的每個(gè)類(在兩種情況下,類都是通過組合和繼承進(jìn)行使用)。此外,類重用還會(huì)強(qiáng)制重用這些第三方類引用的所有類等。如果這些類是在不同的程序集中實(shí)現(xiàn),必須添加各種各樣的程序集,才能獲得對(duì)一個(gè)類型上單個(gè)方法的訪問權(quán)限。
我曾經(jīng)看過一個(gè)可以說明這個(gè)問題的類比:“雖然想要的是香蕉,但最終得到的是手拿香蕉的大猩猩,以及大猩猩所居住的熱帶雨林。” 將這種情況與使用更動(dòng)態(tài)的語(yǔ)言(如 JavaScript)進(jìn)行重用做比較,后者并不關(guān)心類型,只要它實(shí)現(xiàn)函數(shù)本身使用的函數(shù)即可。通過略微寬松類型方法生成的代碼更靈活、更易于重用。委托可以實(shí)現(xiàn)這一點(diǎn)。
可使用 C# 來改善跨多個(gè)項(xiàng)目重用代碼的過程。只需要理解函數(shù)或委托也可以是對(duì)象,并且可以通過弱類型方式控制這些對(duì)象的集合。
早在 2018 年 11 月發(fā)行的《MSDN 雜志》中,我發(fā)表過一篇標(biāo)題為“使用符號(hào)委托創(chuàng)建你自己的腳本語(yǔ)言”的文章 (msdn.com/magazine/mt830373)。本文中提到的有關(guān)委托的思路是在這篇文章的基礎(chǔ)之上形成。本文還介紹了 Lizzie,這是我的自制腳本語(yǔ)言,它的存在歸功于這種以委托為中心的思維模式。如果我使用 OOP 規(guī)則創(chuàng)建了 Lizzie,我會(huì)認(rèn)為,它在大小上可能至少大一個(gè)數(shù)量級(jí)。
當(dāng)然,如今 OOP 和強(qiáng)類型處于主導(dǎo)地位,想要找到一個(gè)主要必需技能不要求它的職位描述,幾乎是不可能的。我在此鄭重聲明,我創(chuàng)建 OOP 代碼的時(shí)間已超過 25 年,所以,我與任何人一樣都會(huì)因?yàn)閷?duì)強(qiáng)類型有偏見而感到內(nèi)疚。然而,如今我在編碼方法上更加務(wù)實(shí),對(duì)類層次結(jié)構(gòu)的最終外觀失去興趣。
并不是我不欣賞外觀精美的類層次結(jié)構(gòu),而是收益遞減。添加到層次結(jié)構(gòu)中的類越多,它就變得越臃腫,直到因不堪重壓而崩潰。有時(shí),卓越的設(shè)計(jì)只用很少的方法、更少的類和大多數(shù)松散耦合的函數(shù),這樣就可以輕松擴(kuò)展代碼,也就不需要“引入大猩猩和熱帶雨林”了。
回到本文反復(fù)出現(xiàn)的主題(從 Miles Davis 的音樂方法中獲得靈感):少即是多(“沒有聲音比有聲音更重要”)。 代碼也不例外。間斷代碼行往往會(huì)產(chǎn)生奇跡,最佳解決方案的衡量依據(jù)更多是不編碼什么,而不是編碼什么。連傻瓜也可以將喇叭吹響,但只有為數(shù)不多的人才能用喇叭吹奏出音樂。像 Miles 這樣能創(chuàng)造出奇跡的人就更少了。
原文作者:Thomas Hansen
原文地址:Minimize Complexity in Multithreaded C# Code
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持我們。
您可能感興趣的文章
- 01-10Extjs4如何處理后臺(tái)json數(shù)據(jù)中日期和時(shí)間
- 01-10asp.net中XML如何做增刪改查操作
- 01-10C#.NET中如何批量插入大量數(shù)據(jù)到數(shù)據(jù)庫(kù)中
- 01-10解析C#中的常量及如何在C#編程中定義常量
- 01-10C#開發(fā)Android百度地圖手機(jī)應(yīng)用程序(多地圖展示)
- 01-10基于C#實(shí)現(xiàn)手機(jī)號(hào)碼歸屬地接口調(diào)用
- 01-10如何使用C#從word文檔中提取圖片
- 01-10c#檢測(cè)文本文件編碼的方法
- 01-10在C#中如何使用正式表達(dá)式獲取匹配所需數(shù)據(jù)
- 01-10C#中如何獲取文件圖標(biāo)


閱讀排行
- 1C語(yǔ)言 while語(yǔ)句的用法詳解
- 2java 實(shí)現(xiàn)簡(jiǎn)單圣誕樹的示例代碼(圣誕
- 3利用C語(yǔ)言實(shí)現(xiàn)“百馬百擔(dān)”問題方法
- 4C語(yǔ)言中計(jì)算正弦的相關(guān)函數(shù)總結(jié)
- 5c語(yǔ)言計(jì)算三角形面積代碼
- 6什么是 WSH(腳本宿主)的詳細(xì)解釋
- 7C++ 中隨機(jī)函數(shù)random函數(shù)的使用方法
- 8正則表達(dá)式匹配各種特殊字符
- 9C語(yǔ)言十進(jìn)制轉(zhuǎn)二進(jìn)制代碼實(shí)例
- 10C語(yǔ)言查找數(shù)組里數(shù)字重復(fù)次數(shù)的方法
本欄相關(guān)
- 01-10C#通過反射獲取當(dāng)前工程中所有窗體并
- 01-10關(guān)于ASP網(wǎng)頁(yè)無法打開的解決方案
- 01-10WinForm限制窗體不能移到屏幕外的方法
- 01-10WinForm繪制圓角的方法
- 01-10C#實(shí)現(xiàn)txt定位指定行完整實(shí)例
- 01-10WinForm實(shí)現(xiàn)仿視頻 器左下角滾動(dòng)新
- 01-10C#停止線程的方法
- 01-10C#實(shí)現(xiàn)清空回收站的方法
- 01-10C#通過重寫Panel改變邊框顏色與寬度的
- 01-10C#實(shí)現(xiàn)讀取注冊(cè)表監(jiān)控當(dāng)前操作系統(tǒng)已
隨機(jī)閱讀
- 01-11Mac OSX 打開原生自帶讀寫NTFS功能(圖文
- 01-11ajax實(shí)現(xiàn)頁(yè)面的局部加載
- 08-05DEDE織夢(mèng)data目錄下的sessions文件夾有什
- 01-10使用C語(yǔ)言求解撲克牌的順子及n個(gè)骰子
- 08-05dedecms(織夢(mèng))副欄目數(shù)量限制代碼修改
- 08-05織夢(mèng)dedecms什么時(shí)候用欄目交叉功能?
- 04-02jquery與jsp,用jquery
- 01-10delphi制作wav文件的方法
- 01-10SublimeText編譯C開發(fā)環(huán)境設(shè)置
- 01-10C#中split用法實(shí)例總結(jié)