次の方法で共有


基礎

トランザクションをサービスに簡単に適用する

Juval Lowy

コードは MSDN コード ギャラリーからダウンロードできます。
オンラインでのコードの参照

目次

状態管理とトランザクション
呼び出しごとのトランザクション サービス
インスタンスの管理とトランザクション
セッション ベースのサービスと VRM
永続性のあるトランザクション サービス
トランザクションの動作
IPC バインドにコンテキストを追加する
InProcFactory とトランザクション

プログラミングには、エラーからの回復という基本的な問題があります。エラーが発生した後で、アプリケーションはエラー発生前の状態を回復する必要があります。アプリケーションで実行される操作について考えてみましょう。ある操作を構成している、より小さな複数の操作が同時に実行され、それぞれが別個に成功または失敗する可能性があるとします。複数の小さな操作のいずれかでエラーが発生すると、システムの状態の一貫性が損なわれます。

たとえば、銀行取り引きアプリケーションを例にとると、2 口座間で資金を送金する操作では、1 つの口座で引き落としが行われ、もう 1 つの口座で入金が行われます。一方の口座で引き落としに成功し、もう一方の口座で入金に失敗した場合、一貫性のない状態が生じます (資金は同時に 2 つの場所に存在できないからです)。また、一方の口座で引き落としに失敗し、もう一方の口座で入金に成功した場合も、同様に一貫性のない状態です (資金が失われることになるからです)。システムのエラーから回復してシステムを元の状態に戻す作業は、常にアプリケーションに依存しています。

いくつか理由がありますが、これは言うほど簡単ではありません。まず最初に、大規模な操作の場合、部分的な成功と部分的な失敗の順列の純粋な数は、たちまち手に負えないほど多くなります。その結果、開発と保守のコストが非常に高く、あまり役に立たないことの多い、脆弱なコードになってしまいます。開発者は、自分たちにとって既知の状況で対応方法もよくわかっている、簡単な回復ケースのみを処理しがちだからです。次の理由は、複合的な操作が、さらに大きな操作の一部になっている場合があることです。自分が管理するコードの実行が成功しても、管理していない範囲でエラーが発生した場合、自分のコードを実行し直す必要があります。つまり、参加しているパーティ間には操作の管理と連携に関する密結合があります。最後の理由は、自分が実行する操作と、他の人間がシステムとの対話で行う操作とを切り離す必要があるということです。これは、アクションの実行後にその一部をロール バックすることでエラーから回復した場合、別のユーザーを暗黙的にエラー状態にしてしまう可能性があるからです。

おわかりのとおり、堅牢なエラー回復コードを手動で記述することは実質的に不可能です。この認識は新しいものではありません。1960 年代にビジネス コンテキストでのソフトウェアの使用が始まった当初から、回復を管理する優れた方法を見つけるべきであるということはわかっていました。より良い方法を紹介しましょう。それはトランザクションです。トランザクションとは、操作のセットです。セット内の操作のいずれかにエラーが発生すると、セット全体が 1 つのアトミック操作として失敗します。トランザクションを使用する場合、回復ロジックを記述する必要はありません。回復する対象がないからです。すべての操作が成功した場合は言うまでもなく、すべての操作が失敗した場合もシステムの状態には影響が及ばないため、回復は必要ありません。

トランザクションを使用する場合、トランザクション リソース マネージャを使用することが重要です。リソース マネージャでは、トランザクションが中止された場合には、トランザクション中に行われた変更をすべてロール バックできます。トランザクションがコミットされた場合には、その変更を保持できます。リソース マネージャには、分離の機能も備わっています。つまり、トランザクションの進行中に、他のすべてのパーティ (そのトランザクション以外) からのアクセスおよび変更の表示を禁止することによって、変更をロール バックできるようにしておきます。これは、トランザクションからリソース以外のマネージャへのアクセスも禁止であることを意味しています。リソース以外のマネージャに変更を加えた場合、その変更はトランザクションが中止されてもロール バックされず、回復が必要になるからです。

従来は、リソース マネージャはデータベースやメッセージ キューなどの永続的なリソースでした。これに対して、私は MSDN Magazine の 2005 年 5 月号に掲載された「.NET の揮発性リソース マネージャがトランザクションに共通の型をもたらす」において、Transactional<T> という汎用的な揮発性リソース マネージャ (VRM) を実装する手法を紹介しました。

public class Transactional<T> : ...
{
   public Transactional(T value);
   public Transactional();
   public T Value
   {get;set;}
   /* Conversion operators to and from T */
}

シリアル化が可能な任意の型パラメータ (int や string など) に Transactional<T> を指定すると、その型は本格的な揮発性リソース マネージャになります。そのリソース マネージャは、アンビエント トランザクションに自動的に参加し、トランザクションの結果に応じて変更をロール バックまたはコミットして、現在の変更を他のすべてのトランザクションから切り離します。

図 1 は、Transactional<T> の使用を示しています。スコープが完了していないため、トランザクションが中止され、number および city の値がそれぞれトランザクション前の状態に戻ります。

図 1 Transactional<T> の使用

Transactional<int> number = new Transactional<int>(3);
Transactional<string> city = new Transactional<string>("New York, ");

city.Value += "NY"; //Can use with or without transactions
using(TransactionScope scope = new TransactionScope())
{
   city.Value = "London, ";
   city.Value += "UK";
   number.Value = 4;
   number.Value++;
}
Debug.Assert(number == 3); //Conversion operators at work
Debug.Assert(city == "New York, NY");

Transactional<T> だけでなく、トランザクション配列や、System.Collections.Generic 内のすべてのコレクションに対するトランザクション バージョンも提供しました (TransactionalDictionary<K,T> など)。これらは、非トランザクションの同類を持つポリモーフィックなコレクションであり、完全に同じ方法で使用されます。

状態管理とトランザクション

トランザクション プログラミングの唯一の目的は、システムの一貫性のある状態を保持することです。Windows Communication Foundation (WCF) では、システムの状態はリソース マネージャおよびサービス インスタンスのメモリ内の状態で構成されています。リソース マネージャでは、トランザクションの結果としてその状態が自動的に管理されますが、メモリ内オブジェクトや静的変数の場合はそうではありません。

この状態管理の問題の解決策は、サービスを状態対応のサービスとして開発し、その状態を積極的に管理することです。トランザクション間で、サービスは状態をリソース マネージャに格納する必要があります。各トランザクションの最初に、サービスは状態をリソースから取得します。そうすることで、リソースをトランザクションに参加させます。トランザクションの最後に、サービスはその状態をリソース マネージャに格納します。この手法により、自動的な状態の回復が円滑に行われます。インスタンスの状態に対する変更はすべて、トランザクションの一部としてコミットまたはロール バックされます。

トランザクションがコミットされた場合、次にサービスが状態を取得したときにはトランザクション後の状態が取得されます。トランザクションが中止された場合、次回はトランザクション前の状態が取得されます。いずれの場合も、サービスは、新しいトランザクションからアクセス可能な一貫性のある状態を取得できます。

トランザクション サービスを記述するうえでの問題がまだ 2 つ残っています。最初の問題は、サービスがトランザクションの開始または終了を認識して、その状態を取得または保存する方法です。サービスは、複数のサービスやコンピュータにまたがる、より大きなトランザクションの一部である場合があります。呼び出し間に、いつトランザクションが終了するかは予測できません。だれがサービスを呼び出して、状態を保存するように指示すればよいでしょうか。次の問題は、分離に関することです。複数のクライアントが同時に、別々のトランザクションで、サービスを呼び出す場合があります。サービスは、1 つのトランザクションによる状態の変更と、別のトランザクションによる変更とをどのように切り離せばよいでしょうか。あるトランザクションが中止され、その変更がロール バックされたとします。このトランザクションの状態にアクセスした別のトランザクションが、その値に基づいて動作した場合、別のトランザクションで一貫性のない状態が生じる可能性があります。

両方の問題の解決策は、メソッド境界とトランザクション境界を等しくすることです。各メソッド呼び出しの最初に、サービスは状態をリソース マネージャから読み取り、各メソッド呼び出しの最後に、サービスは状態をリソース マネージャに保存する必要があります。これにより、トランザクションがメソッド呼び出し間に終了した場合、サービスの状態が保持されるか、またはその状態でロール バックされます。さらに、各メソッド呼び出しで状態を読み取ってリソース マネージャに格納することにより、分離の問題に対処できます。サービスは単純に、同時実行トランザクション間の状態へのアクセスを分離するようにリソース マネージャに指示すればよいからです。

サービスがメソッド境界とトランザクション境界を等しくするので、サービス インスタンスは各メソッド呼び出しの最後にトランザクションの結果に対して投票する必要があります。サービスの観点から見ると、トランザクションはメソッドが返されたときに完了します。WCF では、これは OperationBehavior 属性の TransactionAutoComplete プロパティによって自動的に行われます。このプロパティを true に設定すると、操作中に未処理の例外がなければ、WCF はコミットするための票を自動的に投じます。未処理の例外がある場合は、WCF は中止するための票を投じます。TransactionAutoComplete は既定で true になっているため、トランザクション メソッドは既定で自動完了を使用します。

//These two definitions are equivalent:
[OperationBehavior(TransactionScopeRequired = true,
                   TransactionAutoComplete = true)]   
public void MyMethod(...)
{...}

[OperationBehavior(TransactionScopeRequired = true)]   
public void MyMethod(...)
{...}

WCF トランザクション プログラミングの詳細については、2007 年 5 月号に掲載された私の「基礎」コラムの「WCF トランザクションの伝達」を参照してください。

呼び出しごとのトランザクション サービス

呼び出しごとのサービスでは、呼び出しから戻るとそのインスタンスは破棄されます。したがって、呼び出し間の状態を格納するために使用されるリソース マネージャは、インスタンスのスコープ外にある必要があります。クライアントおよびサービスが、リソース マネージャからのインスタンスの作成または削除をどの操作によって行うかについて同意することも必要です。

同じサービス タイプで同じリソース マネージャにアクセスするインスタンスが多数存在する可能性があるため、各操作には、サービス インスタンスがリソース マネージャで状態を見つけてバインドするためのパラメータを含める必要があります。このパラメータをインスタンス ID と呼びます。クライアントでは、インスタンスをストアから削除するための専用の操作を呼び出すことも必要です。状態対応のトランザクション オブジェクトと呼び出しごとのオブジェクトの動作の要件は同じです。両方ともメソッド境界で状態を取得および保存します。呼び出しごとのサービスにより、任意のリソース マネージャを使用してサービス状態を格納できます。データベースを使用したり、VRM を使用したりできます (図 2 を参照)。

図 2 VRM を使用した呼び出しごとのサービス

[ServiceContract]
interface IMyCounter
{
   [OperationContract]
   [TransactionFlow(TransactionFlowOption.Allowed)]
   void Increment(string instanceId);

   [OperationContract]
   [TransactionFlow(TransactionFlowOption.Allowed)]
   void RemoveCounter(string instanceId);
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyService : IMyCounter
{
   static TransactionalDictionary<string,int> m_StateStore = 
                               new TransactionalDictionary<string,int>();

   [OperationBehavior(TransactionScopeRequired = true)]
   public void Increment(string instanceId)
   {
    if(m_StateStore.ContainsKey(instanceId) == false)
      {
         m_StateStore[instanceId] = 0;
      }
      m_StateStore[instanceId]++;
      Trace.WriteLine(m_StateStore[instanceId]); 
   }
   [OperationBehavior(TransactionScopeRequired = true)]
   public void RemoveCounter(string instanceId)
   {
    if(m_StateStore.ContainsKey(instanceId))
      {
         m_StateStore.Remove(instanceId);
      }
   }
}

//Client side:
MyCounterClient proxy = new MyCounterClient();

using(TransactionScope scope = new TransactionScope())
{
   proxy.Increment("MyInstance");
   scope.Complete();
}    

//This transaction will abort since the scope is not completed 
using(TransactionScope scope = new TransactionScope())
{
   proxy.Increment("MyInstance");
} 

using(TransactionScope scope = new TransactionScope())
{
   proxy.Increment("MyInstance");
   proxy.RemoveCounter("MyInstance");
   scope.Complete();
}

proxy.Close();

//Traces:
1
2
2

インスタンスの管理とトランザクション

WCF では、サービス インスタンスでメソッド境界とトランザクション境界が等しくなるように強制されているので、すべてのインスタンスの状態がメソッド境界でパージされます。既定では、トランザクションが完了すると、WCF ではサービス インスタンスを破棄し、一貫性を損なうおそれのあるものがメモリ内に残らないようにします。

トランザクション サービスのライフサイクルは、ServiceBehavior 属性の ReleaseServiceInstanceOnTransactionComplete プロパティによって制御されます。ReleaseServiceInstanceOnTransactionComplete が true (既定値) に設定されている場合は、メソッドがトランザクションを完了した後でサービス インスタンスが破棄されます。実質的に、インスタンス プログラミング モデルの場合は、すべての WCF サービスが呼び出しごとのサービスになります。

この強制的な手法は WCF から始まったものではありません。MTS 以降、COM+ および Enterprise Services を通して、マイクロソフト プラットフォームの分散トランザクション プログラミング モデルでは、常にトランザクション オブジェクトと呼び出しごとのオブジェクトを同等と見なして扱ってきました。これらのテクノロジのアーキテクトは、複雑で非直感的なプログラミング モデルにおいてトランザクションのオブジェクトの状態が、開発者によって適切に管理されると考えていませんでした。重要な短所は、開発者がトランザクションの利点を活用するには、困難な呼び出しごとのプログラミング モデルを使用する必要があることです (図 2 を参照)。ほとんどの開発者が安心して使用できるのは、使い慣れた通常の Microsoft .NET Framework オブジェクトのセッション ベースのステートフルなプログラミング モデルです。

私は、個人的には、トランザクションを呼び出しごとのインスタンス化と同等に扱う手法を全面的に否定するわけではありませんが、概念的に言うと、この手法にはひずみがあると考えられます。スケーラビリティが求められる場合にのみ、呼び出しごとのインスタンス化モードを選択するべきであり、理想的には、オブジェクト インスタンスの管理およびアプリケーションのスケーラビリティからトランザクションを切り離す必要があります。

アプリケーションを拡張する必要がある場合は、呼び出しごとのモードの選択とトランザクションの使用を組み合わせることでより良い効果が得られます。ただし、スケーラビリティが必要でない場合 (ほとんどのアプリケーションではこちらが一般的なケースでしょう)、セッション ベースのステートフルなトランザクション サービスにすることをお勧めします。このコラムの後半では、セッション ベースのプログラミング モデルを有効化して維持しながら、トランザクションを一般的なサービスで使用する場合に起こる問題の解決策を提案します。

セッション ベースのサービスと VRM

WCF では、ReleaseServiceInstanceOnTransactionComplete を false に設定することにより、セッション セマンティクスをトランザクション サービスで維持できます。この場合、サービス開発者は WCF から離れ、トランザクションでのサービス インスタンスの状態の管理に集中することが可能です。セッションごとのサービスでは、メソッド境界とトランザクション境界を等しくする必要があります。各メソッド呼び出しが異なるトランザクションのものである可能性があり、同一セッションのメソッド呼び出し間でトランザクションが終了する可能性があります。呼び出しごとのサービス同様、状態を手動で管理する場合がありますが (または、他の高度な WCF 機能を使用しますが、今回のコラムでは取り上げません)、図 3 に示すようにサービス メンバの VRM を使用することもできます。

図 3 セッションごとのトランザクション サービスによる VRM の使用

[ServiceBehavior(ReleaseServiceInstanceOnTransactionComplete = false)]
class MyService : IMyContract
{
   Transactional<string> m_Text = new Transactional<string>("Some initial value");
   TransactionalArray<int> m_Numbers = new TransactionalArray<int>(3);

   [OperationBehavior(TransactionScopeRequired = true)]
   public void MyMethod()
   {
      m_Text.Value = "This value will roll back if the transaction aborts";

      //These will roll back if the transaction aborts
      m_Numbers[0] = 11;
      m_Numbers[1] = 22;
      m_Numbers[2] = 33;
   }
}

VRM を使用することにより、ステートフルなプログラミング モデルが有効になります。トランザクションが関与していない場合と同じように、サービス インスタンスは単純に状態にアクセスします。状態に対する変更はすべて、トランザクションによりコミットまたはロール バックされます。ただし、図 3 は専門的なプログラミング モデルであり、特有の短所があります。このモデルでは、VRM を熟知していること、メンバを綿密に定義すること、およびすべての操作でトランザクションが要求されるように、また、完了時のインスタンスの解放を無効化するように構成することが必須となっています。

永続性のあるトランザクション サービス

2008 年 10 月号に掲載されたこのコラム (「永続性のあるサービスで状態を管理する」) で、私は WCF で提供される永続性のあるサービスのサポートについて説明しました。永続性のあるサービスでは各操作ごとに、状態を構成済みのストアから取得し、その状態をストアに戻します。トランザクション リソース マネージャが状態ストアである場合と、そうでない場合があります。

もちろん、サービスがトランザクション サービスである場合はトランザクション ストレージのみを使用し、各操作のトランザクションに参加させます。この場合、トランザクションが中止されると、状態ストアはトランザクション前の状態へとロール バックします。ただし、WCF は、トランザクションを状態ストアに伝達するようにサービスが設計されているかどうかを認識しません。既定では、ストレージがトランザクション リソース マネージャ (SQL Server 2005 や SQL Server 2008 など) であっても、WCF はストレージをトランザクション イベントに参加させません。トランザクションを伝達して基になるストレージを参加させるように WCF を構成するには、DurableService 属性の SaveStateInOperationTransaction プロパティを true に設定します。

[Serializable]
[DurableService(SaveStateInOperationTransaction = true)]
class MyService: IMyContract
{...}

SaveStateInOperationTransaction は既定で false になっています。したがって、状態ストレージはトランザクションに参加しません。SaveStateInOperationTransaction を true に設定すると、トランザクション サービスのみが恩恵を受けます。したがって、WCF ではこの値が true である場合に、サービス上のすべての操作で TransactionScopeRequired が true に設定されているか、または必須トランザクション フローがあることを要求します。TransactionScopeRequired が true に設定された状態で操作が構成されている場合、ストレージを参加させるために使用されるのは操作のアンビエント トランザクションです。

トランザクションの動作

DurableService 属性は永続性のある動作を示しているわけではないので、永続性を意味する "durable" という語は若干不適切です。この属性における永続性とは、WCF が構成済みのストレージから得たサービス状態を自動的に逆シリアル化し、各操作で再度シリアル化するということです。同様に、永続化プロバイダの動作は永続化を意味するわけではありません。規定された抽象プロバイダ クラスから派生したプロバイダで十分だからです。

永続性のあるサービス インフラストラクチャが実際にはシリアル化インフラストラクチャであるという事実によって、トランザクションでサービス状態を管理する手法にこのインフラストラクチャを利用できました。揮発性リソース マネージャに依存しながら、サービス インスタンスによる状態の管理は行いません。これにより、WCF のトランザクション プログラミング モデルはさらに合理化され、単なるオブジェクトおよび共通サービス用のトランザクションの優れたプログラミング モデルの利点を活用することができます。

最初のステップは、TransactionalMemoryProviderFactory および TransactionalInstanceProviderFactory という 2 つのトランザクション メモリ内プロバイダ ファクトリを定義することでした。TransactionalMemoryProviderFactory は、静的 TransactionalDictionary<ID,T> を使用してサービス インスタンスを格納します。辞書はすべてのクライアントおよびセッションで共有されます。ホストが動作中である限り、クライアントは TransactionalMemoryProviderFactory によってサービスの接続または接続解除を行うことができます。TransactionalMemoryProviderFactory を使用する場合は、DurableOperation 属性の CompletesInstance プロパティを使用して、ストアからインスタンス状態を削除する完了操作を指定する必要があります。

一方、TransactionalInstanceProviderFactory は、各セッションと Transactional<T> の固有のインスタンスのマッチングを行います。セッションが閉じられた後でサービス状態はガベージ コレクションの対象になるため、完了操作は必要はありません。

次に、図 4 に示すように TransactionalBehavior 属性を定義しました。TransactionalBehavior は、これらの構成を実行するサービス動作属性です。最初に、SaveStateInOperationTransaction を true に設定して、DurableService 属性をサービス記述に挿入します。2 番目に、AutoCompleteInstance プロパティの値に応じて、永続性のある動作のための TransactionalMemoryProviderFactory または TransactionalInstanceProviderFactory の使用を追加します。AutoCompleteInstance が true (既定値) に設定されている場合は、TransactionalInstanceProviderFactory が使用されます。最後に、TransactionRequiredAllOperations プロパティが true (既定値) に設定されている場合、TransactionalBehavior はすべてのサービス操作の動作で TransactionScopeRequired を true に設定します。したがって、すべての操作にアンビエント トランザクションが提供されます。明示的に false に設定した場合、サービス開発者はどの操作をトランザクションにするかを選択できます。

図 4 TransactionalBehavior 属性

[AttributeUsage(AttributeTargets.Class)]
public class TransactionalBehaviorAttribute : Attribute,IServiceBehavior
{
   public bool TransactionRequiredAllOperations
   {get;set;}

   public bool AutoCompleteInstance
   {get;set;}

   public TransactionalBehaviorAttribute()
   {
      TransactionRequiredAllOperations = true;
      AutoCompleteInstance = true;   
   }
   void IServiceBehavior.Validate(ServiceDescription description,
                                  ServiceHostBase host) 
   {
      DurableServiceAttribute durable = new DurableServiceAttribute();
      durable.SaveStateInOperationTransaction = true;
      description.Behaviors.Add(durable);

      PersistenceProviderFactory factory;
      if(AutoCompleteInstance)
      {
         factory = new TransactionalInstanceProviderFactory();
      }
      else
      {
         factory = new TransactionalMemoryProviderFactory();
      }

      PersistenceProviderBehavior persistenceBehavior = 
                                new PersistenceProviderBehavior(factory);
      description.Behaviors.Add(persistenceBehavior);

      if(TransactionRequiredAllOperations)
      {
         foreach(ServiceEndpoint endpoint in description.Endpoints)
         {
            foreach(OperationDescription operation in endpoint.Contract.Operations)
            {
               OperationBehaviorAttribute operationBehavior =  
                  operation.Behaviors.Find<OperationBehaviorAttribute>();
               operationBehavior.TransactionScopeRequired = true;
            }
         }
      }
   }
   ...
} 

TransactionalBehavior 属性を既定値で使用する場合、クライアントはインスタンス ID の管理や操作を行う必要がありません。クライアントで実行する必要のある作業は、図 5 に示すように、コンテキスト バインディングのいずれかでプロキシを使用して、バインディングでインスタンス ID を管理できるようにすることです。サービスが通常の整数をメンバ変数として操作していることに注意してください。興味深い点は、当然のことながら、永続的な動作なのでインスタンスは呼び出しごとのサービス同様にメソッド境界で非アクティブ化されますが、それでもプログラミング モデルは一般的な .NET オブジェクトのモデルである、ということです。

図 5 TransactionalBehavior 属性の使用

[ServiceContract]
interface IMyCounter
{
   [OperationContract]
   [TransactionFlow(TransactionFlowOption.Allowed)]
   void Increment();
}

[Serializable]
[TransactionalBehavior]
class MyService : IMyCounter
{
   int m_Counter = 0;

   public void Increment()
   {
      m_Counter++;
      Trace.WriteLine(m_Counter);
   }
}
//Client side:
MyCounterClient proxy = new MyCounterClient();

using(TransactionScope scope = new TransactionScope())
{
   proxy.Increment();
   scope.Complete();
}    

//This transaction will abort since the scope is not completed 
using(TransactionScope scope = new TransactionScope())
{
   proxy.Increment();
} 

using(TransactionScope scope = new TransactionScope())
{
   proxy.Increment();
   scope.Complete();
}

proxy.Close();

//Traces:
1
2
2

IPC バインドにコンテキストを追加する

TransactionalBehavior は、コンテキスト プロトコルをサポートするバインドを必要とします。WCF は、基本的な Web サービス (WS) および TCP バインドに対するコンテキストのサポートを提供しますが、パイプとも呼ばれるプロセス間通信 (IPC) バインドはサポートしません。IPC バインドのサポートを提供すると、TransactionalBehavior を IPC で使用でき、緊密な呼び出しで IPC の恩恵を享受できるようになり、非常に有益でしょう。そのために、NetNamedPipeContextBinding クラスを定義しました。

public class NetNamedPipeContextBinding : NetNamedPipeBinding
{
   /* Same constructors as NetNamedPipeBinding */

   public ProtectionLevel ContextProtectionLevel
   {get;set;}
}

NetNamedPipeContextBinding はその基本クラスとまったく同じように使用されます。このバインドは、他の組み込みバインドと同じようにプログラムで使用できます。ただし、カスタム バインドを application.config ファイルで使用する場合は、カスタム バインドが定義されている場所を WCF に通知する必要があります。これはアプリケーションごとに行うことができますが、より簡単な方法としては、次に示すように machine.config でヘルパ クラスの NetNamedPipeContextBindingCollectionElement を参照して、対象コンピュータの各アプリケーションに適用することもできます。

<!--In machine.config-->
<bindingExtensions>
   ...
   <add name = "netNamedPipeContextBinding" 
        type = "ServiceModelEx.                NetNamedPipeContextBindingCollectionElement,
                ServiceModelEx"
   />
</bindingExtensions>

NetNamedPipeContextBinding をワークフロー アプリケーションで使用することもできます。

図 6 に、NetNamedPipeContextBinding およびそのサポートするクラスの実装の抜粋を示します (実装の全体については今月のコード サンプルを参照してください)。NetNamedPipeContextBinding のコンストラクタはいずれも実際の構築を NetNamedPipeBinding の基本コンストラクタに委任します。唯一実行される初期化は、コンテキストの保護レベルを既定値の ProtectionLevel.EncryptAndSign に設定することです。

図 6 NetNamedPipeContextBinding の実装

public class NetNamedPipeContextBinding : NetNamedPipeBinding
{
   internal const string SectionName = "netNamedPipeContextBinding";

   public ProtectionLevel ContextProtectionLevel
   {get;set;}

   public NetNamedPipeContextBinding()
   {
      ContextProtectionLevel = ProtectionLevel.EncryptAndSign;
   }
   public NetNamedPipeContextBinding(NetNamedPipeSecurityMode securityMode) : 
                                                      base(securityMode)
   {
      ContextProtectionLevel = ProtectionLevel.EncryptAndSign;
   }
   public NetNamedPipeContextBinding(string configurationName)
   {
      ContextProtectionLevel = ProtectionLevel.EncryptAndSign;
      ApplyConfiguration(configurationName);
   }

   public override BindingElementCollection CreateBindingElements()
   {
      BindingElement element = new ContextBindingElement(ContextProtectionLevel,
                            ContextExchangeMechanism.ContextSoapHeader);

      BindingElementCollection elements = base.CreateBindingElements();
      elements.Insert(0,element);

      return elements;
   }

   ... //code excerpted for space
}

各バインド クラスの中核となるのは CreateBindingElements メソッドです。NetNamedPipeContextBinding はそのバインド要素が含まれている基本バインド コレクションにアクセスし、ContextBindingElement を追加します。この要素をコレクションに挿入することにより、コンテキスト プロトコルのサポートが追加されます。

実装の残りの部分は、管理構成を有効にするブックキーピングのみです。ApplyConfiguration メソッドは、バインド セクション構成名を受け取るコンストラクタにより呼び出されます。ApplyConfiguration は、ConfigurationManager クラスを使用して .config ファイルの netNamedPipeContextBinding セクションを解析し、そこから NetNamedPipeContextBindingElement のインスタンスを解析します。ApplyConfiguration メソッドを呼び出すことにより、このバインド要素はバインド インスタンスを構成するために使用されます。

NetNamedPipeContextBindingElement のコンストラクタは、構成プロパティが含まれている基本クラスの Properties コレクションに、コンテキスト保護レベルに対する 1 つのプロパティを追加します。OnApplyConfiguration (ApplyConfiguration を呼び出す NetNamedPipeContextBinding.ApplyConfiguration の結果として呼び出される) では、メソッドにより最初に基本要素が構成され、次に構成レベルに応じてコンテキスト保護レベルが設定されます。

NetNamedPipeContextBindingCollectionElement 型を使用して、NetNamedPipeContextBinding が NetNamedPipeContextBindingElement にバインドされます。このようにして、NetNamedPipeContextBindingCollectionElement がバインド拡張として追加されると、構成マネージャはどの型をインスタンス化してバインド パラメータに提供するかを認識します。

InProcFactory とトランザクション

TransactionalBehavior 属性により、使い慣れた .NET のプログラミング モデルを活かして、アプリケーションのほとんどすべてのクラスをトランザクションとして扱うことができます。この手法の弱点としては、WCF が非常に詳細なレベルで使用するように設計されていないため、複数のホストを作成し、開いて、閉じる操作を行う必要があります。また、application.config ファイルに多数のサービス セクションやクライアント セクションが含まれるために管理が難しくなります。これらの問題に対処するために、私の著書『Programming WCF Services, 2nd Edition』では、WCF でサービス クラスをインスタンス化するための InProcFactory というクラスを定義しました。

public static class InProcFactory
{
   public static I CreateInstance<S,I>() where I : class
                                         where S : I;
   public static void CloseProxy<I>(I instance) where I : class;
   //More members
}

私は InProcFactory を使用する際に、ホストを明示的に管理したり、クライアントやサービスの .config ファイルを保持したりせずに、WCF をクラス レベルで利用します。TransactionalBehavior のプログラミング モデルに各クラス レベルからアクセスできるようにするには、InProcFactory クラスで、トランザクション フローを有効にして NetNamedPipeContextBinding を使用します。図 5 の定義を使用して、InProcFactory は図 7 のプログラミング モデルを有効化します。

図 7 TransactionalBehavior と InProcFactory を結合する

IMyCounter proxy = InProcFactory.CreateInstance<MyService,IMyCounter>();

using(TransactionScope scope = new TransactionScope())
{
   proxy.Increment();
   scope.Complete();
}    

//This transaction will abort since the scope is not completed 
using(TransactionScope scope = new TransactionScope())
{
   proxy.Increment();
} 
using(TransactionScope scope = new TransactionScope())
{
   proxy.Increment();
   scope.Complete();
}

InProcFactory.CloseProxy(proxy);

//Traces:
Counter = 1
Counter = 2
Counter = 2

図 7 のプログラミング モデルは、プレーンな C# クラスのものと同じです。所有権のオーバーヘッドはありませんが、このコードでトランザクションのメリットが得られます。これは、今後のモデルにつながる基本のステップと言えます。将来は、メモリ自体がトランザクションに対応し、各オブジェクトをトランザクション対応にすることができるでしょう。

図 8 は、簡潔にするためにいくつかのコードを削除した InProcFactory の実装を示しています。InProcFactory's static コンストラクタは、アプリケーション ドメインごとに 1 回呼び出され、GUID を使用してそれぞれに新しい一意のベース アドレスが割り当てられます。これにより、InProcFactory が同じコンピュータ上で、アプリケーション ドメインおよびプロセスをまたいで複数回使用されます。

図 8 InProcFactory クラス

public static class InProcFactory
{
   struct HostRecord
   {
      public HostRecord(ServiceHost host,string address)
      {
         Host = host;
         Address = new EndpointAddress(address);
      }
      public readonly ServiceHost Host;
      public readonly EndpointAddress Address;
   }
   static readonly Uri BaseAddress = new Uri("net.pipe://localhost/" + 
                                             Guid.NewGuid().ToString());
   static readonly Binding Binding;
   static Dictionary<Type,HostRecord> m_Hosts = new Dictionary<Type,HostRecord>();

   static InProcFactory()
   {
      NetNamedPipeBinding binding = new NetNamedPipeContextBinding();
      binding.TransactionFlow = true;
      Binding = binding;
      AppDomain.CurrentDomain.ProcessExit += delegate
                                             {
                         foreach(HostRecord hostRecord in m_Hosts.Values)
                                                {
                                                 hostRecord.Host.Close();
                                                }
                                             };
   }


public static I CreateInstance<S,I>() where I : class
                                         where S : I
   {
      HostRecord hostRecord = GetHostRecord<S,I>();
      return ChannelFactory<I>.CreateChannel(Binding,hostRecord.Address);
   }
   static HostRecord GetHostRecord<S,I>() where I : class
                                          where S : I
   {
      HostRecord hostRecord;
      if(m_Hosts.ContainsKey(typeof(S)))
      {
         hostRecord = m_Hosts[typeof(S)];
      }
      else
      {
         ServiceHost host = new ServiceHost(typeof(S),BaseAddress);
         string address = BaseAddress.ToString() + Guid.NewGuid().ToString();
         hostRecord = new HostRecord(host,address);
         m_Hosts.Add(typeof(S),hostRecord);
         host.AddServiceEndpoint(typeof(I),Binding,address);
         host.Open();
      }
      return hostRecord;
   }
   public static void CloseProxy<I>(I instance) where I : class
   {
      ICommunicationObject proxy = instance as ICommunicationObject;
      Debug.Assert(proxy != null);
      proxy.Close();
   }
}

InProcFactory は、サービス タイプを特定のホスト インスタンスにマップする辞書を内部的に管理します。CreateInstance が呼び出されて特定のタイプのインスタンスが作成されるときに、GetHostRecord というヘルパ メソッドを使用して辞書が検索されます。辞書にまだそのサービス タイプが含まれていない場合、このヘルプ メソッドはそのサービス タイプのホスト インスタンスを作成し、新しい GUID を一意のパイプ名として使用して、そのホストにエンドポイントを追加します。次に、CreateInstance によりエンドポイントのアドレスをホスト レコードから取得し、ChannelFactory<T> を使用してプロキシを作成します。

最初にクラスを使用したときに呼び出される静的なコンストラクタで、InProcFactory はプロセス終了イベントをサブスクライブし、プロセスがシャットダウンされたときにすべてのホストを閉じます。最後に、クライアントによってプロキシが閉じられるように、InProcFactory は CloseProxy メソッドを提供します。CloseProxy メソッドは、ICommunicationObject のプロキシを問い合わせて、それを閉じます。トランザクション メモリの利点を活用する方法については、インサイト リンクの「トランザクション メモリとは」を参照してください。

トランザクション メモリとは

トランザクション メモリについてお聞きになっているかもしれません。多くのユーザーによって要求される共有データを管理するこの新機能が、同時実行コードの作成で直面するすべての問題を解決するということを聞いたことがあるかもしれません。また、トランザクション メモリは自身に可能なこと以上を請け負い、リサーチ用の玩具にすぎないというふうに聞いているかもしれません。真実はこの 2 つの両極端の間にあります。

トランザクション メモリを使用すると、ロックを個別に管理しなくて済みます。代わりに、作業単位、つまりデータベースの世界で呼ばれるトランザクションという明確に定義された連続ブロックにプログラムを構造化できます。これで、基になる実行時システム、コンパイラ、ハードウェア、または組み合わせによって、必要な分離と整合性が保証されます。

一般に、基になるトランザクション メモリ システムは、比較的細かいオプティミスティック同時実行制御を提供します。リソースを常にロックする代わりに、トランザクション メモリ システムは競合がないことを前提としています。また、この前提が正しくないことが検出されると、トランザクションで行われた仮の変更をすべてロールバックします。実装に応じて、トランザクション メモリ システムは、競合なしで完了できるまでコード ブロックを再実行しようとします。ここでも、ユーザーの指定を必要としたりコードの生産性を犠牲にしたりせず、ユーザー自身がメカニズムを再実行せずに、システムが競合を検出、管理できます。オプティミスティックで細かい同時実行制御、競合管理が使用可能で、特定のロックを指定および管理する必要がなければ、同時実行性を利用するコンポーネントを使用しながらも、問題の解決に連続して取り組むことができるようになります。

トランザクション メモリは既存のロック メカニズムでは簡単に実行できない手法の構成を提供するものです。複数の操作または複数のオブジェクトを合わせて構成する場合、通常はこれらの操作やオブジェクトを合わせて 1 つのロックの下にラップすることで、一般にロックの粒度を大きくする必要があります。トランザクション メモリは、コードの代わりに細かいロックを自動的に管理し、同時にデッドロックの発生を防止します。これにより、スケーラビリティを犠牲にせず、デッドロックの発生しない構成が提供されます。

大規模で商業的なトランザクション メモリは現在実装されていません。ライブラリ、言語の拡張機能、コンパイラ ディレクティブを使用した、経験に基づくソフトウェア手法が学術分野や Web に発表されています。制限されたトランザクション メモリを提供できるハードウェアは、高度な同時実行性を持つ環境のいくつかのハイエンドに存在しますが、このハードウェアを利用するソフトウェアでは明示的な使用が隠されています。リサーチ コミュニティはトランザクション メモリに深い関心を持っており、これらのいくつかの研究の成果が次の 10 年間でより便利な製品として結実するのを期待することになります。

本文のコラムでは、アトミック性 (全体または無という実行特性)、およびトランザクション プログラミングがもたらす副次的な管理性、品質などの利点を提供する現在のトランザクション テクノロジで利用可能な、揮発性リソース マネージャの作成について説明しています。トランザクション メモリは、あらゆる任意のデータ型に対し、非常に軽量な実行時のソフトウェアまたはハードウェア プリミティブを使用して、同様の機能を提供します。また、固有のリソース マネージャを作成する必要なしに、アトミック性に加えてスケーラビリティ、分離、および構成を提供することに焦点を当てます。トランザクション メモリが広く利用されるようになれば、プログラマはより単純なプログラミング モデルなどの揮発性リソース マネージャの恩恵だけでなく、トランザクション メモリ マネージャによってもたらされるスケーラビリティの恩恵も享受できるようになります。

— Dana Groff (マイクロソフトの並列コンピューティング プラットフォーム チームのシニア プログラム マネージャ)

ご意見やご質問は mmnet30@microsoft.com まで英語でお送りください。

Juval Lowy は、WCF のトレーニングとアーキテクチャ コンサルティングを行う IDesign 社に所属するソフトウェア アーキテクトです。最近の著作としては、『Programming WCF Services, 2nd Edition』 (O'Reilly、2008 年) があります。また、シリコン バレー地域の Microsoft Regional Director も務めています。連絡先は www.idesign.net です。