Сообщений 9    Оценка 770        Оценить  
Система Orphus

Контексты .NET vs RealProxy

Автор: Тимофей Казаков
The RSDN Group

Источник: RSDN Magazine #5-2003
Опубликовано: 05.06.2004
Исправлено: 10.12.2016
Версия текста: 1.0
Примеры работы с контекстами
Создание объектов в “глобальном” контексте
Singleton средствами контекстов
Queued контекст
RealProxy
Реализация RealProxy на основе контекстов
ApartmentBoundObject
Managed Stub

Код к статье

В предыдущей статье о контекстах (см. «RSDN Magazine» № 3, 2003) был обойден стороной такой интересный класс, как RealProxy (ну и ProxyAttibute тоже упомянут только вскользь). И, как показывает практика, сделано это было совершенно напрасно. Будем считать, что сейчас пришло время дополнить существующую статью небольшими примерами, а заодно и рассказать о том, что такое RealProxy, зачем он нужен, как его можно использовать и почему в некоторых случаях его использование более предпочтительно, чем использование контекстов.

Примеры работы с контекстами

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

Создание объектов в “глобальном” контексте

В некоторых случаях может потребоваться механизм для создания некоторых объектов в “единственном” контексте. Казалось бы, достаточно написать простейший код (ниже) и задача будет решена. Но не тут-то было – как для obj1, так и для obj2 будут созданы отдельные контексты.

      //
      //  Пример неудачной реализации “глобального” контекста
      //
[Context("Global")]
publicclass CBOGlobal : ContextBoundObject
{
}

publicvoid CBOGlobal_Test()
{
  CBOGlobal obj1 = new CBOGlobal();
  CBOGlobal obj2 = new CBOGlobal();
}

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

Полную реализацию можно посмотреть в отдельном архиве, а здесь рассмотрим лишь “серверный” приемник, так как именно в нем сосредоточена вся нужная нам функциональность (реализация ContextAttribute с интерфейсом IContributeServerContextSink более чем тривиальна).

      //
      // Реализация ServerContextSink.
      // Использование этого приемника приведет к тому, что все создаваемые 
      // объекты попадут в один "глобальный" контекст.
      //
      class GlobalSink : IMessageSink
{
  static IMessageSink _nextSink;
  static Context _globalContext;

  public GlobalSink(IMessageSink nextSink)
  {
    //// Приемник, передаваемый в поле nextSink, – это своеобразный вход // для обработки сообщения пришедшего в контекст. В случае реализации // “глобального” контекста нам нужно запомнить самый первый приемник // и сохранить его в static-поле – все сообщения будут идти через него.//if (_nextSink == null)
    {
      _nextSink = nextSink;
    }
  }
  // // Обработка сообщения. // Этот приемник запоминает контекст, в котором он создан,// и при создании новых объектов автоматически создает их// в уже существующем.//public IMessage SyncProcessMessage(IMessage msg)
  {
    IConstructionCallMessage ccm = msg as IConstructionCallMessage;
    if (ccm == null)
    {
      //// Очевидно, что пришло сообщение IMethodCallMessage.// Эти сообщения приходят уже после создания объекта,// и в нашем случае они будет приходить уже в «правильном»// контексте.//return _nextSink.SyncProcessMessage(msg);
    }

    //// Если понадобится использовать подобную реализацию на практике, // то здесь нужно будет вставить код, который определяет какой // контекст надо использовать на основе аргументов конструктора.// if (_globalContext == null)
    {
      _globalContext = System.Threading.Thread.CurrentContext;
    }
    
    //// Перед созданием объекта необходимо проверить, в каком контексте // мы находимся. Если идентификаторы текущего контекста и сохраненного // “глобального” совпадают, то сообщение (IMessage) передается в приемник, // хранящийся в поле _nextSink. Если-же контекст не совпал, то мы сохраняем // параметры вызова в экземпляре класса ContextWorkItem и, используя метод // Context.DoCallBack, переходим в интересующий нас контекст.//if (_globalContext.ContextID !=  
      System.Threading.Thread.CurrentContext.ContextID)
    {
      ContextWorkItem workItem = new ContextWorkItem(this, msg);
      _globalContext.DoCallBack(new 
        CrossContextDelegate(workItem.ProcessMessage));
      return workItem.Result; 
    }
    //// Мы находимся в нужном контексте и самое время // завершить создание объекта.//return _nextSink.SyncProcessMessage(msg);
  }
  public IMessageSink NextSink
  { 
    get { return _nextSink; }
  }
  //// ContextWorkItem – этот класс нам нужен для использования в // методе Context.DoCallback.//class ContextWorkItem 
  {
    IMessageSink _sink;
    IMessage _source;
    IMessage _result;

    public ContextWorkItem(IMessageSink sink, IMessage source)
    {
      _sink = sink;
      _source = source;
    }
    publicvoid ProcessMessage()
    {
      _result = _sink.SyncProcessMessage(_source);
    }
    public IMessage Result
    {
      get { return _result; }
    }
  }
}

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

Почему был выбран серверный приемник? Как мы уже знаем, серверный приемник создается из расчета “один на контекст” и именно этот приемник будет первым обрабатывать сообщение IConstructionCallMessage. Исходя из этого и определяется реализация приемника:

При создании первого объекта мы запоминаем контекст, в котором это произошло, а также ссылку на серверный приемник, который обрабатывал сообщение о создании объекта.

ПРИМЕЧАНИЕ

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

При создании последующих экземпляров объектов будет происходить следующее. Все новые экземпляры будут создаваться в индивидуальном контексте. Однако по условию задачи нам нужно, чтобы это происходило в уже созданном “глобальном” контексте. Нет ничего проще! Используя метод Context.DoCallBack, осуществляем переход в наш глобальный контекст и завершаем инициализацию объекта уже в нем.

Singleton средствами контекстов

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

      //
      // Использование двух атрибутов - суровая необходимость,
      // в противном случае каждый объект будет создан в своем контексте
      // и ничего работать не будет.
      //
[Singleton, Global]
publicclass CBOSingleton : ContextBoundObject
{
  privatestring _property;
  publicstring Property
  {
    get { return _property; }
    set { _property = value; }
  }
}

[AttributeUsage(AttributeTargets.Class)]
publicclass SingletonAttribute : ContextAttribute, IContributeObjectSink
{
  public SingletonAttribute() : base ("SingletonContext")
  {
  }
  public IMessageSink GetObjectSink(MarshalByRefObject obj,
                                    IMessageSink nextSink)
  {
    // // “Объектный” приемник. Он запомнит первый переданный экземпляр // MarshalByRefObject obj и именно ему будет передевать все // приходящие вызовы// returnnew SingletonSink(obj, nextSink);
  }

  //// Реализация ObjectSink.// Использование этого приемника приведет к тому, что // все вызовы будут обрабатываться только одним объектом // (поле _singleton)//class SingletonSink : IMessageSink
  {
    IMessageSink _nextSink;
    static MarshalByRefObject _singleton;

    public SingletonSink(MarshalByRefObject singleton, IMessageSink nextSink)
    {
      _nextSink = nextSink;
      if (_singleton == null)
      {
        _singleton = singleton;
      }
    }
    ////  Делаем вызов RemotingServices.ExecuteMessage только для//  "первого объекта". Кстати, именно использование //  ExecuteMessage потребовало использования GlobalAttribute.//  Связано это с тем, что ExecuteMessage должен вызываться только //  в "родном" контексте объекта. В противном случае произойдет следующее: //  будет произведена проверка контекста (а так как текущий контекст и //  контекст объекта не совпадают, это приведет к //  повторному вызову RealProxy.Invoke).//public IMessage SyncProcessMessage(IMessage msg)
    {
      IConstructionCallMessage ccm = msg as IConstructionCallMessage;
      if (ccm == null)
      {
        ////  Пришло сообщение IMethodCallMessage. //  Для реализации Singleton-поведения мы обрабатываем его, используя//  объект, сохраненный в static-поле _singleton//return RemotingServices.ExecuteMessage(_singleton,
          (IMethodCallMessage) msg);
      }
      return _nextSink.SyncProcessMessage(msg);
    }
    public IMessageSink NextSink
    { 
      get { return _nextSink; }
    }
  }
}

//// Пример использования Singleton-класса//class TestConsole
{
  staticvoid Main(string[] args)
  {
    CBOSingleton obj1 = new CBOSingleton();
    CBOSingleton obj2 = new CBOSingleton();

    obj1.Property = "Singleton: OK";
    Console.WriteLine(obj2.Property);
  }
}

Можно сказать, что это первая ласточка того, с какими архитектурными проблемами можно столкнуться при использовании контекстов. Итак, что же неладно с этим примером?

Проблема Описание
Ссылки obj1 и obj2 не равны между собой Контексты хорошо подходят для перехвата вызова методов объекта, но что-либо изменить в процессе его создания достаточно сложно. И, как следствие, obj1 и obj2 указывают каждый на свою __TransparentProxy (__TP). Это порождает следующую проблему.
Конструктор вызывается для каждого экземпляра Известно, что у нас есть связь __TP(RealProxy(Object, и эта связь реализуется по принципу «один к одному». Эту проблему можно было бы решить реализацией “серверного” приемника, создав в нем ответное сообщение через EnterpriseServicesHelper.CreateConstructionReturnMessage. Нужно только «достать» из IMessage ссылку на связанный с ним экземпляр __TransparentProxy. А сделать этого нельзя («легальными» способами нельзя, а нелегальный пример можно посмотреть в примере про “перехват создания MarshalByRefObject”).
Прочие проблемы Средствами контекстов можно перехватить вызов только в том случае, если он пересекает границу контекста. Локальный вызов перехватить, к сожалению, нельзя, и поэтому нужно внимательно отслеживать ситуации, когда может быть осуществлен вызов метода в обход механизма контекстов.

Проблемы проблемами, но рассмотреть, как реализован этот пример, стоит. Как и в большинстве случаев использования контекста, основная реализация будет находится в “приемнике объекта”. Этот вид приемников отличается тем, что распространяет свое действие на конкретный экземпляр и, в отличие от серверных, это последний обработчик (последний, на который мы можем влиять) в цепочке вызовов. В самом простом случае будет достаточно запомнить первый созданный экземпляр в статическом поле и при последующей обработке вызовов направлять все вызовы в него. Также в этом примере нам пригодилась уже рассмотренная реализация “глобального” контекста, поскольку нужно, чтобы все создаваемые экземпляры класса CBOSingleton оказывались именно в одном контексте.

Queued контекст

Во всех ранее приведенных примерах создание контекста и инициализация его свойств осуществлялись в момент создания CBO-объекта (ContextBoundObject). Это достаточно удобно, когда главную роль играет объект, а контекст предоставляет вспомогательные сервисы. Однако нам никто не запрещает создавать контекст самостоятельно и использовать его по собственному усмотрению. В большинстве случаев процесс создания контекста можно представить в следующем виде (рисунок 1). Использовать вновь созданный контекст можно через механизм обратных вызовов (Context.DoCallBack)


Рисунок 1. Создание контекста “вручную”.

Теперь после небольшого вступления попробуем рассмотреть более полезный пример – использование технологии контекстов для создания контекста с поддержкой “отложенных” вызовов.

Необходимость отложенных вызовов возникает достаточно часто – например, у нас есть алгоритм, который выполняется достаточно долго, и есть задача сохранения промежуточных результатов в БД. Самый простой способ – открыть соединение, начать транзакцию, запустить вычисления, периодически сохранять результаты в базе данных и по окончании транзакции подтвердить внесенные изменения. Это было бы замечательно, если не одно «но» – ресурсы базы данных будут использоваться более чем неэффективно. И одно из решений – это использование специального контекста, который будет «накапливать» нужные вызовы, в результате чего, когда придет время, у нас будет возможность выполнить эти вызовы максимально быстро (возможно, и не один раз).

Реализация класса QueuedContext - это типичный пример контекста, который создается «явным» образом, хотя в остальном он мало чем отличается от рассмотренных ранее примеров. В данном случае у класса QueuedContext появилось три новых метода:

Естественно, что не обошлось и без “приемника” – был использован объектный приемник (ObjectSink), метод SyncProcessMessage которого отвечает за принятие решения о том, нужно обработать сообщение немедленно или добавить его в очередь контекста.

      //
      //  Реализация "Queued" контекста.
      //
      public
      class QueuedContext : Context
{
  privatebool _isPaused;
  private System.Collections.Queue _messageQueue;

  ////  Конструктор. //  Полностью формирует контекст.//public QueuedContext()
  {
    _messageQueue = new System.Collections.Queue();
    _isPaused = true;

    //// Мы добавляем свойство, реализующее интерфейс IContributeQueueSink.// В дальнейшем реализованный приемник будет либо сохранять все вызовы во// внутренней очереди, либо “прозрачно” пропускать их через себя.// this.SetProperty(new ContributeQueueSink());
    //// Контекст обязательно нужно “зафиксировать” – // пока это не сделано, использовать его по назначению нельзя.// this.Freeze();
  }

  ////  Помещение сообщения во внутреннюю очередь.//internalvoid AddMessage(QueuedMessage message) 
  {
    lock (_messageQueue.SyncRoot)
    {
      _messageQueue.Enqueue(message);
    }
  }
  
  ////  Создание нового экземпляра класса.//publicobject CreateInstance(Type serverType) 
  {
    ////  Реализация этого класса не приводится. //  Единственная его задача – вызов CreateInstanceImpl.//
    CreateInstanceWorkItem workItem = new CreateInstanceWorkItem(this, 
      serverType);
    
    ////  Вызов метода CreateInstance не приведет к переключению контекста. //  Здесь нам приходится делать это самостоятельно//
    DoCallBack(new CrossContextDelegate(workItem.CreateInstance));

    return workItem.Result;
  }

  ////  Приостанавливает вызов методов. //  Все сообщения будут направляться в очередь//publicvirtualvoid Pause()
  {
    _isPaused = true;
  }

  ////  "Запускает" контекст и обрабатывает накопившиеся сообщения//publicvirtualvoid Resume()
  {
    _isPaused = false;
    DoCallBack(new CrossContextDelegate(DispatchQueuedMessages));
  }

  ////  Обработка накопившихся сообщений и очистка очереди//privatevoid DispatchQueuedMessages()
  {
    lock (_messageQueue.SyncRoot)
    {
      while (_messageQueue.Count > 0)
      {
        QueuedMessage message = (QueuedMessage) _messageQueue.Dequeue();
        message.Dispatch();
      }
    }
  }  

  ////  Вспомогательный класс для создания объектов через Context.DoCallBack//internalclass CreateInstanceWorkItem
  {
    Type _serverType;
    object _result;

    public CreateInstanceWorkItem(QueuedContext context, Type serverType)
    {
      _serverType = serverType;
    }

    publicvoid CreateInstance()
    {
      if (!serverType.IsContextful)
      {
        thrownew InvalidCastException();
      }
      _result = _serverType.GetConstructor(Type.EmptyTypes).Invoke(null);
    }

    publicobject Result
    {
      get { return _result; }
    }
  }

  ////  Свойство контекста. //  Здесь еще надо добавить реализацию IContributeServerContextSink //  и соответствующего приемника.//  Это нужно для того, чтобы нельзя было создавать новые объекты //  в "приостановленном" контексте (во избежание возможных проблем)//internalclass ContributeQueueSink : IContextProperty, IContributeObjectSink
  {
    publicbool IsNewContextOK(Context newCtx)
    {
      returntrue;
    }

    public IMessageSink GetObjectSink(MarshalByRefObject obj, 
      IMessageSink nextSink)
    {
      returnnew QueuedObjectSink(
        (QueuedContext) System.Threading.Thread.CurrentContext, 
        obj, nextSink);
    }
  }

  ////  Сообщение. //  Инкапсулирует "приемник" и сообщение в сериализированной форме.//  internalclass QueuedMessage 
  {
    IMessageSink _sink;
    MemoryStream _stream;

    public QueuedMessage(IMessageSink sink, IMessage message)
    {
      _sink = sink;        
      _stream = new MemoryStream();

      BinaryFormatter formatter = new BinaryFormatter( 
        new RemotingSurrogateSelector(), 
        new StreamingContext(StreamingContextStates.Remoting));

      ////  Переводим сообщение в сериализированную форму//
      formatter.Serialize(_stream, message);
      _stream.Position = 0;
    }

    ////  Обработать сохраненное сообщение//publicvoid Dispatch()
    {
      ////  Восстанавливаем сообщение из сериализированной формы и обрабатываем его//
      BinaryFormatter formatter = new BinaryFormatter(
        new RemotingSurrogateSelector(), 
        new StreamingContext(StreamingContextStates.Remoting));
      IMessage message = (IMessage) formatter.Deserialize(_stream);
      _sink.SyncProcessMessage(message);
    }
  }

  ////  Реализация "объектного" приемника.//  Осуществляет проверку контекста и, по ее результатам, ставит //  сообщение в очередь или обрабатывет его.//internalclass QueuedObjectSink : IMessageSink
  {
    private QueuedContext _context;
    private IMessageSink _nextSink;
    private MarshalByRefObject _instance;

    public QueuedObjectSink(QueuedContext context, 
      MarshalByRefObject instance, IMessageSink nextSink)
    {
      _context = context;
      _nextSink = nextSink;
      _instance = instance;
    }

    public IMessage SyncProcessMessage(IMessage msg)
    {
      if (!_context.IsPaused)
      {
        ////  Контекст находится в “online” - вызываем //  RemotingServices.ExecuteMessage для немедленной обработки вызова//return RemotingServices.ExecuteMessage(_instance, 
                                               (IMethodCallMessage) msg);
      }
      else
      {
        ////  Сохраняем пришедшее сообщение во внутренней очереди//
        _context.AddMessage(new QueuedMessage(this, msg));
        returnnew ReturnMessage(null, (IMethodCallMessage) msg);
      }
    }
  }
}

////  Этот объект поможет проверить работу созданного контекста//publicclass CBOQueue : ContextBoundObject
{
  publicvoid Method1()
  {
    Console.WriteLine("Method1");
  }

  publicvoid Method2()
  {
    Console.WriteLine("Method2");
  }
}

////  Демонстрация использования//class TestConsole
{
  staticvoid Main(string[] args)
  {
    QueuedContext context = new QueuedContext();
    
    ////  Создаем экземпляр класса CBOQueue в Queued контексте//
    CBOQueue queue = (CBOQueue) context.CreateInstance(typeof(CBOQueue));
    Console.WriteLine("Context paused");
    context.Pause();

    queue.Method1();
    queue.Method2();

    Console.WriteLine("Context resumed");
    ////  Два предыдущих вызова Method1 и Method2 будут обработаны только сейчас//
    context.Resume();
  }
}

Приведенные выше примеры могут показаться надуманными, но они отражают одну основную проблему в использовании контекстов – не существует никаких средств воздействия на процесс создания экземпляра класса. Собственно, эта проблема уже проявилась в примере про Singleton и повторилась в примере с QueuedContext. В случае с Singleton все вызовы методов обрабатывал первый экземпляр, хотя от создания второго (третьего и т.д.) избавиться не удалось. Для QueuedContext, возможно, эта проблема не так существенна, но от вызова конструктора тоже никуда не деться. Что не говори, а особой радости это не приносит.

Но не все так плохо – еще остался в запасе такой класс, как RealProxy. Как ни удивительно, именно он может решить все эти проблемы.

RealProxy

Класс RealProxy (в реальности используется его наследник RemoteProxy) играет ключевую роль как в создании контекста, так и в дальнейшей работе с ним. Однако, RealProxy это абстрактный класс и для его использования необходимо реализовать метод RealProxy.Invoke. К этому мы вернемся чуть позже, а для начала посмотрим, какие возможности есть у этого класса:

Метод Описание
RealProxy (Type serverType) Инициализация RealProxy. Инициализированный RealProxy будет представлять “серверный” объект указанного типа.
RealProxy (Type serverType, IntPtr stub, Object stubData) Эта форма конструктора аналогична предыдущей за одним небольшим исключением. Здесь мы можем передать в инициализируемый объект два параметра - stub и stubData. В дальнейшем они будут использоваться для принятия решения о том, как выполнять метод объекта, локально или используя RealProxy.Invoke. Дальше мы рассмотрим использование stub и stubData более подробно.
Invoke (IMessage) Этот метод перекрывается потомком и обычно здесь реализуется обработка сообщений двух типов – IConstructionCallMessage (вызов конструктора) и IMethodCallMessage (вызов метода)
InitializeServerObject Инициализация “серверного” объекта, связанного с текущим экземпляром RealProxy. Для этого InitializeServerObject создает экземпляр “серверного” класса; через поле __identity (класс MarshalByRefObject) серверный объект ассоциируется с текущим экземпляром RealProxy; проверяется значение хранящееся в поле stub класса __TransparentProxy и если оно не менялось, то в поле stub сохраняется внутренний идентификатор контекста
AttachObject / DetachObject AttachObject устанавливает связь между произвольным MBR-объектом и текущим экземпляром RealProxy. В чем-то этот метод аналогичен InitializeServerObject, но есть небольшое исключение – значение stubData остается неизменным.
GetStubData / SetStubData Эта группа методов позволяет изменить/получить значение stubData. Используя эту информацию, можно принять решение о том, как производить вызов метода. Если использовать стандартную реализацию, то stubData – это внутренний идентификатор контекста.
GetTransparentProxy Получить ссылку на связанный объект __TransparentProxy. Полученный __TP совместно с IMessage используется для вызова методов “серверного” объекта.
GetUnwrappedServer Получить указатель на “серверный” объект. Обычно не «обернутые» (wrapped) указатели на серверные объекты используются достаточно редко, но иногда может возникнуть необходимость сделать вызов в обход механизмов RealProxy.

Это основные методы класса RealProxy – название остальных методов и их назначение можно посмотреть в MSDN.

Помимо класса RealProxy в состав CLR входит специальный атрибут ProxyAttibute, используя который, можно установить соответствие между серверным типом и прокси-классом, обслуживающим этот тип. Для этого у ProxyAttribute определен виртуальный метод CreateInstance. Основная задача этого метода – создать неинициализированный экземпляр “серверного” класса или вернуть TransparentProxy (в зависимости от требований текущего контекста).

1. Для создания неинициализированного экземпляра можно воспользоваться классом System.Runtime.Serialization.FormatterServices.

2. В настоящий момент атрибут ProxyAttribute можно использовать только для наследников ContextBoundObject.

Перед тем, как написать реализацию своего первого RealProxy, хорошо бы посмотреть на рисунок 2, на котором показано, как этот класс используется инфраструктурой.


Рисунок 2. Проверка контекста на этапе создания объекта.

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

      public
      class MyCustomProxy: RealProxy
{
  private MarshalByRefObject target;
  public MyCustomProxy(Type serverType, MarshalByRefObject mbr) 
          : base (serverType)
  {
    target = mbr;
  }

  publicoverride IMessage Invoke(IMessage msg)
  {
    if (msg is IConstructionCallMessage)
    {
      RealProxy rp = RemotingServices.GetRealProxy(target);
      rp.InitializeServerObject((IConstructionCallMessage) msg);

      return EnterpriseServicesHelper.CreateConstructionReturnMessage     
        ((IConstructionCallMessage) msg, 
        (MarshalByRefObject) GetTransparentProxy());
    }
    return RemotingServices.ExecuteMessage(target, 
      (IMethodCallMessage) msg);
  }
}

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

Какими проблемами страдает подобная реализация?

ПРИМЕЧАНИЕ

Указатель this в виде __TransparentProxy – это следствие отсутствия какого-либо маршалинга данных между контекстами. В противном случае затраты на вызов метода CBO-объекта оказались бы слишком высоки.

Ну, раз уж есть плохой пример реализации RealProxy, то самое время привести правильный пример.

Реализация RealProxy на основе контекстов

Перед тем как начать что-либо делать, давайте вернемся к рисунку 2. На нем показано, что существует некая функция CheckContext (название может быть изменено). Эта функция в качестве входного аргумента получает значение stubData, оценив которое, принимает решение о том, как осуществить вызов метода – через RealProxy.Invoke, или обратиться к объекту напрямую. По умолчанию – это та функция, которая проверяет текущий контекст. Именно поэтому самый простой способ написать собственную версию RealProxy – это воспользоваться существующей инфраструктурой контекстов.

Перед тем, как что-то делать, рассмотрим алгоритм работы создаваемого прокси. На этапе инициализации (рисунок 3) мы создаем собственный внутренний контекст и, используя RealProxy.InitilizeServerObject, создаем “серверный” объект (не стоит забывать, что в этом методе также устанавливается значение stubData)


Рисунок 3. RealProxy на этапе создания серверного объекта.

В процессе работы для всех случаев, когда произошло несовпадение контекста (рисунок 5), будет вызываться метод Invoke, который обеспечит переключение контекста и обеспечит корректный вызов объекта.


Рисунок 4. RealProxy в работе.

      //
      //  Реализация RealProxy на основе существующей инфраструктуры контекстов
      //
[CBOProxy]
publicclass CBOTest : ContextBoundObject
{
  public CBOTest()
  {
  }

  publicvoid TestMethod()
  {
    Console.WriteLine("From Context");
  }
}

publicclass CBOProxy : RealProxy
{
  ////  Для реализации proxy воспользуемся инфраструктурой контекстов//
  Context _innerContext;

  public CBOProxy(Type serverType)
    : base (serverType)
  {
    _innerContext = new Context();
    _innerContext.Freeze();
  }
  
  publicoverride IMessage Invoke(IMessage msg)
  {
    ObjectCall call = new ObjectCall(this, msg);

    Console.WriteLine("Begin: {0}", msg.Properties["__MethodName"]);
    IConstructionCallMessage ccm = msg as IConstructionCallMessage;
    if (ccm != null) 
    {
      _innerContext.DoCallBack(
                               new CrossContextDelegate(call.Initialize));
    }
    else 
    {
      _innerContext.DoCallBack(
                               new CrossContextDelegate(call.Execute));        
    }
    Console.WriteLine("End: {0}", msg.Properties["__MethodName"]);

    return call._returnMessage;
  }    
  
  ////  Вспомогательный объект для хранения параметров вызова.//class ObjectCall
  {
    internal CBOProxy _proxy;
    internal IMessage _callMessage;
    internal IMessage _returnMessage;

    public ObjectCall (CBOProxy proxy, IMessage callMessage)
    {
      _proxy = proxy;
      _callMessage = callMessage;
    }

    ////  Вызов конструктора и инициализация//publicvoid Initialize()
    {
      ////  Вызов InitializeServerObject создаст //  экземпляр "серверного" объекта//  и свяжет TransparentProxy с текущим контекстом//
      _returnMessage = _proxy.InitializeServerObject(
          (IConstructionCallMessage) _callMessage);
    }

    ////  Вызов методов. //  Стоит обратить внимание, что в момент вызова этого метода//  значение свойства Context.InternalContextID == __TP.stubData//  В противном случае вызов RemotingServices.ExecuteMessage //  приведет к зацикливанию.//publicvoid Execute()
    {
      _returnMessage = RemotingServices.ExecuteMessage(
          (MarshalByRefObject)_proxy.GetTransparentProxy(), 
          (IMethodCallMessage) _callMessage);
    }
  }
}

[AttributeUsage(AttributeTargets.Class)]
publicclass CBOProxyAttribute : ProxyAttribute
{
  publicoverride MarshalByRefObject CreateInstance(Type serverType)
  {
    ////  При необходимости делегируем все в стандартную реализацию//if((RemotingConfiguration.IsWellKnownClientType(serverType) != null) || 
       (RemotingConfiguration.IsRemotelyActivatedClientType(serverType)  !=null))
    {
      returnbase.CreateInstance (serverType);
    }
    else
    {
      ////  Создаем собственный Proxy//return (MarshalByRefObject)new CBOProxy(serverType).GetTransparentProxy();        
    }
  }
}

class TestConsole
{
  staticvoid Main(string[] args)
  {
    CBOTest cboTest = new CBOTest();
    cboTest.TestMethod();
  }
}

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

ApartmentBoundObject

В качестве наглядного примера рассмотрим пример ApartmentBound объекта. Все методы этого объекта будут вызываться только в его родном потоке. Для этого нам придется написать собственную реализацию функции (stub), которая будет проверять контекст вызова.

ПРИМЕЧАНИЕ

Практически все объекты в .NET сделаны в расчете на потоковую модель MTA (если проводить аналогии с COM). Это значит, что программист должен заботится сам о потокобезопасности создаваемых классов.

К сожалению, эффективно решать данную задачу средствами одного C# достаточно сложно, так как в этом случае нам придется взять на себя написание специального stub-метода, который должен сравнить текущий контекст с информацией, хранящейся в stubData, и принять решение о переключении контекста. Что нужно знать про метод stub? Обычно это очень компактный кусок кода (все-таки он будет предварять вызов каждого метода объекта). Параметры этому методу передаются через регистр EAX (указатель на объект stubData) и регистр ECX (указатель на __TransparentProxy).

СОВЕТ

Не стоит забывать, что в stubData хранится управляемый тип (object), поэтому первые четыре байта по адресу [EAX] - это тип объекта, а реальные данные можно найти, начиная с адреса [EAX + 4].

По результатам проверки контекста в EAX возвращается либо 0 (если текущий контекст является родным контекстом объекта), либо любое другое значение (если не является)

Приведенный ниже пример почти аналогичен реализации RealProxy c использованием контекстов. Вот только из-за некоторой избыточности managed-расширений языка C++ он может показаться чуть более сложным.

      //
      //  Stub для проверки контекста. 
      //  На входе: EAX == stubData
      //  На выходе EAX == 0 - ContextMatch
      //        EAX != 0 - ContextNoMatch
      //  Есть предчувствие, что будет работать только для x86 :)
      //
__declspec(naked) staticvoid ContextCheck()
{    
  __asm 
  {       
    push ebx
    mov  ebx, [eax + 4]
    or   ebx, ebx
    jz   _notmatch // Получилось так, что объект еще // до конца не инициализирован
    mov    eax, fs:[0x18]    // Оптимизированная версия 
    mov    eax, [eax + 0x24] // GetCurrentThreadID
    sub    eax, ebx

_notmatch:
    pop    ebx
    ret
  }
}

public __gc class ApartmentProxy : public RealProxy
{    
  //// Helper для передачи сообщения между потоками//
  __nogc class ApartmentMessage 
  {
    private:
      HANDLE m_hEvent;
    public:
      void * _proxy;
      void * _message;
      void * _result;

    public:
      ApartmentMessage(ApartmentProxy * proxy, IMessage * message)
      {
        m_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);          
        
        _proxy = static_cast<void *> 
                    (static_cast<IntPtr>(GCHandle::Alloc(proxy)));
        _message = static_cast<void *>
                    (static_cast<IntPtr>(GCHandle::Alloc(message)));
        _result = 0;
      }

      virtual ~ApartmentMessage()
      {
        GCHandle::op_Explicit(IntPtr(_proxy)).Free();
        GCHandle::op_Explicit(IntPtr(_message)).Free();
        GCHandle::op_Explicit(IntPtr(_result)).Free();
      }
      
      IMessage* Result()
      {
        return static_cast<IMessage*> 
                      (GCHandle::op_Explicit(IntPtr(_result)).Target);
      }

      ////  Методы синхронные - надо ждать//void Wait()
      {
        DWORD dwWaitResult;
        
        do {
          dwWaitResult = WaitForSingleObjectEx(m_hEvent, INFINITE, TRUE);
        }
        while (dwWaitResult != WAIT_OBJECT_0 || dwWaitResult == WAIT_ABANDONED);
      }

    
      ////  Обработчик APC вызовов//static VOID CALLBACK APCProc(ULONG_PTR dwParam)
      {
        ApartmentMessage* message = reinterpret_cast<ApartmentMessage*> 
                                      (dwParam);
        message->Dispatch();
      }
      
  private: 
      ////  Обработать сообщение//void Dispatch()
      {
        IMessage* pMessage = static_cast<IMessage*> 
                   (GCHandle::op_Explicit(IntPtr(_message)).Target);
        ApartmentProxy* pProxy = static_cast<ApartmentProxy*> 
                   (GCHandle::op_Explicit(IntPtr(_proxy)).Target);
        
        try 
        {
          _result = static_cast<IntPtr>(GCHandle::Alloc(
              RemotingServices::ExecuteMessage(
                static_cast<MarshalByRefObject*> 
                (pProxy->GetTransparentProxy()),
                static_cast<IMethodCallMessage*>(pMessage)))
              ).ToPointer();
        }
        __finally {
          //// Вызов обработан – теперь мы можем отпустить ожидающий нас поток.//
          SetEvent(m_hEvent);
        }
      }
    };
  private:
    HANDLE m_hThread;
    Object * m_lock;

  public: 
    ApartmentProxy(Type* serverType) 
      : RealProxy(serverType, 
        IntPtr(ContextCheck), __box(IntPtr(0)))
    {
      m_lock = new Object();
      HANDLE __pin * pThread = &m_hThread;
      DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), pThread, 0, FALSE, DUPLICATE_SAME_ACCESS);
    }

  private:
    ~ApartmentProxy()
    {
      CloseHandle(m_hThread);
    }
  
  public: 
    void AttachObject(MarshalByRefObject * mbr)
    {
      AttachServer(mbr);
    }

    IMessage* Invoke(IMessage* msg)
    {      
      try 
      {
        //// Блокируем Invoke от использования из других потоков.//
        System::Threading::Monitor::Enter(m_lock);
        IConstructionCallMessage* ccm = 
          dynamic_cast<IConstructionCallMessage*> (msg);
        if (ccm != 0)
        {
          ////  Завершаем инициализацию//
          RealProxy::SetStubData(this, __box(IntPtr((void*)GetCurrentThreadId())));
          IMethodReturnMessage* mrm = InitializeServerObject(ccm);

          return mrm;
        }  

        ApartmentMessage Message(this, msg);
        //// Используя стандартные возможности WinAPI по асинхронному вызову// процедур, посылаем в нужный поток полученное сообщение.//
        QueueUserAPC(ApartmentMessage::APCProc, m_hThread, (ULONG_PTR) &Message);
        Message.Wait();
        
        return Message.Result();
      }
      __finally
      {
        System::Threading::Monitor::Exit(m_lock);
      }
    }  
};

Очевидно, что использование WinAPI, Managed C++ и встроенного ассемблера – это не самое замечательное решение, и вероятно, что для кого-то этот способ окажется совершенно неприемлемым. Но выход есть. :)

Managed Stub

Данный раздел предназначен для тех, кто любит создавать себе лишние проблемы, не любит прибегать к низкоуровневым средствам и считает, что программы, написанные на C#, работают так же быстро, как и программы, написанные на C++.

Вероятно, после предыдущего примера кому-то могло показаться, что написание методов stub с использованием C# – это практически нереальная задача. Но, как уже сказано, не все потеряно.

Основная задача при реализации метода stub – это получение доступа к передаваемым параметрам. В нашем случае это регистры EAX и ECX.

ПРЕДУПРЕЖДЕНИЕ

Лучше считать, что использование регистра ECX в stub-методе является недокументированной возможностью. И как следствие – лучше ей не злоупотреблять

Если получить доступ к EAX из управляемого кода может оказаться проблематично, то в случае ECX – это проще простого.

ПРИМЕЧАНИЕ

Методы в .NET следуют соглашению о вызовах __fastcall. По этому соглашению первые два параметра передаются через регистры ECX и EDX. Если это метод класса, то в регистре ECX будет указатель на this

В этом примере используется одна из особенностей .NET Framework – можно вызвать метод класса, передав в качестве this указатель на произвольный объект (разумеется, что просто так это не получится, но есть способы, позволяющие добиться этого). В данном случае можно воспользоваться этой особенностью для получения доступа к закрытым полям класса TransparentProxy. Так, зная, расположение полей (Layout) в классе Transparent Proxy мы можем создать класс “двойник” и, используя его получить доступ к закрытым данным без использования Reflection. В нашем случае – подобная используется техника для получения доступа к полям класса __TransparentProxy.

      //
      // Не стоит особенно рассчитывать, что реализации ManagedStub, отличные 
      // от приведенной, будут работать (да и работа этого на 100% не гарантируется :) 
      //
      public
      class AdvancedProxy : RealProxy
{
    public AdvancedProxy(Type serverType) 
        : base (serverType, StubData.StubPointer(), StubData.defaultData)
    {

    }    

    ////  У данного класса порядок следования полей совпадает //  с порядком полей в классе __TransparentProxy.//class StubData
    {      
        internalstaticobject defaultData = newobject();
		
        object _realProxy;
        object _stubData;

        bool CheckContext()
        {
            ////  В этом методе this (передается в регистре ECX) это //  указатель на __TransparentProxy, а не на StubData //  как можно было бы подумать //return _stubData == StubData.defaultData;
        }

        ////  Получим указатель на stub метод//internalstatic IntPtr StubPointer()
        {
            MethodInfo checkContext = typeof(StubData).GetMethod(
                "CheckContext", 
                BindingFlags.NonPublic | BindingFlags.Instance);

            return checkContext.MethodHandle.GetFunctionPointer();
        }

        ////  Для подавления предупреждений компилятора//  В реальности мы даже не будем создавать экземпляры этого класса//
        StubData()
        {
            _realProxy = 0;
            _stubData = 0;
        }
    }    

    ////  В данной реализации RealProxy мы перехватываем только создание объекта, //  все остальные вызовы будут выполняться без перехвата.//publicoverride IMessage Invoke(IMessage msg)
    {   
        IConstructionCallMessage ccm = (IConstructionCallMessage) msg;
        RealProxy.SetStubData(this, newobject());

        return InitializeServerObject(ccm);
    }
}

Как легко заметить, использование класса RealProxy дает намного больше возможностей, чем базовая реализация механизма контекстов. Так, используя класс ApartmentBoundProxy, можно реализовать для Windows.Forms классов прозрачный механизм подписки на события, возникающие в других потоках. Пример реализации кода stub на С# не отличается богатой функциональностью, но его можно расширить – например, для реализации отложенной инициализации или создания пула объектов. Нельзя не упомянуть и самый известный пример использования RealProxy – реалилизацию в составе System.EnterpriseServices, где использование RealProxy позволило добиться “прозрачной” интеграции со службами COM+. Будем надеятся, что это не последний пример использования этого класса.


Эта статья опубликована в журнале RSDN Magazine #5-2003. Информацию о журнале можно найти здесь
    Сообщений 9    Оценка 770        Оценить