Системы Foundation

Простое применение транзакций к службам.

Джувел Лоуи (Juval Lowy)

Загружаемый файл с кодом доступен в коллекции кода MSDN
Обзор кода в интерактивном режиме

Cодержание

Управление состоянием и транзакции
Транзакционные службы для отдельных вызовов
Управление экземплярами и транзакции
Службы на основе сеансов и VRM
Транзакционные службы длительного пользования
Транзакционное поведение
Добавление контекста к привязке IPC
InProcFactory и транзакции

Фундаментальной проблемой в программировании является восстановление после ошибок. После ошибки приложение должно восстановить себя к состоянию, в котором оно находилось до ее возникновения. Представьте себе приложение, пытающееся выполнить операцию, состоящую из нескольких меньших, потенциально параллельных операций, каждая из которых может завершиться успешно или нет, независимо от других. Ошибка в любой из меньших операций означает, что система находится в несогласованном состоянии.

Возьмем, для примера, банковское приложение, передающее средства между двумя учетными записями, записывая их в кредит одной записи и в дебет другой. Успешная передача денег одной учетной записи и неудача снятия их с другой – несогласованное состояние, поскольку средства не могут находиться в двух местах одновременно, в то время как успешное снятие денег без их передачи ведет в к равно несогласованному состоянию, в котором деньги пропадают. Восстановление после ошибки с возвращением системы к первоначальному состоянию всегда является задачей приложения.

По ряду причин это гораздо проще сказать, чем сделать. Во-первых, для крупной операции само число вариантов частичного успеха и частичного сбоя может быстро выйти из под контроля. Это ведет к ненадежному коду, очень дорогому в разработке и поддержке и зачастую к коду, не работающему на практике, поскольку разработчики часто имеют дело лишь со случаями легкого восстановления, которые им известны и с которыми они знают что делать. Во-вторых, составная операция может быть частью гораздо большей операции и даже в случае безупречного выполнения кода все равно может потребоваться ее отменить, если что-то, неподконтрольное ее создателю, столкнется с ошибкой. Это подразумевает тесную связь между участвующими сторонами по вопросу координации операций и управления ими. Наконец, также необходимо изолировать свою работу от тех, кому еще требуется взаимодействовать с системой, поскольку в случае восстановление после ошибки путем отката некоторых действий, кто-то еще может неявно быть помещенным в состояние ошибки.

Как можно заметить, писать надежный код восстановления после ошибок вручную практически невозможно. Это не новость. С тех пор как программное обеспечение начало использоваться в бизнесе (в 60-х годах), было ясно, что должен существовать лучший способ управления восстановлением. Лучший способ существует: транзакции. Транзакция – это набор операций, в котором сбой любой отдельной операции ведет к сбою всего набора как одной единой операции. При использовании транзакций писать логику восстановления нет нужды, поскольку восстанавливать нечего –. либо потому, что все операции завершились успешно, либо потому, что все они потерпели сбой и не изменили при этом состояния системы.

При использовании транзакций важно использовать диспетчеры ресурсов транзакций. Диспетчер ресурсов может откатить все изменения, внесенные в ходе транзакции, если транзакция отменяется, и сохранить изменения, если транзакция фиксируется. Диспетчер ресурсов также предоставляет изоляцию; то есть пока транзакция находится в процессе, диспетчер ресурсов предоставляет доступ к ней всех прочих сторон (помимо транзакции) и просмотр изменений, которые все еще могут быть отменены. Это также значит, что транзакция никогда не должна получать доступ к диспетчерам, помимо диспетчеров ресурсов, поскольку внесенные в них изменения не будут устранены в случае отмены транзакции, делая необходимым восстановление.

Диспетчеры ресурсов традиционно были устойчивыми ресурсами, такими как базы данных и очереди сообщений. Но в статье из выпуска журнала MSDN Magazine за май 2005 года, озаглавленной «Не удается зафиксировать? Временные диспетчеры ресурсов .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>. Поскольку область не завершена, транзакция изменяется и значения номера и города возвращаются к состоянию до транзакции.

Рис. 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 это выполняется автоматически, через свойство TransactionAutoComplete атрибута OperationBehavior. Когда для этого свойства установлено значение 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 можно найти в выпуске моей рубрики «Системы Foundation» за май 2007 года «Распространение транзакций WCF».

Транзакционные службы для отдельных вызовов

В случае службы для отдельного вызова экземпляр уничтожается после возвращения вызова. В силу этого диспетчер ресурсов, используемый для сохранения состояния между вызовами, должен находиться вне области экземпляра. Клиент и служба должны также согласиться, какие операции отвечают за создание или удаление экземпляра из диспетчера ресурсов.

Поскольку доступ к тому же диспетчеру ресурсов могут получать много экземпляров одного типа службы, каждая операция должна содержать какой-либо параметр, позволяющий экземпляру службы найти свое состояние в диспетчере ресурсов и провести привязку к нему. Я называю этот параметр идентификатором экземпляра. Клиент также должен вызывать специальную операцию для удаления состояния экземпляра из хранилища. Учтите, что ограничения поведения для осведомленного о состоянии объекта транзакций и для объекта отдельного вызова идентичны: и тот и другой получают и сохраняют свое состояние на границах методов. В случае службы, обслуживающей отдельные вызовы, для сохранения состояния можно использовать любой диспетчер ресурсов. Можно использовать базу данных или 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 уничтожает экземпляр службы, гарантируя, что в памяти не останется ничего, что могло бы повредить согласованности.

Жизненный цикл транзакционной службы контролируется свойством ReleaseServiceInstanceOnTransactionComplete атрибута ServiceBehavior. Когда ReleaseServiceInstanceOnTransactionComplete установлен на true (как по умолчанию), он избавляется от экземпляра службы, после того, как метод завершает транзакцию, по сути превращая службу WCF в службу для отдельных вызовов, в той мере, в которой застрагивается модель программирования экземпляров.

Этот неуклюжий подход не является порождением WCF. Все распределенные программные модели транзакций на платформе Майкрософт, начиная с MTS, включая COM+ и Enterprise Services приравнивали объект транзакций к объекту отдельного вызова. Архитекторы этих технологий просто не доверяли способностям разработчиков адекватно управлять состоянием объекта при применении транзакций – программная модель этого довольно замысловата и не интуитивна. Основной недостаток этого состоит в том, что всем разработчикам, желающие воспользоваться транзакциями, пришлось принять нетривиальную модель для отдельных вызовов (см. рис. 2), тогда как большинство разработчиков чувствуют себя гораздо увереннее, используя знакомую программную модель с сохранением состояния, на основе сеансов, характерную для обычных объектов Microsoft .NET Framework.

Лично я всегда чувствовал, что приравнивание транзакций к созданию экземпляров для отдельных вызовов является необходимым злом, но концептуально это искажено. Режим создания экземпляров для отдельных вызовов следует применять только там, где требуется масштабируемость и, в идеале, транзакции должны быть отделены от управления экземплярами объекта и учета приложением масштабируемости.

Если от приложения требуется масштабируемость, выбор отдельных вызовов и использование транзакций могут очень хорошо работать вместе. Однако, если масштабируемость не нужна (что, вероятно, является обычным делом для большинства приложений), службам должно быть позволено быть основанными на сеансах, сохраняющими состояние и транзакционными. Остальная часть статьи представляет мое решение проблемы включения и сохранения программной модели на основе сеансов при использовании транзакций с общими службами.

Службы на основе сеансов и 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, тщательного определения членов, а также дисциплины, чтобы всегда настраивать все операции на необходимость транзакций и отключать выпуск экземпляра после завершения.

Транзакционные службы длительного пользования

В выпуске этой статьи за октябрь («Управление состоянием с помощью служб длительного пользования»), я представил поддержку, предлагаемую WCF для служб длительного пользования. Служба длительного пользования получает свое состояние из настроенного хранилища и сохраняет свое состояние обратно в это хранилище при каждой операции. Хранилище состояния может быть а может и не быть диспетчером транзакционных ресурсов.

Если служба является транзакционной, она, само собой, должна использовать только транзакционное хранилище и записывать его в транзакцию каждой операции. Таким образом, если транзакция будет прервана, хранилище состояния будет возвращено в состояние до транзакции. Однако WCF не знает, разработана ли служба для распространения ее транзакций на хранилище состояние, и по умолчанию она не запишет хранилище в транзакцию, даже хранилище является диспетчером ресурсов транзакций, таким как SQL Server 2005 или SQL Server 2008. Чтобы проинструктировать WCF распространять транзакции и записать лежащее в основе хранилище, установите свойство SaveStateInOperationTransaction атрибута DurableService на true:

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

SaveStateInOperationTransaction по умолчанию установлено на false, в силу чего хранилище состояния не будет участвовать в транзакции. Поскольку лишь транзакционная служба может воспользоваться установкой SaveStateInOperationTransaction на true, если этот параметр имеет значение true, то WCF будет наставить на том, чтобы либо TransactionScopeRequired было установлено на true для всех операций на службе, либо все они имели бы обязательный поток транзакций. Если операция настраивается с установкой TransactionScopeRequired на true, для записи хранилища в список будет использоваться внешняя операция.

Транзакционное поведение

В случае атрибутаDurableService, словно durable («длительного пользования») неверно, поскольку оно не обязательно указывает на соответствующее поведение. Оно лишь значит, что WCF автоматически десериализует состояние службы из настроенного хранилища и сериализует его обратно при каждой операции. Аналогично, поведение поставщика сохранения состояния не обязательно означает сохранение состояния, поскольку подойдет любой поставщик, производный от предписанного абстрактного класса поставщика.

Тот факт, что инфраструктура службы длительного пользования на деле является инфраструктурой сериализации, позволил мне обратить его в методику управления состоянием службы при транзакциях, в то же время полагаясь во внутреннем механизме на временный диспетчер ресурсов и не заставляя экземпляр службы что-то делать насчет этого. Это дополнительно упрощает транзакционную программную модель WCF и дает преимущества превосходящей программной модели транзакций простым объектам и обычным службам.

Первым шагом будет определение двух транзакционных, находящихся в памяти, фабрик поставщика, именуемых TransactionalMemoryProviderFactory и TransactionalInstanceProviderFactory. TransactionalMemoryProviderFactory использует статичный TransactionalDictionary<ID,T> для хранения экземпляров службы. Словарь используется совместно всеми клиентами и сеансами. Пока узел работает, TransactionalMemoryProviderFactory позволяет клиентам подключаться к службе и отключаться от нее. При использовании TransactionalMemoryProviderFactory следует назначить завершающую операцию, которая удаляет состояние экземпляра из хранилища, используя свойство CompletesInstance атрибута DurableOperation.

TransactionalInstanceProviderFactory, сопоставляет с каждым сеансом выделенный экземпляр Transactional<T>. В операции завершения нет нужды, поскольку состояние службы будет собрано как мусор после завершения сеанса.

Далее я определил атрибут TransactionalBehavior, показанный на рис. 4. TransactionalBehavior – это поведение атрибута службы, выполняющего эти настройки. Сначала оно внедряет в описание службы атрибут DurableService с SaveStateInOperationTransaction установленным на true. Затем оно добавляет использование либо TransactionalMemoryProviderFactory либо TransactionalInstanceProviderFactory для устойчивого поведения, в соответствии со значением свойства AutoCompleteInstance. Если 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 со значениями по умолчанию от клиента не требуется управления идентификатором экземпляра или какого-либо взаимодействия с ним. Для клиента достаточно использовать прокси через одну из привязок контекста и позволить привязке управлять идентификатором экземпляра, как показано на рис. 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 предоставляет поддержку контекста для базовых привязок, привязок веб-служб (WS) и привязок TCP, в этом списке нет привязок межпроцессного взаимодействия (IPC), также именуемых трубами. Наличие такой поддержки для привязок IPC было бы ценным, поскольку это сделало бы возможным использование TransactionalBehavior через IPC, давая преимущества IPC для близких вызовов. С этой целью я определил класс NetNamedPipeContextBinding:

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

   public ProtectionLevel ContextProtectionLevel
   {get;set;}
}

NetNamedPipeContextBinding используется точно так же, как его базовый класс. Эту привязку можно использовать программно, подобно любой другой встроенной привязке. Однако при использовании собственной привязки в файле CONFIG приложения, необходимо проинформировать WCF, где определена эта привязка. Хотя это можно делать и для отдельных приложений, более простым вариантом будет сослаться на вспомогательный класс NetNamedPipeContextBindingCollectionElement в machine.config, чтобы охватить все приложения на компьютере, как показано здесь:

<!--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 добавляет к его коллекции свойств конфигурации базового класса единственное свойство для уровня защиты контекста. В OnApplyConfiguration (вызываемом как результат отправки NetNamedPipeContextBinding.ApplyConfiguration вызова ApplyConfiguration), метод сперва настраивает свой базовый элемент и затем устанавливает свой уровень защиты контекста в соответствии с настроенным уровнем.

Тип NetNamedPipeContextBindingCollectionElement используется для привязки NetNamedPipeContextBinding с помощью NetNamedPipeContextBindingElement. Таким образом, при добавлении NetNamedPipeContextBindingCollectionElement как расширения привязки, диспетчер конфигурации знает, экземпляр какого типа создать и снабдить параметрами привязки.

InProcFactory и транзакции

Атрибут TransactionalBehavior позволяет считать почти каждый класс в приложении транзакционным, не нарушая программной модели, знакомой по .NET. Недостаток этого состоит в том, что WCF никогда не планировалась к использованию на уровне очень мелких деталей – необходимо будет создавать, открывать и закрывать много узлов и файл CONFIG приложения станет неуправляемым, с массами служебных и клиентских разделов. Чтобы решить эти проблемы, во второй редакции своей книги Programming WCF («Программирование WCF») я определил класс, именуемый 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), старший руководитель программы в рабочей группе платформы параллельных вычислений корпорации Майкрософт.

Вопросы и комментарии направляйте по адресу mmnet30@microsoft.com.

Джувел Лоуи (Juval Lowy) — архитектор программного обеспечения в компании IDesign, проводящий обучение по WCF и дающий консультации по архитектуре WCF. Одна из его последних книг — Programming WCF Services, 2nd Edition («Программирование служб WCF, издание второе», издательство O'Reilly, 2008 г.). Он также является региональным директором корпорации Майкрософт в Силиконовой долине. Связаться с Джувелом можно через веб-узел www.idesign.net.