<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
筆者上次用C#寫.Net程式碼差不多還是10多年以前,由於當時Java已經頗具王者風範,Net幾乎被打得潰不成軍。因此當時筆者對於這個.Net的專案態度比較敷衍了事,沒有對其中一些優秀機制有很深的瞭解,在去年寫《C和Java沒那麼香了,高並行時代誰能稱王》時都沒給.Net以一席之地,不過最近恰好機緣巧合,我又接手了一個Windows方面的專案,這也讓我有機會重新審視一下自己關於.Net框架的相關知識。
專案原型要實現的功能並不複雜,主要就是記錄移動儲存裝置中檔案拷出的記錄,而且需要儘可能少的佔用系統資源,而在開發過程中我無意中加了一行看似沒有任何效果的程式碼,使用Invoke方法記錄檔案拷出情況,這樣的操作卻讓程式執行效率明顯會更高,這背後的原因特別值得總結。
由於我需要記錄的檔案拷出資訊並沒有回顯在UI的需要,因此也就沒考慮並行衝突的問題,在最初版本的實現中,我對於filesystemwatcher的回撥事件,都是直接處理的,如下:
private void DeleteFileHandler(object sender, FileSystemEventArgs e) { if(files.Contains(e.FullPath)) { files.Remove(e.FullPath); //一些其它操作 } }
這個程式的處理效率在普通的辦公PC上如果同時拷出20個檔案,那麼在拷貝過程中,U盤監測程式的CPU使用率大約是0.7%。
但是一個非常偶然的機會,我使用了Event/Delegate的Invoke機制,結果發現這樣一個看似的廢操作,卻讓程式的CPU佔用率下降到0.2%左右
private void UdiskWather_Deleted(object sender, FileSystemEventArgs e) { if(this.InvokeRequired) { this.Invoke(new DeleteDelegate(DeleteFileHandler), new object[] { sender,e }); } else { DeleteFileHandler(sender, e); } }
在我最初的認識中.net中的Delegate機制在呼叫過程中是要進行拆、裝箱操作的,因此這不拖慢操作就不錯了,但實際的驗證結果卻相反。
這裡先給出結論,Invoke能提升程式執行效率,其關鍵還是在於執行緒在多核之間切換的消耗要遠遠高於拆、裝箱的資源消耗,我們知道我們程式的核心就是操作files這個共用變數,每次在被檢測的U盤目錄中如果發生檔案變動,其回撥通知函數可能都執行在不同的執行緒,如下:
Invoke機制的背後其實就是保證所有對於files這個共用變數的操作,全部都是由一個執行緒執行完成的。
目前.Net的程式碼都開源的,下面我們大致講解一下Invoke的呼叫過程,不管是BeginInvoke還是Invoke背後其實都是呼叫的MarshaledInvoke方法來完成的,如下:
public IAsyncResult BeginInvoke(Delegate method, params Object[] args) { using (new MultithreadSafeCallScope()) { Control marshaler = FindMarshalingControl(); return(IAsyncResult)marshaler.MarshaledInvoke(this, method, args, false); } }
MarshaledInvoke的主要工作是建立ThreadMethodEntry物件,並把它放在一個連結串列裡進行管理,然後呼叫PostMessage將相關資訊發給要通訊的執行緒,如下:
private Object MarshaledInvoke(Control caller, Delegate method, Object[] args, bool synchronous) { if (!IsHandleCreated) { throw new InvalidOperationException(SR.GetString(SR.ErrorNoMarshalingThread)); } ActiveXImpl activeXImpl = (ActiveXImpl)Properties.GetObject(PropActiveXImpl); if (activeXImpl != null) { IntSecurity.UnmanagedCode.Demand(); } // We don't want to wait if we're on the same thread, or else we'll deadlock. // It is important that syncSameThread always be false for asynchronous calls. // bool syncSameThread = false; int pid; // ignored if (SafeNativeMethods.GetWindowThreadProcessId(new HandleRef(this, Handle), out pid) == SafeNativeMethods.GetCurrentThreadId()) { if (synchronous) syncSameThread = true; } // Store the compressed stack information from the thread that is calling the Invoke() // so we can assign the same security context to the thread that will actually execute // the delegate being passed. // ExecutionContext executionContext = null; if (!syncSameThread) { executionContext = ExecutionContext.Capture(); } ThreadMethodEntry tme = new ThreadMethodEntry(caller, this, method, args, synchronous, executionContext); lock (this) { if (threadCallbackList == null) { threadCallbackList = new Queue(); } } lock (threadCallbackList) { if (threadCallbackMessage == 0) { threadCallbackMessage = SafeNativeMethods.RegisterWindowMessage(Application.WindowMessagesVersion + "_ThreadCallbackMessage"); } threadCallbackList.Enqueue(tme); } if (syncSameThread) { InvokeMarshaledCallbacks(); } else { // UnsafeNativeMethods.PostMessage(new HandleRef(this, Handle), threadCallbackMessage, IntPtr.Zero, IntPtr.Zero); } if (synchronous) { if (!tme.IsCompleted) { WaitForWaitHandle(tme.AsyncWaitHandle); } if (tme.exception != null) { throw tme.exception; } return tme.retVal; } else { return(IAsyncResult)tme; } }
Invoke的機制就保證了一個共用變數只能由一個執行緒維護,這和GO語言使用通訊來替代共用記憶體的設計是暗合的,他們的理念都是 "讓同一塊記憶體在同一時間內只被一個執行緒操作" 。這和現代計算體系結構的多核CPU(SMP)有著密不可分的聯絡,
這裡我們先來科普一下CPU之間的通訊MESI協定的內容。我們知道現代的CPU都配備了快取記憶體,按照多核快取記憶體同步的MESI協定約定,每個快取行都有四個狀態,分別是E(exclusive)、M(modified)、S(shared)、I(invalid),其中:
M:代表該快取行中的內容被修改,並且該快取行只被快取在該CPU中。這個狀態代表快取行的資料和記憶體中的資料不同。
E:代表該快取行對應記憶體中的內容只被該CPU快取,其他CPU沒有快取該快取對應記憶體行中的內容。這個狀態的快取行中的資料與記憶體的資料一致。
I:代表該快取行中的內容無效。
S:該狀態意味著資料不止存在本地CPU快取中,還存在其它CPU的快取中。這個狀態的資料和記憶體中的資料也是一致的。不過只要有CPU修改該快取行都會使該行狀態變成 I 。
四種狀態的狀態轉移圖如下:
我們上文也提到了,不同的執行緒是有大概率是執行在不同CPU核上的,在不同CPU操作同一塊記憶體時,站在CPU0的角度上看,就是CPU1會不斷髮起remote write的操作,這會使該快取記憶體的狀態總是會在S和I之間進行狀態遷移,而一旦狀態變為I將耗費比較多的時間進行狀態同步。
因此我們可以基本得出 this.Invoke(new DeleteDelegate(DeleteFileHandler)
, new object[] { sender,e }); ;這行看似無關緊要的程式碼之後,無意中使files共用變數的維護操作,由多核多執行緒共同操作,變成了眾多子執行緒向主執行緒通訊,所有維護操作均由主執行緒進行,這也使最終的執行效率有所提高。
在當前使用通訊替代共用記憶體的大潮之下,鎖其實是最重要的設計。
我們看到在.Net的Invoke實現中,使用了兩把鎖lock (this) 與lock (threadCallbackList)。
lock (this) { if (threadCallbackList == null) { threadCallbackList = new Queue(); } } lock (threadCallbackList) { if (threadCallbackMessage == 0) { threadCallbackMessage = SafeNativeMethods.RegisterWindowMessage(Application.WindowMessagesVersion + "_ThreadCallbackMessage"); } threadCallbackList.Enqueue(tme); }
在.NET當中lock關鍵字的基本可以理解為提供了一個近似於CAS的鎖(Compare And Swap)。CAS的原理不斷地把"期望值"和"實際值"進行比較,當它們相等時,說明持有鎖的CPU已經釋放了該鎖,那麼試圖獲取這把鎖的CPU就會嘗試將"new"的值(0)寫入"p"(交換),以表明自己成為spinlock新的owner。虛擬碼演示如下:
void CAS(int p, int old,int new) { if *p != old do nothing else *p ← new }
基於CAS的鎖效率沒問題,尤其是在沒有多核競爭的情況CAS表現得尤其優秀,但CAS最大的問題就是不公平,因為如果有多個CPU同時在申請一把鎖,那麼剛剛釋放鎖的CPU極可能在下一輪的競爭中獲取優勢,再次獲得這把鎖,這樣的結果就是一個CPU忙死,而其它CPU卻很閒,我們很多時候詬病多核SOC“一核有難,八核圍觀”其實很多時候都是由這種不公平造成的。
為了解決CAS的不公平問題,業界大神們又引入了TAS(Test And Set Lock)機制,個人感覺還是把TAS中的T理解為Ticket更好記一些,TAS方案中維護了一個請求該鎖的頭尾索引值,由"head"和"tail"兩個索引組成。
struct lockStruct{ int32 head; int32 tail; } ;
"head"代表請求佇列的頭部,"tail"代表請求佇列的尾部,其初始值都為0。
最一開始時,第一個申請的CPU發現該佇列的tail值是0,那麼這個CPU會直接獲取這把鎖,並會把tail值更新為1,並在釋放該鎖時將head值更新為1。
在一般情況下當鎖被持有的CPU釋放時,該佇列的head值會被加1,當其他CPU在試圖獲取這個鎖時,鎖的tail值獲取到,然後把這個tail值加1,並儲存在自己專屬的暫存器當中,然後再把更新後的tail值更新到佇列的tail當中。接下來就是不斷地迴圈比較,判斷該鎖當前的"head"值,是否和自己儲存在暫存器中的"tail"值相等,相等時則代表成功獲得該鎖。
TAS這類似於使用者到政務大廳去辦事時,首先要在叫號機取號,當工作人員廣播叫到的號碼與你手中的號碼一致時,你就獲取了辦事櫃檯的所有權。
但是TAS卻存在一定的效率問題,根據我們上文介紹的MESI協定,這個lock的頭尾索引其實是在各個CPU之間共用的,因此tail和head頻繁更新,還是會引發調整快取不停的invalidate,這會極大的影響效率。
因此我們看到在.Net的實現中乾脆就直接引入了threadCallbackList的佇列,並不斷將tme(ThreadMethodEntry)加入隊尾,而接收訊息的程序,則不斷從隊首獲取訊息.
lock (threadCallbackList) { if (threadCallbackMessage == 0) { threadCallbackMessage = SafeNativeMethods.RegisterWindowMessage(Application.WindowMessagesVersion + "_ThreadCallbackMessage"); } threadCallbackList.Enqueue(tme); }
當隊首指向這個tme時,訊息才被傳送,其實是一種類似於MAS的實現,當然MAS實際是為每個CPU都建立了一個專屬的佇列,和Invoke的設計略有不同,不過基本的思想是一致的。
本篇文章就到這裡了,希望能夠給你帶來幫助,也希望您能夠多多關注it145.com的更多內容!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45