基础内容

轻松将事务应用于服务

Juval Lowy

代码下载位置:MSDN 代码库
在线浏览代码

目录

状态管理和事务
单调用事务性服务
实例管理和事务
基于会话的服务和 VRM
事务性持久服务
事务性行为
向 IPC 绑定添加上下文
InProcFactory 和事务

编程中的一个根本问题就是错误恢复。发生错误后,应用程序必须自行恢复到产生错误之前的状态。请考虑这样一个应用程序,它试图执行一项由若干个更小操作组成的操作,这些小操作可能并行发生,而且每个单独操作的成功或失败都与其他操作无关。任何一个更小操作出现错误,都意味着系统处于不一致的状态。

以银行业务应用程序为例,该应用程序通过将资金划入一个帐户并从另一帐户转出资金,来在两个帐户之间转移资金。从一个帐户转出资金成功但将其划入另一帐户失败就属于不一致的状态,因为资金无法同时存在于两个位置中;同样,转出资金失败但划入资金成功也会导致不一致状态,此时资金失踪。通常总是由应用程序通过将系统恢复为原始状态来从错误中恢复。

由于某些原因,做比说要难得多。首先,对于一项大的操作,如果大量排列部分成功、部分失败,则操作很快就会变得无法控制。这将导致代码容易毁坏,使开发和维护费用变得非常昂贵,且代码经常在实际中不起作用,其原因是开发人员通常只处理易于恢复的情形,在此类情况下,他们能够意识到问题所在且知道如何处理。其次,您的复合操作可能是更大操作的一部分,即使您的代码执行完美无缺,如果不属您控制范围的事物出错,您也必须撤消该操作。这意味着操作的管理和协作参与方之间需要紧密配合。最后,还需将您的工作与其他需要和系统交互的人员隔离开来,因为如果您以后通过回滚某些操作来从错误中恢复,会隐式将其他人员置于错误状态中。

如您所见,手动编写可靠的错误恢复代码实际上是不可能的,这一点人们早已认识到了。自二十世纪六十年代在业务环境中使用软件起,人们就非常清楚必须要有一个更好的方法来管理恢复。这个方法就是:事务。事务是一组操作,其任何一个操作的失败都会导致整组失败,就象一个原子操作一样。使用事务时,无需编写恢复逻辑,因为没有要恢复的内容。当所有操作均成功时,没有要恢复的内容;当所有操作均失败时,不会对系统状态产生任何影响,因此也没有要恢复的内容。

使用事务时,关键是要使用事务性资源管理器。该资源管理器能够在事务中止时回滚事务期间所发生的全部更改,并在提交事务时保持更改。资源管理器还提供隔离功能;也就是说,当事务正在进行中时,资源管理器会阻止所有方(包括事务在内)访问事务和看到更改(这些更改仍能够回滚)。这也意味着,事务永远不应访问非资源管理器,因为在事务中止时对此类管理器所做的任何更改都不会回滚,这就需要进行恢复。

传统意义上,资源管理器为持久资源,如数据库和消息队列。然而,在 2005 年 5 月刊的《MSDN 杂志》**中,我在一篇名为“无法提交?.NET 中的不稳定资源管理器将事务引入公共类型”的文章中介绍了实现常用不稳定资源管理器 (VRM)(名为 Transactional<T>)的技术:

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) 为例,系统状态包括资源管理器以及服务实例的内存状态。虽然资源管理器会在事务结果产生时自动管理其状态,但对于内存中对象或静态变量而言,情况却并非如此。

此状态管理问题的解决方案是开发一项能够感知状态的服务,主动管理其状态。在事务之间,服务应将其状态储存在资源管理器中。在每个事务开始时,服务应从资源检索其状态,并通过此操作使资源参与到事务中。在事务结束时,服务应将其状态保存回资源管理器中。此技术可提供完善的状态自动恢复功能。对实例状态所做的任何更改都会作为事务的一部分提交或回滚。

如果提交事务,则在服务下次获取其状态时,它将具有事务后状态。如果中止事务,则下次它将具有事务前状态。无论采用何种方法,服务都将具有一致的状态,可供新事务访问。

编写事务性服务时,还存在两个其他问题。第一个问题是服务如何能够知道事务开始和结束的时间,以便它可以获取和保存其状态。服务可能属于一个更大的事务,此类事务跨多个服务和机器。在两次调用之间的任一时刻,事务都有可能结束。谁将调用服务,让服务知道要保存其状态? 第二个问题必须利用隔离来解决。不同的客户端可能对不同的事务同时调用服务。服务如何将不同事务对其状态的更改相互隔离?如果另一个事务要访问其状态并根据它的值进行操作,则在原始事务中止且更改回滚时,此事务将处于不一致的状态。

这两个问题的解决方案是使方法边界与事务边界相等。在每个方法调用开始时,服务应从资源管理器读取其状态;在每个方法调用结束时,服务应将其状态保存到资源管理器中。此操作可确保:如事务在两次方法调用之间结束,可保持服务状态或将其回滚。此外,在每个方法调用中,在资源管理器内读取和存储状态可解决隔离难题,因为服务只让资源管理器在并行事务之间隔离对状态的访问。

由于服务使方法边界与事务边界相等,因此服务实例还必须在每个方法调用结束时对事务的结果进行表决。从服务角度而言,方法一旦返回,事务即会完成。在 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 开始,Microsoft 平台上分布的所有事务性编程模型均通过 COM+ 和 Enterprise Services 使事务性对象等同于单调用对象。这些技术的架构师只是不相信开发人员能够在面对事务(既复杂又不直观的编程模型)时正确地管理对象的状态。虽然大多数开发人员能够更方便地使用他们所熟悉的常规 Microsoft .NET Framework 对象的基于会话且有状态的编程模型,但主要缺点是所有想要从事务中受益的开发人员都必须采用有实际意义的单调用编程模型(请参见图 2)。

我个人始终认为使事务等同于单调用实例化是不得不解决的棘手问题,然而从概念上讲,它是被曲解了。在需要可伸缩性时,人们应该只选择单调用实例模式;理想情况下,事务应与对象实例管理和应用程序对可伸缩性的考量相隔离。

如果需要扩展您的应用程序,则选择单调用并使用事务的效果会很好。然而,如果您不需要可伸缩性(大多数应用程序可能均如此),则不应允许您的服务以会话为基础、有状态且是事务性的。本专栏的其余部分介绍了我的一项解决方案,它能启用并保留基于会话的编程模型,同时配合使用事务与常用服务。

基于会话的服务和 VRM

WCF 通过将 ReleaseServiceInstanceOnTransactionComplete 设置为 false,允许使用事务性服务维护会话语义。在此情况下,WCF 将置身事外,让服务开发人员在面对事务时负责管理服务实例的状态。单会话服务仍必须使方法边界与事务边界相等,因为每个方法调用可能在不同的事务中,而且事务可能会在同一会话中的两次方法调用之间结束。虽然您能够像使用单调用服务那样手动管理此状态(或使用不在本专栏范围内的某些其他高级 WCF 功能),但您也可以针对服务成员使用 VRM,如图 3 所示。

图 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 传播事务并利用基础存储,请将 DurableService 的 SaveStateInOperationTransaction 属性设置为 true:

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

SaveStateInOperationTransaction 默认为 false,因而状态存储不会参与事务。由于只有事务性服务能够从 SaveStateInOperationTransaction 设置为 true 中受益,因此如果它为 true,WCF 便会要求服务上的所有操作将 TransactionScopeRequired 设置为 true 或具有强制事务流。如果配置操作时将 TransactionScopeRequired 设置为 true,则将使用操作的环境事务来利用存储。

事务性行为

对于 DurableService 属性而言,单词“持久”不太恰当,因为它此时不一定指示持久行为。它所代表的含义是 WCF 将自动从已配置的存储中反序列化服务状态,然后在每次操作时再次将其序列化。同样,持久性提供程序行为不一定是指持久性,因为从预定抽象提供程序类衍生的任何提供程序都满足持久性。

实际上,持久服务基础结构在现实中是序列化基础结构,这使我可以在技术中利用这一结构,在出现事务时管理服务状态,同时在底层依赖不稳定资源管理器,而不必让服务实例对其做任何事情。这样进一步改进了 WCF 的事务性编程模型,高级事务编程模型在纯粹对象和常用服务方面的优势也得以体现。

第一步是定义两个事务性内存中提供程序工厂:TransactionalMemoryProviderFactory 和 TransactionalInstanceProviderFactory。TransactionalMemoryProviderFactory 使用静态 TransactionalDictionary<ID,T> 储存服务实例。目录在所有客户端和会话之间共享。只要主机在运行,TransactionalMemoryProviderFactory 就允许客户端连接服务或从服务断开连接。使用 TransactionalMemoryProviderFactory 时,应指定一个完整的操作,以使用 DurableOperation 的 CompletesInstance 属性从存储中移除实例状态。

另一方面,TransactionalInstanceProviderFactory 将每个会话与专用的 Transactional<T> 实例相匹配。不需要完成操作,因为在关闭会话后服务状态将做为垃圾被收集起来。

接下来,我定义了 TransactionalBehavior 属性,如图 4 所示。TransactionalBehavior 是执行下列配置的服务行为属性。首先,它在 SaveStateInOperationTransaction 设置为 true 时向服务说明中插入一个 DurableService 属性。其次,它根据 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 交互。对客户端而言,必须要做的就是通过一种上下文绑定来使用代理,并让绑定管理实例 ID,如图 5 所示。请注意,服务与作为其成员变量的正常整数进行交互。有趣的是,由于持久行为的缘故,取消实例激活当然仍与方法边界上的单调用服务类似,但编程模型是常用 .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 绑定非常重要,因为这样可以通过 IPC 来使用 TransactionalBehavior,从而使熟知的调用从 IPC 中受益。为此,我定义了 NetNamedPipeContextBinding 类:

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

   public ProtectionLevel ContextProtectionLevel
   {get;set;}
}

NetNamedPipeContextBinding 的使用方法与其基类完全相同。可以像任何其他内置绑定一样,以编程方式使用此绑定。然而,当在应用程序 .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 集合中添加上下文保护级别的单一属性。在 OnApplyConfiguration(它作为 NetNamedPipeContextBinding.ApplyConfiguration 调用 ApplyConfiguration 的结果而被调用)中,方法首先配置其基本元素,然后根据已配置的级别设置上下文保护级别。

NetNamedPipeContextBindingCollectionElement 类型用于绑定 NetNamedPipeContextBinding 与 NetNamedPipeContextBindingElement。采用此方式时,如果将 NetNamedPipeContextBindingCollectionElement 添加为绑定扩展,则配置管理器将知道要实例化哪个类型并为其提供绑定参数。

InProcFactory 和事务

TransactionalBehavior 属性允许您几乎可以将应用程序中的每个类都视为事务性类,而不会损害熟悉的 .NET 编程模型。弊端是 WCF 从未被设计为用于非常细化的层级——您将必须创建、打开并关闭多个主机,而且您的应用程序 .config 文件将变得不能使用服务和客户端分数进行管理。为解决这些问题,在我编写的《Programming WCF, 2nd Edition》一书中,我定义了名为 InProcFactory 的类,它让您可以通过 WCF 来实例化服务类:

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 时,您在类级别利用 WCF,而无需凭借显式管理主机或者具有客户端或服务 .config 文件。为使每个类级别均能访问 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 的静态构造函数均调用一次,使用 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 方法,该方法查询 ICommunicationObject 的代理并将其关闭。要了解如何能够利用事务性内存,请参阅“深入分析”侧栏“什么是事务性内存?”。

什么是事务性内存?

您可能已经听说过事务性内存,这是一种用于管理共享数据的新技术,许多人声称它可以解决在编写并发代码时遇到的所有问题。或者您听到的是,事务性内存言过其实,只不过是一个研究工具。真实情况是它属于两种极端之间的事物。

事务性内存使您可以不必管理单个锁。您可以用定义明确的序列块(在数据库中称为工作单元或事务)构建程序。然后,您可以让基础运行时系统、编译器、硬件或它们的组合提供所需的隔离性和一致性保证。

通常,基础事务性内存系统能够相当精细地提供最优的并发控制。事务性内存系统不必经常锁定资源,它假定不存在争用。它还检测这些假定何时不成立,然后回滚在事务期间所做的任何试探性更改。事务性内存系统然后可能会根据实现方式尝试重新执行代码块,直到它能够在无争用的情况下完成。系统仍可以检测争用并对其加以管理,不会要求您指定引起的回退或编写代码,您也不必重试机制。如果您有最优的精细并发控制、争用管理,且无需指定和管理特定的锁,可以考虑使用那些利用并发的组件,以序列方式解决您的问题。

事务性内存承诺提供复合功能,这是现有锁定机制无法轻松实现的。要将多个操作或多个对象复合在一起,一般需要增加锁的粒度——通常是将这些操作或对象共同封装在一个锁下。事务性内存代表您的代码自动管理细粒度锁定,同时又能避免死锁,保证提供复合功能时不会损害可伸缩性,也不会引入死锁。

目前,事务性内存尚未大规模实现商业化。使用库、语言扩展、或编译器指令的实验软件方法已在学术界发表或在网站上发布。某些高端、高并发环境中,确实有可以提供有限事务性内存的硬件,但利用此硬件的软件没有明确指出使用了这种硬件。事务性内存让研究社区倍感兴奋,您可期待此研究的一些成果在未来十年内能够使其成为更易于使用的产品。

附属专栏描述了不稳定资源管理器的创建过程,您可以将该管理器与当前事务技术配合使用以提供原子性(或全部执行,或都不执行)、后续可管理性、质量以及事务性编程所具有的其他优势。事务性内存提供相似的功能,但它对于任意数据类型使用相当轻量化的运行时软件或硬件基元,并侧重于提供可伸缩性、隔离性、复合以及原子性,无需您创建自己的资源管理器。事务性内存得以广泛使用,程序员将不仅能从不稳定资源管理器(如更简单的编程模型)中受益,还会获得事务性内存管理器所带来的可伸缩性收益。

— Dana Groff 是 Microsoft 并行计算平台构建团队的一名高级项目经理

请将您想询问的问题和提出的意见发送至 mmnet30@microsoft.com

Juval Lowy 是 IDesign 的一名软件架构师,该公司提供 WCF 培训和体系结构咨询。他最近编写了一本名为《Programming WCF Services, 2nd Edition》**(O'Reilly, 2008) 的新书。另外,他还是 Microsoft 硅谷地区的区域总监。您可以通过 www.idesign.net 与 Juval 联系。