2019 年 2 月

第 34 卷,第 2 期

本文章是由機器翻譯。

[C#]

盡可能地減少多執行緒 C# 程式碼的複雜度

藉由Thomas Hansen |2019 年 2 月

分支或多執行緒的程式設計,是要正確進行程式設計時最困難的事情。這是因為其平行的本質,這需要完全不同的思維,比使用單一執行緒線性的程式設計。類比問題是雜耍,空氣中必須保留的多個球而不需要它們彼此會造成負面影響。它是主要的挑戰。不過,使用適當的工具和適當的心態,很容易管理。

在本文中,我探討一些我精心的工具簡化多執行緒的程式設計,以及避免發生問題,例如競爭情況、 死結和其他問題。工具鏈為基礎,有些人認為語法捷徑 」 和 「 神奇的委派。不過,套句絕佳爵士音樂家英哩 Davis 「 在音樂、 無回應 」 比音效更重要。 奇妙的開始之間的雜訊。

換句話說,它也不一定相關功能,您可以編碼,而什麼可以但選擇不要將您建立的一堆線之間的 magic。Bill Gates 引號被認為:"來測量的數行程式碼,根據工作的品質就像是其加權測量飛機的品質。 」 因此,而不是教導您如何撰寫更多程式碼,我希望幫助您更少的程式碼。

同步處理的挑戰

您會遇到與多執行緒程式設計的第一個問題同步處理共用資源的存取權。當兩個或多個執行緒共用的存取權的物件,並可能二者或許想要同時修改物件時,會發生問題。當C#第一次發行時,鎖定陳述式實作基本的方式,以確保只有一個執行緒無法存取指定的資源,例如資料檔案,且運作良好。Lock 關鍵字在C#因此很容易了解,它單獨改革了我們認為這個問題的方式。

不過,簡單的鎖定會受到主要的缺點:它不會區分唯讀存取權的寫入權限。比方說,您可能有想要從共用的物件,讀取 10 個不同的執行緒,而這些執行緒可以有同時存取您的執行個體而不會造成問題的 ReaderWriterLockSlim 類別透過 System.Threading 命名空間中。不同於鎖定陳述式中,此類別可讓您指定您的程式碼時是否寫入的物件,或只從物件讀取。這可讓多個讀取器進入,在此同時,但拒絕任何寫入程式碼存取,直到所有其他讀取和寫入執行緒完成執行他們的東西。

現在問題來了:使用 ReaderWriterLock 類別時的語法會變成冗長,有許多重複的程式碼,可減少可讀性並經過一段時間,使維護變得複雜,而您的程式碼通常會變成散佈與多個試和 finally 區塊。簡單打錯字可能也會產生災難性的效果,有時候也非常難以找出更新版本。 

藉由將 ReaderWriterLockSlim 封裝成一個簡單的類別,突然它解決的問題不需要重複的程式碼,同時降低維護次要的錯字會小孩子一天的風險。類別,如中所示**[圖 1**,完全以 lambda 以前為基礎。它可說是就語法的捷徑,解決一些假設幾個介面存在的委派。最重要的是,它可協助讓您的程式碼很多 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();
    }
  }
}

有只 27 行中的程式碼**[圖 1優雅簡潔的方式以確保物件同步處理跨多個執行緒,並提供。類別會假設讀取的介面和寫入介面對您的型別。您也可以使用它重複此範本類別本身三次,如果基於某些原因,您無法變更您要同步處理存取基礎類別的實作。基本使用方式可能如下所示[圖 2**。

[圖 2 使用同步類別

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**,而不論多少執行緒正在執行您 Foo 的方法,將會叫用方法,只要另一個讀取或寫入方法正在執行任何寫入。不過,多個讀取方法可以叫用同時,而無需使用多個 try/catch/finally 陳述式,散佈您的程式碼或一再重複相同的程式碼。在此使用簡單的字串沒有意義,因為 System.String 是不變。我使用一個簡單的字串物件,來簡化範例。

基本概念是可以修改您的執行個體的狀態的所有方法,必須都加入 IWriteToShared 介面。在此同時,只能從您的執行個體讀取的所有方法應該都加入 IReadFromShared 介面。藉由分隔這類的考量,為兩個不同的介面,並在您的基礎類型上實作這兩個介面,您接著可以使用同步器類別來同步處理至您的執行個體的存取。就這樣,同步處理您的程式碼的存取權的藝術變得簡單許多,並可以執行大部分的情況下更多的宣告式的方式。

說到多執行緒程式設計、 syntactic sugar 可能成功和失敗之間的差異。偵錯多執行緒程式碼通常非常困難,而同步處理物件的建立單元測試可以是 futility 的運用。

如果您想,您可以建立多載的型別,只有一個泛型引數,繼承自原始的同步器類別,並轉移其單一的泛型引數型別引數為三次到其基底類別即可。這麼做,您不需要讀取或寫入的介面,因為您可以直接使用您型別的具象實作。不過,此方法需要,您以手動方式處理這些組件,必須使用寫入或讀取的方法。它也是稍微較不安全,但確實可讓您包裝您無法將變更同步處理程式執行個體的類別。

您分岔的 lambda 集合

一旦您已採取到神奇的 lambda 的第一個步驟 (或委派,所謂C#),並不難想像一下您可以執行多個與它們。比方說,一般週期性主題中的多執行緒是能夠連絡到其他伺服器,以擷取資料並傳回資料送回呼叫端的多個執行緒。

最基本的例子可從 20 的網頁,讀取資料的應用程式,並完成傳回的 HTML 回到建立某種類型的所有頁面的內容為基礎的彙總結果的單一執行緒時。除非您建立一個執行緒的每個擷取方法時,此程式碼將會遠比預期還要慢,所有的執行時間的 99%會可能會花在等候傳回的 HTTP 要求。

在單一執行緒上執行此程式碼沒有效率,而且建立執行緒的語法很難正確。挑戰化合物,為您支援多個執行緒,以及其附帶的物件,強制重複本身,因為它們撰寫程式碼的開發人員。之後您所見,您可以建立委派和類別,將它們包裝的集合,您可以建立您的所有執行緒的單一方法引動過程。就這樣,建立的執行緒變得更少吃點苦頭。

在 [ [圖 3您會發現一段程式碼會建立兩個以平行方式執行這類 lambda。請注意,此程式碼實際上是從 Lizzie 指令碼語言,您可以在找到我第一版的單元測試bit.ly/2FfH5y8

[圖 3 建立的 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");
}

如果您仔細看一下這段程式碼,您會發現不假設我 lambda 的任何一個,在其他正在執行評估的結果。沒有明確指定的執行順序,並在個別執行緒上執行這些 lambda。這是因為動作類別**[圖 3**可讓您加入委派,因此您可以稍後再決定是否您想要執行平行或循序的委派。

若要這樣做,您必須建立一大堆 lambda,並使用您慣用的機制加以執行。您可以看到先前所述同步器類別中的**[圖 3**,同步處理共用的字串資源的存取權。不過,它會使用新方法上同步處理程式,稱為 「 我並未包含在清單中的指派**[圖 1**我同步器的類別。指派方法會使用同一個 「 lambda 以前 」,我先前所述的 Write 和 Read 方法中。

如果您想要研究的動作類別的實作,請注意,務必下載版本 0.1 Lizzie,因為我完全重寫成獨立的程式設計語言,在較新版本的程式碼。

函式程式設計C#

大部分的開發人員傾向於將C#為幾乎的同義詞,或至少密切相關,物件導向程式設計 (OOP) — 而且很顯然。不過,藉由重新思考如何使用C#,並探討其功能的層面,就會比較容易解決一些問題。其目前格式的 OOP 不只是非常重複使用好記,以及大量的原因是,它強型別。

比方說,重複使用單一類別會強制您初始的類別所參考的每個單一類別,重複使用 — 這兩個所使用透過撰寫,並透過繼承。此外,類別重複使用會強制您重複使用所有的類別,這些協力廠商類別參考,並依此類推。而且如果這些類別會實作不同的組件中,您必須包含一套完整的組件,只要能夠存取單一類型上的單一方法。

我一次讀取需比喻來說明此問題:「 您想要的 banana,但您得到一隻,保留 banana 和雨林 gorilla 所在的位置 」。 比較具有更多動態語言,JavaScript,只要它會實作您的函式會使用本身的函式,不在意您的類型,例如重複使用這種情況。稍微較弱型別的方法會產生更具彈性且更輕鬆地重複使用程式碼。委派可讓您執行此作業。

您可以使用C#可重複使用程式碼的改善跨多個專案的方式。您只需要知道函式或委派也可以是物件,您可以以弱型別的方式處理這些物件的集合。

在 2018 年 11 月號 MSDN Magazine 的想法出現在這篇文章的委派建置那些事情發生在之前的文章我撰寫了,「 建立您自己指令碼語言與符號委派 」 (msdn.com/magazine/mt830373).這篇文章也引進了 Lizzie,欠它的存在,此委派為中心的思維模式來我 homebrew 指令碼語言。如果我已建立 Lizzie 使用 OOP 規則,我認為是,它可能會在至少一個級距較大的大小。

當然,OOP 和強型別目前在這類主控項的位置是幾乎很難找到沒有提到作為其主要的必要技能的作業描述。在此我建立了 OOP 程式碼超過 25 年的資料,因此我已經為連載的強型別偏差的任何人。現在,不過,我是在我方法中撰寫程式碼,更實用,而且小於想要了解如何尋找我的類別階層最後。

不,謝謝不美觀的類別階層架構,但有遞減報酬。多個類別新增至階層中,就越不簡潔得,直到它摺疊在其自己的權數。有時候,優異的設計有幾個方法,則較少的類別,而大部分是鬆散偶合的函式,允許程式碼輕鬆地擴充,而不需要 「 自備 gorilla 和雨林。 」

我回到週期性的佈景主題的文章,靈感的音樂英哩 Davis 的方法,其中少就是多和 「 無回應 」 比音效更重要。 程式碼是這樣,在過。Magic 通常存留的線條,之間,最理想的解決方案可以測量更什麼您沒有撰寫程式碼,而不是您執行的動作。任何不聰明的一面可以讓 trumpet 並雜訊,但是少可以從中建立音樂。而且更少仍然會讓 magic,未英哩的方式。


Thomas Hansen金融科技和 ForEx 業界身為軟體開發人員的運作方式,並且住在賽普勒斯。


MSDN Magazine 論壇中的這篇文章的討論