程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 多語言編程人員 - 借助STM.NET處理ACID事務

多語言編程人員 - 借助STM.NET處理ACID事務

編輯:關於.NET

本專欄專門介紹編程語言,但您會發現有時語言理念不需要直接進行修改就可以轉換成其他語言,這 一點很有意思。

Microsoft Research 語言 C-Omega 就是這樣的一個例子,該語言有時簡寫為 Cw,因為希臘語 Omega 符號看起來很像 US 鍵盤布局上的小寫字母 w。C-Omega 除引入了許多數據統一和代碼統一概念 (可最終作為 LINQ 轉換為 C# 和 Visual Basic 語言)之外,還提供了一種新的名為“chords”的並 發方法,可將 C-Omega 保存到 Joins 庫中。雖然在撰寫本文時,Joins 產品尚未推出,但整個 chords 並發概念可通過某個庫進行提供,這意味著任何普通的 C# 或 Visual Basic(或其他 .NET 語言)程序 都可以利用此概念。

除了此概念外,還提供了一個 Code Contracts 工具。契約式設計是一種語言功能,在 Eiffel 等語 言中起到了顯著作用,該功能最初通過 Microsoft Research 語言 Spec# 應用於 .NET。類似類型的契 約式保證系統也是通過 Microsoft Research 引入的,其中包括我最喜歡的產品之一 Fugue,它利用自 定義屬性和靜態分析檢查客戶端代碼是否正確。

再次重申,盡管 Code Contracts 尚未作為正式產品發布,也沒有獲得允許在生產軟件中使用的許可 證,但它作為一種庫而不是一種獨立的語言存在,本身就意味兩層含義。首先,在理論上,Code Contracts 可由任何 .NET 開發人員作為庫編寫,這足夠說明具有類似的功能。其次,該產品提到的功 能(假定確實具有這些功能)可用於各種語言,包括 C# 和 Visual Basic。

如果您嗅到了一絲主題的氣息,那麼您猜對了!本月,我要重點介紹一個最近發布的源於多語言世界 的庫:軟件事務內存或 STM。STM.NET 庫可通過 DevLabs 網站下載,但與我提到的其他一些實現大相徑 庭的是,它不是一個獨立的庫,不可以鏈接到程序或作為靜態分析工具運行,從整體看,它是 .NET 基 類庫的一個替代和補充等。

但是請注意,STM.NET 的當前實現與當前 Visual Studio 2010 Beta 的兼容性不是很理想,所以在 這種情況下,您所關心的有關在計算機上安裝未完成的/Beta/CTP 軟件的常見免責聲明,一定要加倍注 意。該產品應該與 Visual Studio 2008 一起安裝,但我仍不會將其安裝在您的工作機上。下面是另一 個示例,其中 Virtual PC 是您主要使用的工具。

入門

雖然 STM.NET 綜合了多種不同的語言,但 STM 的概念非常直觀並易於理解:並非強制開發人員重點 研究實現操作並發的方法,如鎖定等,而是允許他們在具備特定的支持並發特性時標記應該執行哪部分 代碼,並在必要時使用語言工具(編譯器或解釋器)管理鎖定。換句話說,STM.NET 與數據庫管理員和 用戶的性質一樣,可使程序員使用 ACID 樣式的事務性語義標記代碼,並將管理鎖定的單調工作留給基 礎環境。

雖然 STM.NET 可能看起來只是管理並發的另一種嘗試,但實際上 STM 的作用遠不止這些,它嘗試將 數據庫 ACID 事務的全部四種特質引入內存編程模型。除了代表程序員對鎖定進行管理外,STM 模型還 提供了原子性、一致性、隔離和持續性,無論同時存在多個執行線程,單憑這些特性就可使編程更簡單 。

下面以偽代碼(已屢見不鮮)為例,請注意這種情況:

BankTransfer(Account from, Account to, int amount) {
  from.Debit(amount);
  to.Credit(amount);
}

如果 Credit 失敗並引發一個異常,將出現什麼情況?如果來源帳戶的借方仍有記錄,而貸方卻沒有 相應進項,顯然用戶會很不樂意,此時開發人員就得去補救了:

BankTransfer(Account from, Account to, int amount) {
  int originalFromAmount = from.Amount;
  int originalToAmount = to.Amount;
  try {
   from.Debit(amount);
   to.Credit(amount);
  }
  catch (Exception x) {
   from.Amount = originalFromAmount;
   to.Amount = originalToAmount;
  }
}

這乍一看好像負面影響很大。但是請記住,根據對 Debit 和 Credit 方法的精確實現程度,在 Debit 操作完成之前或 Credit 操作完成之後(但沒有結束)可能會引發異常。這就意味著 BankTransfer 方法必須確保:在此操作中引用和使用的所有數據都可以回到操作開始時的准確狀態。

如果 BankTransfer 變得更復雜,例如,同時對三個或四個數據項執行操作,則 catch 塊中的恢復 代碼會立刻變得非常混亂。而且,我不得不承認此模式的出現頻率非常高。

需要注意的另一個方面是隔離。在原始代碼中,另一個線程如果在執行結余操作時進行讀取,則讀取 的余額可能不正確且至少損壞一個帳戶。此外,如果只是對結余操作進行鎖定,而 from/to 對有時順序 無常,則可能遇到死鎖情況。STM 不使用鎖定,就可以對結余操作進行保護。

但是,如果該語言提供某種類型的事務性操作,例如,可以實際處理鎖定和失敗/回滾的原子性關鍵 字,就像 BEGIN TRANSACTION/COMMIT 可以對數據庫執行的操作一樣,則編寫 BankTransfer 示例的代 碼會變得非常簡單,如下所示:

BankTransfer(Account from, Account to, int amount) {
  atomic {
   from.Debit(amount);
   to.Credit(amount);
  }
}

您不得不承認,這解決了很多麻煩。

但是,基於庫的 STM.NET 方法不會提供多少事務性操作,因為 C# 語言不允許這種靈活程度的語法 。而您可以使用以下代碼行解決一些問題:

public static void Transfer(
  BankAccount from, BankAccount to, int amount) {
  Atomic.Do(() => {
   // Be optimistic, credit the beneficiary first
   to.ModifyBalance(amount);
   from.ModifyBalance(-amount);
  });
}

深入分析:這確實是語言的一大變革

遺憾的是,在重讀 Neward 的專欄時,我突然意識到我對它產生了基本誤解。Neward 嘗試將語言擴 展劃分為兩種:需要進行語言更改的擴展和完全屬於庫更改的擴展。該專欄嘗試將 STM.NET 劃分為後者 (僅限於庫更改),但我堅決認為這是錯誤的。

僅限於庫更改的擴展是可以完全在現有語言中實現的一種擴展。基於庫的 STM 系統確實存在;在這 些系統中,通常要求將應采用事務性語義的數據聲明為某種特殊類型,例如“TransactionalInt”。 STM.NET 則不同,它可以為普通數據透明地提供事務性語義,只是因為它本身能夠在動態事務范圍內進 行訪問。

這要求對在事務中執行的代碼進行的每次讀寫操作進行修改,以便進行其他要求必要鎖定的關聯調用 、創建和填充卷影副本等。在實現時,我們大范圍地修改了 CLR 的 JIT 編譯器,以生成要在事務中執 行的差別迥異的代碼。原子性關鍵字(即使我們是通過基於委托的 API 提供的也是一樣)從根本上更改 了語言語義。

因此,我認為我們確實改變了語言。在 C# 等 .NET 語言中,語言語義通過兩個方面才可以實現:源 代碼級語言編譯器以及該編譯器對本身聲明的 MSIL 的語義的假設 — CLR 運行時執行 IL 的方式。我 們從根本上改變了 CLR 對字節碼的解釋,所以我認為這改變了語言。

請特別注意,假設 CLR 的 JIT 編譯器遇到以下代碼:

try {
  <body>
}
catch (AtomicMarkerException) {}

需要動態修改 <body> 中的代碼(在其調用的方法中遞歸執行)才能確保事務性語義。我應該 強調的是,這與處理異常絕對沒有任何關系 — 這只是一個用於識別原子塊的技巧,因為 try/catch 結 構是 IL 中唯一可用於識別詞義塊的機制。從長遠來看,我們希望 IL 語言中出現一些更類似於顯式“ 原子”塊的內容。為此,基於委托的接口應運而生。

總之,IL 級原子塊雖然已表達,但確實從根本上更改了在其中運行的代碼的語義。這就是為什麼 STM.NET 包含一個新的、進行大量修改的 CLR 運行時而不只是更改 BCL 的原因。如果您采用了存儲 CLR 運行時並將其與 BCL 從 STM.NET 中一起運行,則不會獲得事務性語義(事實上,我懷疑它根本就 不能運行)。

—Dave Detlefs 博士,Microsoft 公共語言運行時架構師

語法不像 原子性關鍵字那樣簡潔,但 C# 可以通過匿名方法來捕獲用於構成所需原子塊的主體的代碼塊,因此它 可以在相似類型的語義下執行。(非常遺憾,直至撰寫本文時,STM.NET 的初步成果僅可支持 C#。它不 能在所有語言中使用並不是由於技術原因,而是因為 STM.NET 團隊為首次發行只重點研究了 C#。)

STM.NET 入門

您首先需要從 DevLabs 網站下載 Microsoft .NET Framework 4 Beta 1, 才能使用 Software Transactional Memory V1.0 內部測試版,該名稱過於繁瑣,我簡稱為 STM.NET BCL 或 STM.NET。登錄該網站後,還要下載 STM.NET 文檔和示例。前者是真正的 BCL 和 STM.NET 工具 及補充程序集,後者包含一個用於構建 STM.Net 應用程序的 Visual Studio 2008 模板,其中提供了一 個文檔和大量示例項目。

創建新的啟用 STM.NET 的應用程序與任何其他應用程序一樣,首先打 開“新建項目”對話框(請參見圖 1)。選擇 TMConsoleApplication 模板時需要執行幾項 操作,其中一些不是很直觀。例如,直至撰寫本文時,對 STM.NET 庫執行操作時,.NET 應用程序的 app.config 需要使用以下版本控制技巧:

圖 1 使用 TMConsoleApplication 模板開始創建新項 目

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup>
   <requiredRuntime version="v4.0.20506"/>
  </startup>
  ...
</configuration>

此時將顯示其他設置,但需要使用 requiredRuntime 值告知 CLR 啟動器填充程序根據運行時的 STM.NET 版本進行綁定。此外,TMConsoleApplication 模板將根據 mscorlib 版本將程序集與 STM.NET 的安裝目錄中安裝的 System.Transactions 程序集綁定在一起,而不是根據存儲 .NET Framework 3.0 或 3.5 CLR 附帶的版本。必須注意這個問題,因為如果 STM.NET 為您編寫的代碼之外的任何代碼提供 事務性訪問,則需要使用其自身的 mscorlib 副本。另外,如果它與其他形式的事務(例如輕型事務管 理器 (LTM) 提供的輕型事務)正確交互,則還需要具有自身版本的 System.Transactions。

除此之外,STM.NET 應用程序也是一個傳統的 .NET 應用程序,使用 C# 編寫,編譯到 IL 中以及與 其余未修改的 .NET 程序集相鏈接等。與過去十年中出現的 COM+ 和 EnterpriseServices 組件一樣, STM.NET 程序集中也會包含更多的擴展,用於描述與 STM.NET 事務性行為進行交互的方法的事務性行為 ,我會對相應內容及時進行介紹。

你好,STM.NET

由於 2009 年 9 月出版的《MSDN 雜志》中的 Axum 示例,在使用 STM.NET 時,先編寫一個傳統的 Hello World 應用程序事實上比您最初想象的要難,主要是因為如果您在編寫時不涉及事務,則與傳統 的使用 C# 編寫的 Hello World 沒有什麼兩樣。如果在編寫時利用了 STM.NET 事務行為,則必須考慮 將文本寫入控制台實際上是一種可以實現的方法(至少 STM.NET 可以辦到),這就意味著很難嘗試回滾 Console.WriteLine 語句。

所以,我們通過《STM.NET 用戶指南》中的一個簡單示例來快速演示 STM.NET 內部測試版。在該示 例中,MyObject 對象包含兩個私有字符串和一個方法,該方法用於將這兩個字符串設為某一對值:

class MyObject {
  private string m_string1 = "1";
  private string m_string2 = "2";

  public bool Validate() {
   return (m_string1.Equals(m_string2) == false);
  }
  public void SetStrings(string s1, string s2) {
   m_string1 = s1;
   Thread.Sleep(1);  // simulates some work
   m_string2 = s2;
  }
}

因為為字段分配參數本身就是一個原子性操作,所以此時不需要考慮並發。但是正如之前顯示的 BankAccount 示例,您希望設置兩者或者都不進行設置,並且在設置操作期間不希望顯示部分更新(一 個字符串正在設置,而另一個沒有)。您將生成兩個線程,對這兩個字符串盲目地重復設置,而第三個 線程用來驗證 MyObject 實例的內容,如果事件 Validate 返回 false 則報告一個沖突(請參見圖 2) 。

圖 2 手動驗證對 MyObject 的原子性更新

[AtomicNotSupported]
static void Main(string[] args) {
  MyObject obj = new MyObject();
  int completionCounter = 0; int iterations = 1000;
  bool violations = false;

  Thread t1 = new Thread(new ThreadStart(delegate {
   for (int i = 0; i < iterations; i++)
    obj.SetStrings("Hello", "World");
   completionCounter++;
  }));

  Thread t2 = new Thread(new ThreadStart(delegate {
   for (int i = 0; i < iterations; i++)
    obj.SetStrings("World", "Hello");
   completionCounter++;
  }));

  Thread t3 = new Thread(new ThreadStart(delegate {
   while (completionCounter < 2) {
    if (!obj.Validate()) {
     Console.WriteLine("Violation!");
     violations = true;
    }
   }
  }));

  t1.Start(); t2.Start(); t3.Start();
  while (completionCounter < 2)
   Thread.Sleep(1000);

  Console.WriteLine("Violations: " + violations);
...

請注意此示例的構建方式,如果將 obj 中的兩個字符串設置為同一值,則驗證失敗,說明線程 t1 的 SetStrings(“Hello”、“World”)進行了部分更新(使第一個“Hello”與 t2 設置的第二個 “Hello”相匹配)。

粗略看一下 SetStrings 的實現過程,則會發現此代碼很難達到線程安全。如果中途發生線程切換( 可能會調用 Thread.Sleep,這會導致當前正在執行的線程放棄其時間片),則另一線程會輕易地再次跳 到 SetStrings 的中間位置,使 MyObject 實例處於無效狀態。運行該實現,經過足夠的迭代後,開始 出現沖突。(在便攜式計算機上,我必須運行該實現兩次才會顯示沖突,這充分證明了:運行一次沒有 發生錯誤並不意味著該代碼不存在並發錯誤。)

要修改此代碼以使用 STM.NET,只需要對 MyObject 類進行很小的更改,如圖 3 所示。

圖 3 使用 STM.NET 驗證 MyObject

class MyObject {
  private string m_string1 = "1";
  private string m_string2 = "2";

  public bool Validate() {
   bool result = false;
   Atomic.Do(() => {
    result = (m_string1.Equals(m_string2) == false);
   });
   return result;
  }

  public void SetStrings(string s1, string s2) {
   Atomic.Do(() => {
    m_string1 = s1;
    Thread.Sleep(1); // simulates some work
    m_string2 = s2;
   });
  }
}

您可以看到,唯一需要修改的地方就是使用 Atomic.Do 操作將 Validate 和 SetStrings 的主體封 裝到原子性方法中。現在運行就不會顯示沖突了。

事務性關聯

善於觀察的讀者可能已經發現了圖 2 中 Main 方法頂部的 [AtomicNotSupported] 屬性,可能想了 解它的用途,或更想知道它是不是與 COM+ 提供的屬性的作用一樣。事實證明,確實是這樣:STM.NET 環境需要一些支持,才可以了解在運行原子塊時調用的方法是否具有事務性,以便它才可以為這些方法 提供必要和所需的支持。

當前的 STM.NET 版本中可以提供以下三種屬性:

AtomicSupported — 程序集、方法、字段或委托支持事務性行為,可在原子塊內外成功使用。

AtomicNotSupported — 程序集、方法或委托不支持事務性行為,因此不應在原子塊內部使用。

AtomicRequired — 程序集、方法、字段或委托不光支持事務性行為,僅應在原子塊內不使用(這可 以保證始終在事務性語義下使用此項目)。

從技術角度講,還有第四個屬性 AtomicUnchecked,它可以向 STM.NET 傳達在某個時段內不應檢查 某個項目。它作為一個應急出口,以避免同時檢查所有代碼。

在嘗試運行以下簡單代碼時,AtomicNotSupported 屬性將導致 STM.NET 系統引發 AtomicContractViolationException:

[AtomicNotSupported]
static void Main(string[] args) {
  Atomic.Do( () => {
   Console.WriteLine("Howdy, world!");
  });

  System.Console.WriteLine("Simulation done");
}

因為沒有使用 AtomicSupported 對 System.Console.WriteLine 方法進行標記,所以當 Atomic.Do 遇到原子塊中的調用時會引發異常。此安全措施可確保在原子塊內僅執行具事務性的方法,並為代碼提 供其他安全保護。

你好,STM.NET(第二部分)

如果您一定要編寫傳統的 Hello World,應該怎麼辦?如果您非常希望在執行其他兩個事務性操作的 同時在控制台上顯示一行文字(或寫入文件,或執行一些其他非事務性行為),但僅在這兩個操作都成 功時才可以顯示,您應該怎麼辦?STM.NET 提供了三種方法可以解決此問題。

第一種方法,通過將代碼置於傳遞到 Atomic.DoAfterCommit 的塊中,在事務之外(而且僅在事務提 交後)執行非事務性操作。因為該塊中的代碼通常要使用從事務內生成或修改的數據,所以 DoAfterCommit 將從事務內傳遞到該代碼塊的上下文參數作為其唯一的參數。

第二種方法,您可以通過調用 Atomic.DoWithCompensation 創建一個補償操作,以在事務最終失敗 時執行,其此方法也會使用上下文參數將數據從事務內部封送到提交或補償代碼塊(根據需要)。

第三種方法,您可以執行與上面完全相同的操作並創建事務性資源管理器 (RM),以了解如何使用 STM.NET 事務性系統。創建過程實際上比看上去簡單 — 只需繼承 STM.NET 類 TransactionalOperation,它包含 OnCommit 和 OnAbort 方法,您可對這兩種方法進行覆蓋以分別提供 適當的行為。使用此新類型的 RM 時,首先使用它調用 OnOperation,從而有效地使資源參與到 STM.NET 事務中。如果其他操作失敗,則對其調用 FailOperation。

因此,如果要事務性寫入某個基於文本的流,您可以編寫一個添加文本的資源管理器,如圖 4 所示 。這樣,您就可以在原子塊內通過 TxAppender 寫入某個文本流,事實上這要求您借助 [Atomic- Required] 屬性來完成(請參見圖 5)。

圖 4 事務性資源管理器

public class TxAppender : TransactionalOperation {
  private TextWriter m_tw;
  private List<string> m_lines;

  public TxAppender(TextWriter tw) : base() {
   m_tw = tw;
   m_lines = new List<string>();
  }

  // This is the only supported public method
  [AtomicRequired]
  public void Append(string line) {
   OnOperation();

   try {
    m_lines.Add(line);
   }
   catch (Exception e) {
    FailOperation();
    throw e;
   }
  }

  protected override void OnCommit() {
   foreach (string line in m_lines) {
    m_tw.WriteLine(line);
   }
   m_lines = new List<string>();
  }

  protected override void OnAbort() {
   m_lines.Clear();
  }
}

圖 5 使用 TxAppender

public static void Test13() {
  TxAppender tracer = 
   new TxAppender(Console.Out);
  Console.WriteLine(
   "Before transactions. m_balance= " +
   m_balance);

  Atomic.Do(delegate() {
   tracer.Append("Append 1: " + m_balance);
   m_balance = m_balance + 1;
   tracer.Append("Append 2: " + m_balance);
  });

  Console.WriteLine(
   "After transactions. m_balance= "
   + m_balance);

  Atomic.Do(delegate() {
   tracer.Append("Append 1: " + m_balance);
   m_balance = m_balance + 1;
   tracer.Append("Append 2: " + m_balance);
  });

  Console.WriteLine(
   "After transactions. m_balance= "
   + m_balance);
}

很明顯,這個操作需要的時間比較長,所以僅適用於特定的情形。對於一些媒體類型,此操作可能會 失敗,但對於大多數媒體類型,如果所有實際不可逆行為延遲到 OnCommit 方法,則此操作足以滿足您 大部分進程內的事務性需求。

使用 STM.NET

使用 STM 系統需要一小段適應過程,但一旦習慣了,您就離不開它了。想想哪些可能的情況使用 STM.NET 可以簡化編碼。

當與其他事務處理資源一起使用時,STM.NET 可快速、輕松地集成到現有事務處理系統中,使 Atomic.Do 成為系統中唯一的事務處理的代碼源。STM.NET 示例在 TraditionalTransactions 示例中對 此進行了演示,即向 MSMQ 專用隊列發布消息並聲明當原子塊失敗時不會向該隊列發布任何消息。這種 用法可能最顯而易見。

在對話框中(尤甚是多步驟向導進程對話框或設置對話框),用戶點擊“取消”按鈕就可使對設置或 對話框數據元素的更改回到更改前的狀態,這一功能非常有用。

單元測試(如 NUnit、MSTest 和其他系統)將盡最大努力來確保:如果測試編寫正確,不會將某個 測試的結果洩露給下一個測試。如果 STM.NET 達到生產狀態,NUnit 和 MSTest 可以重構其測試案例執 行代碼以使用 STM 事務將測試結果相互分隔開來,這將在每個測試方法結束時生成一個回滾,從而清除 測試可能生成的任何更改。此外,在執行測試時,在 AtomicUnsupported 方法之外調用的任何測試都將 標記為錯誤,而不是將測試結果自行洩露給測試環境外的媒體(例如磁盤或數據庫)。

STM.NET 還可以用於實現域對象屬性。雖然大多數域對象的某些屬性非常簡單,可分配給字段或返回 該字段的值,但對於使用多步算法的更復雜的屬性會面臨多個線程導致的風險:顯示部分更新(當其他 線程調用正在設置的屬性)或虛構的更新(如果其他線程調用正在設置的屬性,由於某種形式的驗證錯 誤,最終會放棄原始更新)。

更有趣的是,Microsoft 外的研究人員正在研究將事務擴展到硬件,以便將來可以由內存芯片本身對 更新對象的字段或本地變量提供硬件級別的事務性保護,與如今的方法相比,事務可以飛速發展。

但是,對於 Axum,Microsoft 依靠您的反饋來決定此技術是否值得繼續研究和全面推廣,所以如果 您對這個想法感興趣,或者其中缺少一些對您的編碼實踐很重要的內容,請告知 Microsoft,他們很樂 意傾聽您的建議。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved