Что такое контекст в c
Перейти к содержимому

Что такое контекст в c

  • автор:

What is a context?

It seems to me that a Context class is a control console whose object can invoke any included functions, such as Datacontext and DomainContext in WCF Ria service. Do I understand this concept correctly? If so, in what circumstances do I need to create a context class in my own class hierarchy?

Beside DataContext, what other well-known Context classes does the .net framework have?

Josh's user avatar

5 Answers 5

You can think about the context as a wrapper for related «things» such as HttpContext, DbContext, ObjectContext. i.e.: HttpContext contains any information you can reach for HTTP related operations.

DbContext contains the methods and properties for database communication. Likewise ObjectContext.

I would say it’s a placeholder or container of related things for something.

Josh's user avatar

To me, a context object defines a set of values and/or functions that are bound to the current execution path. In other words, just like speaking about a technical topic in the context of a job interview is different than speaking about the same topic at a nerd dinner, the context changes based on factors that affect the runtime environment of the consuming code. That seems abstract, but I can’t think of a better way to describe it at the moment!

Another famous context in .NET is the HttpContext object. Which values will change based on what Http operation is being handled. For example, the url will change in HttpContext.Current.Request.Uri . Hope that puts it in context for you 🙂

A context is commonly a storage mechanism for a group of actions. HttpContext , for example

Encapsulates all HTTP-specific information about an individual HTTP request.

For your WCF example, the «context» is the service. Different services have different contexts. Contexts can be as granular as you want. Some are broad, like the DomainContext , and some are granular, like HttpContext .

Contexts are everywhere, make them when you need to access or set like minded data or functions to things that can be decoupled.

All contexts are like this, they just encapsulate logic for particular action sets.

Here is another post describing the context design pattern.

a Context class is used in some OOP Design patterns, e.g: — State pattern — Strategy pattern

Razvan's user avatar

A context is an ordered sequence of properties that define an environment for the objects resident inside it. Contexts get created during the activation process for objects that are configured to require certain automatic services, such as synchronization, transactions, just-in-time activation, security, and so on. Multiple objects can live inside a context.

A new object’s context is generally chosen based on meta-data attributes on the class. Some important types of context are:

ExecutionContext:

This is the parent context, all the other contexts are a part of it. It is the system that .NET features like Task use to capture and propagate context, but has no behavior of its own.

SecurityContext:

This is where we find any security information that would normally be confined to the current thread. If your code needs to run as a particular user, you may be impersonating that user, or ASP.NET may be doing impersonation for you. In that case, the impersonation is stored in the SecurityContext

CallContext:

This allows the programmer to store custom data that should be available for the lifetime of a logical thread. Although considered bad practice in a lot of situations, it can avoid excessive numbers of method parameters as various context is passed around the program. LogicalCallContext is a related system that works across AppDomains.

Область видимости переменных, константы

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

Поле, также известное как переменная-член класса, находится в области видимости до тех пор, пока в этой области находится содержащий поле класс.

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

Локальная переменная, объявленная в операторах цикла for, while или подобных им, видима в пределах тела цикла.

Конфликты областей видимости локальных переменных

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

Рассмотрим следующий пример кода:

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

Вот другой пример:

Если вы попытаетесь скомпилировать это, то получите следующее сообщение об ошибке:

ScopeTest.cs (12,15) : error CS0136: A local variable named ‘3’ cannot be declared in this scope because it would give a different meaning to ‘j’, which is already used in a ‘parent or current’ scope to denote something else

Дело в том, что переменная j, которая определена перед началом цикла for, внутри цикла все еще находится в области видимости и не может из нее выйти до завершения метода Main(). Хотя вторая переменная j (недопустимая) объявлена в контексте цикла, этот контекст вложен в контекст метода Main(). Компилятор не может различить эти две переменных, поэтому не допустит объявления второй из них.

Конфликты областей видимости полей и локальных переменных

В некоторых случаях два идентификатора с одинаковыми именами (хотя и не совпадающими полностью уточненными именами) и одинаковой областью видимости можно различить, и тогда компилятор допускает объявление второй переменной. Причина в том, что C# делает принципиальное различие между переменными, объявленными на уровне типа (полями) и переменными, объявленными в методах (локальными). Рассмотрим следующий фрагмент кода:

Этот код компилируется, несмотря на то, что здесь в контексте метода Main() присутствуют две переменных с именем j: переменная j, определенная на уровне класса и существующая до тех пор, пока не будет уничтожен класс (когда завершится метод Main(), а вместе с ним и программа), и переменная j, определенная внутри Main(). В данном случае новая переменная с именем j, объявленная в методе Main(), скрывает переменную уровня класса с тем же именем. Поэтому когда вы запустите этот код, на дисплее будет отображено число 30.

Константы

Как следует из названия, константа — это переменная, значение которой не меняется за время ее существования. Предваряя переменную ключевым словом const при ее объявлении и инициализации, вы объявляете ее как константу:

Ниже перечислены основные характеристики констант:

Они должны инициализироваться при объявлении, и однажды присвоенные им значения никогда не могут быть изменены.

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

Константы всегда неявно статические. Однако вы не должны (и фактически не можете) включать модификатор static в объявление константы.

Использование констант в программах обеспечивает, по крайней мере, три преимущества:

Константы облегчают чтение программ, заменяя «магические» числа и строки читаемыми именами, назначение которых легко понять.

Константы облегчают модификацию программ. Например, предположим, что в программе C# имеется константа SalesTax (налог с продаж), которой присвоено значение 6 процентов. Если налог с продаж когда-нибудь изменится, вы можете модифицировать все вычисления налога, просто присвоив новое значение этой константе, и не понадобится просматривать код в поисках значений и изменять каждое из них, надеясь, что оно нигде не будет пропущено.

Константы позволяют избежать ошибок в программах. Если попытаться присвоить новое значение константе где-то в другом месте программы, а не там, где она объявлена, компилятор выдаст сообщение об ошибке.

Механизм контекстов в .NET

Вместе с появлением .NET нам была предложено масса полезных и интересных технологий и одна из них – контексты. Естественно, что контексты не являются чем-то принципиально новым. В том же COM они были доступны, начиная с Windows2000, но популярными они не были. Возможно, с появлением .NET контексты будут использоваться более активно. Почему «возможно», а не «наверняка»? Да потому, что большая часть документации пространства имен System.Runtime.Remoting.Contexts содержит только «This type supports the .NET Framework infrastructure and is not intended to be used directly from your code», то есть классы, входящие в это пространство имён, не документированы. Конечно, нельзя сказать, что за все время существования .NET ничего не изменилось. Например, MSDN Magazine уже напечатал две статьи в мартовских выпусках за 2002 и 2003 годы, и очень вероятно, что в марте 2004 выйдет еще одна статья, посвященная ведению лога вызовов на более высоком уровне.

Что такое контексты и зачем они нужны?

Вольный перевод определения контекста из MSDN говорит примерно следующее: «Контекст – это упорядоченный набор свойств, определяющих окружение для объектов, которые исполняются в нем.» Создается контекст на этапе активации объекта, для работы которого нужны те или иные службы (JIT-активация, поддержка транзакций, синхронизация, безопасность и многое другое). Естественно, что в рамках одного контекста может существовать несколько объектов.

Ядром инфраструктуры контекстов является класс Context (пространство имен System.Runtime.Remoting.Contexts). Рассмотрим его основные методы:

Context Constructor Конструктор класса. Обычно контексты создаются в процессе активации ContextBoundObject (CBO), но иногда может потребоваться явно создать новый контекст.
SetProperty Установить свойство контекста.
GetProperty Получить свойство контекста.
Freeze Заморозить контекст. «Заморозка» контекста подразумевает, что все нужные свойства контекста уже добавлены, и мы готовы начать его использование.
DoCallBack Сделать обратный вызов в контекст. Этот метод аналогичен методу AppDomain.DoCallBack с единственным отличием, что вызов будет обрабатываться в другом контексте.
RegisterDynamicProperty Регистрация динамического свойства.
UnregisterDynamicProperty Удалить динамическое свойство.

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

Как уже упоминалось, использование контекстов тесно связано с классом ContextBoundObject. Это единственный класс (не считая его наследников), для которого всегда создается контекст исполнения. Естественно, встает вопрос – как управлять созданием контекста и наполнением его свойствами?

Наверное, все знают о механизме атрибутов в .NET Framework и о том, как их можно использовать для расширения метаинформации о классе. Контексты в этом случае не исключение. Здесь также активно используются атрибуты для описания дополнительных возможностей. И основным атрибутом здесь выступает ContextAttribute. Обычно этот атрибут используется для управления созданием нового контекста и определения свойств создаваемого контекста. К атрибутам и свойствам мы вернемся чуть позже, а пока рассмотрим, как обеспечивается взаимодействие с CBO.

Работа с CBO в .NET Framework построена на использовании связки TransparentProxy и RealProxy. И здесь программист может столкнуться с уже упоминавшейся трудностью – отсутствием документации. Если RealProxy еще худо-бедно документирован, то с TransparentProxy дела обстоят хуже. В MSDN есть несколько незначительных упоминаний о том, что он существует, и что обычному программисту он не нужен. TP трудно назвать полноценным классом – это, скорее, некий мост между средой исполнения и управляемым кодом. Поэтому легко можно обойтись без документации, так как единственное, что необходимо помнить – это то, что в нем хранится ссылка на RealProxy и некий объект под названием StubData, который используется для принятия решения о том, какой из механизмов вызова методов CBO-объекта будет использоваться (рисунок 1).

Естественно, что это создает один из побочных эффектов – все (sic!) обращения к CBO идут через TransparentProxy (TP), что вносит накладные расходы.

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

Каждый раз, когда исполняемая среда создаёт объект, требующий управляемой активации, она проверяет наличие у этого объекта атрибутов контекста. У этих атрибутов запрашивается два специальных интерфейса – IContextAttribute и IСontextProperty (ContextBoundAttribute реализует оба этих интерфейса). Интерфейс IContextAtrtribute используется для принятия решения о создании нового контекста и наполнении его свойствами, а IContextProperty используется для описания свойств контекста. Активация любого ContextBound-объекта производится в несколько этапов (рисунок 2).

IContextAttribute.IsContextOK – каждый атрибут проверяет пригодность текущего контекста для активации. Если хоть один атрибут возвращает false, то принимается решение о создании нового контекста;

IContextAttribute.GetPropertiesForNewContext – если было принято решение о создании нового контекста, каждый атрибут получает возможность добавить в новый контекст свой список свойств;

IContextProperty.Freeze – каждое свойство уведомляется о том, что инициализация контекста завершена, и оно должно зафиксировать свое состояние;

IContextProperty.IsNewContextOK – после фиксации состояния контекста каждое свойство получает возможность проверить корректность созданного контекста и если, все в порядке, то дальше начинается процесс активации объекта.

Обычно интерфейсы IContextAttribute и IContextProperty напрямую не используются, а используется класс ContextAttribute реализующий оба этих интерфейса.

СОВЕТ

Естественно, что использование ContextBoundAttribute при объявлении класса не является обязательным требованием. Мы всегда можем воспользоваться классом Activator и указать нужные атрибуты в методе CreateInstance, или самостоятельно создать новый контекст и форсировать активацию CBO именно в нем.

Кроме интерфейса IContextProperty каждое свойство может реализовать и несколько дополнительных интерфейсов, каждый из которых так или иначе может влиять на процесс взаимодействия с ContextBoundObject:

IContributeClientContextSink Интерфейс IContributeClientContextSink дает возможность свойству принять участие в создании клиентского приемника (IMessageSink) контекста. Клиентский приемник сообщений создается в момент первого вызова из-за границы контекста и может использоваться для перехвата всех исходящих вызовов
IContributeServerContextSink Интерфейс IContributeServerContextSink дает возможность свойству принять участие в создании серверного приемника. Серверный приемник сообщений создается в момент первого входа в контекст и может использоваться для перехвата всех входящих в контекст вызовов.
IContributeEnvoySink Интерфейс IContributeEnvoySink дает возможность свойству принять участие в создании «дипломатического» приемника. «Дипломатический» приемник создается в серверном контексте, передается на сторону клиента (именно поэтому необходимо, чтобы класс, реализующий данный приемник, поддерживал сериализацию) и позволяет воздействовать на цепочку обработки сообщений еще до того, как произойдет переход из клиентского контекста в серверный. Одно из возможных назначений EnvoySink – это контроль входных параметров объекта, интеллектуальное кэширование вызовов и т.д. EnvoySink привязывается к конкретному объекту и служит для обработки сообщений конкретного объекта.
IContributeObjectSink Интерфейс IContributeObjectSink дает возможность свойству принять участие в создании приемника «объекта». Можно сказать, что этот приемник является серверным аналогом EnvoySink. Он так же, как и EnvoySink, привязывается к конкретному объекту и служит для обработки сообщений конкретного объекта.
IContextPropertyActivator Интерфейс IContextPropertyActivator дает возможность свойству влиять на процесс активации объекта.

На процесс взаимодействия с ContextBoundObject могут оказывать свое влияние и динамические свойства (IDynamicProperty). В отличие от обычных свойств, динамические свойства могут подключаться и отключаться в процессе существования контекста (методы Context.RegisterDynamicProperty и Context.UnregisterDynamicProperty):

IContributeDynamicSink Интерфейс IContributeDynamicSink дает возможность динамическому свойству создать IDynamicMessageSink. В целом возможности, предоставляемые посредством динамического приемника, схожи с теми, что можно получить через IContextPropertyActivator. Основное отличие между ними заключается в том, что IContextPropertyActivator действует только на этапе активации объекта и имеет более высокий приоритет.

Теперь рассмотрим использование свойств, входящих в состав контекста (для простоты будем рассматривать только синхронные вызовы).

Начнем мы это делать с серверного контекста. Почему с серверного? Конечно, можно было бы начать и с клиентского, но именно свойства серверного контекста обычно задействуются первыми. Отчего это происходит именно так – достаточно вспомнить с чего начинается исполнение большинства программ:

Что получается, когда управление попадает в Main? Естественно, создаётся контекст, используемый по умолчанию, у которого клиентские свойства не определены. Когда будет создан первый ContextBoundObject, то первыми начнут работать свойства именно его (т.е. серверного контекста).

Активизация CBO в серверном контексте

Будем считать, что для своего первого CBO мы не поскупились на всевозможные свойства и решили использовать все доступные нам возможности. Что при этом будет происходить, изображено на рисунке 4. Процесс активации объекта начинается в методе DoCrossContextActivation класса ActivationServices (к сожалению, данный класс объявлен как internal, и поэтому его использование сильно затруднено). Первым делом в этом методе создается и инициализируется новый контекст. После этого контекст замораживается (IContextProperty.Freeze), и каждое свойство получает возможность проверить состояние нового контекста (блок IContextProperty на рисунке 2). После проверки контекста инициируется процесс создания «серверного приемника». Для этого каждое свойство проверяется на предмет поддержки интерфейса IContributeServerContextSink и, если такая поддержка обнаруживается, то создается цепочка «приемников», которая завершается специальным приемником ServerContextTerminatorSink.

После формирования цепочки «серверных приемников» вызов переходит к первому приемнику в цепочке и, следуя по ней, достигает последнего – ServerContextTerminatorSink, который уведомляет свойства контекста, поддерживающие интерфейс IСontextPropertyActivator, и передает вызов активатору объекта — IConstructionCallMessage.Activator.Activate. Обычно (если никто ничего не поменял на предыдущем шаге) активатором выступает внутренний класс ConstructionLevelActivator, обеспечивающий конструирование объекта и создание его «дипломатических приемников». Так же, как и для «серверных приемников», это происходит через опрос свойств контекста, только в данном случае используется интерфейс IContributeEnvoySink.

Что является здесь наиболее важным? Во-первых, получая вызов IContributeServerContextSink.GetServerContextSink, мы можем быть уверены, что создаваемый объект является в этом контексте «первым». Во вторых, именно на этапе активации создается EnvoySink объекта и, хотя вызова SyncProcessMessage для EnvoySink в серверном контексте не будет, именно на этапе активации объект, реализующий EnvoySink, будет отправлен в клиентский контекст (вполне возможно, что его даже придется сериализовать).

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

Активация CBO в клиентском контексте

Активация в клиентском контексте (рисунок 5) начинается после того, как было принято решение о создании нового контекста. Это происходит непосредственно после того, как были опрошены все атрибуты, и один из них (IContextAttribute.IsContextOK) вернул false. Естественно, что еще до начала активации была создана «парочка» TransparentProxy и RealProxy, и именно благодаря действиям RealProxy запускается механизм создания нового контекста (вообще-то, RealProxy — это абстрактный класс, в реальности используется недокументированный класс RemotingProxy)

Так или иначе, будем считать, что активация CBO в клиентском контексте начинается с создания цепочки «клиентских приемников». Для этого используются свойства контекста с интерфейсом IContributeClientSink (стоит отметить, что IContributeClientSink используется только один раз, в тот момент, когда управление передается за границу клиентского контекста). После обработки сообщения в клиентском приемнике мы попадаем в ClientContextTerminatorSink, который «уведомляет» динамические свойства контекста и IContextPropertyActivator о вызове. Последним этапом обработки сообщения в клиентском контексте является специальный объект ContextLevelActivator. Именно на этом этапе осуществляется проверка свойств (рисунок 2) вновь созданного серверного контекста – IContextProperty.Freeze & IContextProperty.IsNewContextOK.

Но на этом работа со свойствами контекста еще не завершена. Часть свойств обрабатывается при первом вызове метода CBO.

Вызов метода CBO в клиентском контексте

Когда рассматривался процесс активизации объекта в серверном контексте, был упомянут специальный приемник – EnvoySink. Как уже говорилось – этот приемник был создан в процессе «серверной» активации и именно он получит шанс первым обработать сообщение на стороне клиента. Такое «поведение» делает “дипломатические” приемники очень удобным инструментом для контроля/модификации входных параметров. Наиболее полезным это может оказаться в распределенных приложениях. В этом случае неверные данные даже не будут переданы на сервер (нужно учитывать, что есть возможность обойти вызов EnvoySink, поэтому наиболее эффективным будет дублирование проверяющего кода как в EnvoySink, так и в ObjectSink).

Вызов метода CBO объекта в серверном контексте

Выйдя за пределы клиентского контекста, вызов проходит через уже созданную цепочку серверных приемников и попадает в ServerContextTerminatorSink (рисунок 7). Этот стандартный приемник инициирует создание цепочки «объектных приемников», которая будет завершена приемником ServerObjectTerminatorSink (как и во всех остальных случаях, приемники создаются с помощью свойств контекста, только в данном случае будет использоваться IContributeObjectSink).

Что было пропущено на этой диаграмме? Для упрощения можно считать, что вызов методов CBO-объекта осуществляется напрямую из ServerObjectTerminatorSink. В реальности же есть еще один дополнительный приемник – это StackBuilderSink, и именно он отвечает за доставку вызова к методам объекта. Также была пропущена одна интересная возможность – CBO сам может реализовать IMessageSink, и тогда реализация из ServerObjectTerminatorSink не будет использовать StackBuilderSink, а вызовет метод SyncProcessMessage у CBO-объекта.

СОВЕТ

Может возникнуть вопрос – если нет возможности вызвать StackBuilderSink (например, если CBO-объект реализовал IMessageSink), то как обратиться к объекту, имея на руках только IMessage? В этом случае все просто – метод RemotingServices.ExecuteMessage обеспечивает практически «прямой» вызов StackBuilderSink.

Естественно, что может возникнуть вопрос, для чего нужны TerminatorSink и с чем связано такое название. TerminatorSink – это специальный приемник, который завершает обработку сообщений на определенном уровне и служит неким гарантом того, что он на этом «уровне» всегда будет последним (именно поэтому его свойство IMessageSink.NextSink всегда возвращает null. Таким образом, его просто невозможно пропустить).

Что осталось еще?

Особенно радует, что поддержка контекстов в .NET не ограничивается абстрактными интерфейсами. Существуют и специальные атрибуты ContextStaticAttribute и SynchronizationAttribute, их достаточно подробное описание и назначение можно найти в MSDN.

Также заслуживает отдельного упоминания специальный атрибут ProxyAttribute. Использование этого атрибута позволяет перехватить момент создания класса специального класса RealProxy и при необходимости использовать вместо стандартной реализации собственную. Бесспорно, что это очень интересная возможность, единственная причина, по которой мы не будем ее рассматривать, заключается в том, что к контекстам она не имеет никакого отношения. Достаточно посмотреть, с чего начинается работа с контекстом (рисунок 5). Если мы создаем собственную реализацию RealProxy, то мы теряем практически всю инфраструктуру (очень сомнительно, что кто-то сможет написать собственную реализацию того, что было показано на рисунках 4 – 7).

Примеры использования контекстов

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

Первую задачу которую мы попробуем решить, используя контексты, – это задача «перехвата» создания MarshalByRefObject. Учитывая, что это всего лишь пример, упростим себе задачу и ограничимся простой подменой типа. Основой для теста будут классы Source и Derived

Перед решением этой задачи нужно будет ответить на вопрос – а только ли на наследников ContextBoundObject распространяется действие свойств контекста? Нет, не только. Действие контекста распространяется и на наследников класса MarshalByRefObject (естественно, что только для случая, когда класс зарегистрирован как remote-класс). Разумеется, не стоит надеяться, что будут доступны все возможности контекстов, но и тех, которые доступны, нам хватит.

С чего стоит начать? Для этого нужно посмотреть на рисунок 5. ClientContextSink получает управление в клиентском контексте первым. Единственное, свойства контекста по умолчанию мы изменить не можем. Ничего не поделаешь, придется создавать свой контекст.

Первым, что понадобится, – это реализация свойства контекста с интерфейсом IContributeClientContextSink, который создаст нам нужную реализацию ClientContextSink

Помимо атрибута, придется реализовать и ClientContextSink, специальный «приемник», который возьмет на себя подмену объектов. Именно он проделает всю основную работу по перехвату.

Не будем вдаваться в подробности того, как осуществляется подключение экземпляра объекта к Proxy. Вместо этого приведем диаграмму связей TransparentProxy, RealProxy и MBR объекта (рисунок 3).

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

Автоматические транзакции средствами контекстов

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

В .NET уже встроена поддержка автоматических транзакций, эту задачу решает класс System.EnterpriseServices.ServicedComponent. К сожалению, единственное, что мешает его полноценно использовать – тесная интеграция с COM, и, как следствие, необходимость регистрации компонентов в каталоге COM+,что, разумеется, сильно ограничивает возможности распространения подобных приложений. И что особенно огорчает – использование ServicedComponent – это практически единственный способ получить поддержку автоматических транзакций в .NET

ПРИМЕЧАНИЕ

Примечание: Поддержку автоматических транзакций можно также получить через ASP.NET или Web-сервис, но это не всегда удобно. Еще один вариант – начиная с .NET Framework 1.1 появилось несколько специальных классов, позволяющих использовать возможности COM+ 1.5, но по каким-то причинам эти классы доступны только под Windows Server 2003.

Итак, мы можем получить поддержку транзакций для своего объекта, воспользовавшись для этого сервисами, предоставляемыми COM+. Для этого можно использовать COM+ 1.5 и его интерфейсы (CoCreateActivity, IServiceCall, IServiceActivity и т.д.), но в этом случае мы вынуждены будем работать в WinXP или в Win2003, что само по себе является достаточно серьёзным ограничением. Можно также написать специальный библиотечный COM+-компонент, который «поделится» с нами своими сервисами. Правда, может показаться, что в этом случае мы от чего ушли, к тому и пришли, – устанавливать компонент в каталог COM+ все равно нужно. Но это не совсем так, ибо – установка такого компонента сродни установке .NET Framework её нужно будет произвести только единожды, в дальнейшем мы сможем ее использовать из любого приложения. Да и возможность работы в Win2000 будет тоже неплохим плюсом.

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

Первое, что нам нужно будет решить, — это на каком этапе будут добавляться службы контекста. В данном случае нужно предоставлять службы транзакций для конкретного экземпляра объекта. Ответив на этот вопрос можно нарисовать примерную схему (рисунок 8) того, как должны осуществляться вызовы методов нашего класса. А теперь, сама реализация:

Это, естественно, не весь код, некоторые незначительные классы и реализация COM+-компонента не приведены (весь код целиком можно посмотреть в прилагаемом архиве). Но основная идея проста – это перехват сообщений о вызове методов ContextBoundObject и обработка их уже в контексте COM+.

Заключение.

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

Что такое контекст в c

Контекст синхронизации (SynchronizationContext) — позволяет возобновить выполнение метода в конкретном потоке.
Текущий контекст SynchronizationContext – это свойство текущего потока. Идея в том, что всякий раз, как код исполняется в каком-то специальном потоке, мы
можем получить текущий контекст синхронизации и сохранить его.
Впоследствии этот контекст можно использовать для того, чтобы продолжить исполнение кода в том потоке, в котором оно было начато.

В классе SynchronizationContext есть важный метод Post , который гарантирует, что переданный делегат будет исполняться в правильном контексте.

Важно! В момент приостановки метода при встрече оператора await текущий контекст SynchronizationContext сохраняется.
Далее, когда метод возобновляется, компилятор вставляет вызов Post , чтобы исполнение возобновилось в запомненном контексте.

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

К числу таких длительных операций можно отнести:
• сетевые запросы;
• доступ к диску;
• продолжительные задержки.

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

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

Асинхронный код освобождает поток , из которого был запущен. И это очень хорошо по многим причинам.

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

Но самым важным мне кажется тот факт, что асинхронное выполнение открывает возможность для параллельных вычислений .

В версии C# 5.0 Microsoft добавила механизм, предстающий в виде двух новых ключевых слов: async и await .

Механизм async встроен в компилятор и без поддержки с его стороны не мог бы быть реализован в библиотеке. Компилятор преобразовывает исходный код, то есть действует примерно по тому же принципу, что в случае лямбда-выражений и итераторов в предыдущих версиях C#.

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

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

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

Библиотека Task Parallel Library была включена в версию .NET Framework 4.0. Важнейшим в ней является класс Task, представляющий выполняемую операцию. Его универсальный вариант, Task<T>, играет роль обещания вернуть значение (типа T), когда в будущем, по завершении операции, оно станет доступно.

Как мы увидим ниже, механизм async в C# 5.0 активно пользуется классом Task. Но и без async классом Task, а особенно его вариантом Task , можно воспользоваться при написании асинхронных программ. Для этого нужно запустить операцию, которая возвращает Task<T>, а затем вызвать метод ContinueWith для регистрации обратного вызова.
Это называется асинхронность вручную потому что используем метод ContinueWith .

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

1-ый шаг это пометка метода ключевым словом async .
Оно включается в сигнатуру метода точно так же, как, например, слово static.

2-ой шаг мы должны дождаться завершения скачивания, воспользовавшись ключевым словом await .
С точки зрения синтаксиса C#, await – это унарный оператор, такой же как оператор ! или оператор приведения типа (type).
Он располагается слева от выражения и означает, что нужно дождаться завершения асинхронного выполнения этого выражения.

На заметку!
Метод, помеченный ключевым словом async, автоматически не становится асинхронным .
Async-методы лишь упрощают использование других асинхронных методов. Они начинают исполняться синхронно, и так происходит до тех пор, пока не встретится вызов асинхронного метода внутри оператора await .
В этот момент сам вызывающий метод становится асинхронным.
Если же оператор await не встретится, то метод так и будет выполняться синхронно до своего завершения.

Я говорил, что класс Task представляет выполняемую операцию, а его подкласс Task<T> – операцию, которая в будущем вернет значение типа T. Можно считать, что Task<T> – это обещание вернуть значение типа T по завершении длительной операции.
Оба класса Task и Task<T> могут представлять асинхронные операции, и оба умеют вызывать ваш код по завершении операции. Чтобы воспользоваться этой возможностью вручную, необходимо вызвать метод ContinueWith , передав ему код, который должен быть выполнен, когда длительная операция завершится. Именно так и поступает оператор await , чтобы выполнить оставшуюся часть async-метода .

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

Метод, помеченный ключевым словом async , может возвращать значения трех типов:
• void
• Task
• Task , где T – некоторый тип.

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

Я провожу различие между типом возвращаемого методом значения (например, Task<string> ) и типом результата, который нужен вызывающей программе (в данном случае string).
В обычных, не асинхронных, методах тип возвращаемого значения совпадает с типом результата, тогда как в асинхронных методах они различны – и это очень существенно.

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

Async-методы , возвращающие тип Task , позволяют вызывающей программе ждать завершения асинхронной операции и распространяют исключения, имевшие место при ее выполнении.

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

Ключевое слово async указывается в объявлении метода, как public или static.
Однако спецификатор async не считается частью сигнатуры метода, когда речь заходит о переопределении виртуальных методов, реализации интерфейса или вызове.
То есть в отношении переопределения методов и реализации интерфейсов, слово async полностью игнорируется.

В объявлениях методов интерфейса слово async запрещено просто потому, что в этом нет необходимости. Если в интерфейсе объявлен метод, возвращающий тип Task , то в реализующем интерфейс классе этот метод может быть помечен словом async, а может быть и не помечен – на усмотрение программиста.

Когда поток исполнения программы доходит до оператора await, должны произойти две вещи:

1) Текущий поток должен быть освобожден, чтобы поведение программы было асинхронным. С привычной, синхронной, точки зрения это означает, что метод должен вернуть управление.

2) Когда задача Task , погруженная в оператор await, завершится, ваш метод должен продолжить выполнение с того места, где перед этим вернул управление, как будто этого возврата никогда не было.

Чтобы добиться такого поведения, метод должен приостановить выполнение, дойдя до await , и возобновить его впоследствии.

Чтобы стало яснее, сколько работы должен выполнить компилятор C#, встретив в программе оператор await , я перечислю, какие именно аспекты состояния метода необходимо сохранить.

Во-первых, запоминаются все локальные переменные метода, в том числе:
• параметры метода
• все переменные, определенные в текущей области видимости
• все прочие переменные, например счетчики циклов
• переменную this, если метод не статический

В результате после возобновления метода окажутся доступны все переменные-члены класса.
Всё это сохраняется в виде объекта в куче .NET, обслуживаемой сборщиком мусора. Таким образом, встретив await , компилятор выделяет память для объекта, то есть расходует ресурсы, но в большинстве случае это не приводит к потере производительности.
C# также запоминает место, где встретился оператор await .

контекст синхронизации, который среди прочего позволяет возобновить выполнение метода в конкретном потоке.
мое: Task помогает востанавливать контекст синхронизации, перед обратным вызовом

ExecutionContext — это родительский контекст, включающий все остальные контексты. Он не имеет собственного поведения, а служит только для запоминания и передачи контекста и используется такими компонентами .NET, как класс Task.

Оператор await можно использовать почти в любом месте метода, помеченного ключевым словом async .

Оператор await может встречаться внутри блока try , но не внутри блоков catch или finally .

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

Если использовать в этой точке await, то стек окажется другим, и определить в этой ситуации поведение повторного возбуждения исключения было бы очень сложно.
Напомню, что await всегда можно поставить не внутри блока catch, а после него, для чего следует либо воспользоваться предложением return, либо завести булевскую переменную, в которой запомнить, возбуждала ли исходная операция исключение. Например, вместо такого некорректного в C# кода:

можно было бы написать:

bool failed = false;

try
<
page = await webClient.DownloadStringTaskAsync( «http://aaa.com» );
>
catch (WebException)
<
failed = true;
>

if (failed)
<
page = await webClient.DownloadStringTaskAsync( «http://ooo.com» );
>

По завершении операции в объекте Task сохраняется информация о том, завершилась ли она успешно или с ошибкой. Получить к ней доступ проще всего с помощью свойства IsFaulted, которое равно true, если во время выполнения операции произошло исключение.
Оператор await знает об этом и повторно возбуждает исключение, хранящееся в Task.
У читателя, знакомого с системой исключений в .NET, может возникнуть вопрос, корректно ли сохраняется первоначальная трассировка стека исключения при его повторном возбуждении.

Раньше это было невозможно; каждое исключение могло быть возбуждено только один раз. Однако в .NET 4.5 это ограничение снято благодаря новому классу ExceptionDispatchInfo, который взаимодействует с классом Exception с целью запоминания трассировки стека и воспроизведения ее при повторном возбуждении.

Async-методы также знают об исключениях. Любое исключение, возбужденное, но не перехваченное в async-методе, помещается в объект Task, возвращаемый вызывающей программе. Если в этот момент вызывающая программа уже ждет объекта Task, то исключение будет возбуждено в точке ожидания. Таким образом, исключение передается вызывающей программе вместе со сформированной виртуальной трассировкой стека – точно так же, как в синхронном коде.

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

Асинхронные методы до поры исполняются синхронно Выше я уже отмечал, что async-метод становится асинхронным , только встретив вызов асинхронного метода внутри оператора await . До этого момента он работает в том потоке, в котором вызван, как обычный синхронный метод.

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

Класс Task позволяет это сделать, а для обновления пользовательского интерфейса по завершении вычисления мы можем, как обычно, использовать await:

Метод Task.Run исполняет переданный ему делегат в потоке, взятом из пула ThreadPool. В данном случае я воспользовался лямбда-выражением, чтобы упростить передачу счетной задаче локальных переменных. Возвращенная задача Task запускается немедленно, и мы можем дождаться ее завершения, как любой другой задачи:

Это очень простой способ выполнить некоторую работу в фоновом потоке.
Если необходим более точный контроль над тем, какой поток производит вычисления или как он планируется, в классе Task имеется статическое свойство Factory типа TaskFactory. У него есть метод StartNew с различными перегруженными вариантами для управления вычислением:

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

Для решения этой задачи можно воспользоваться методом Task.
WhenAll , который принимает несколько объектов Task и порождает агрегированную задачу, которая завершается, когда завершены все исходные задачи. Вот простейший вариант метода WhenAll (имеется также перегруженный вариант для коллекции универсальных объектов Task<T>):

Task WhenAll (IEnumerable<Task> tasks)

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

Универсальный вариант WhenAll возвращает массив, содержащий
результаты отдельных поданных на вход задач Task. Это сделано скорее для удобства, чем по необходимости, потому что доступ к исходным объектам Task сохраняется, и ничто не мешает опросить их свойство Result , так как точно известно, что все задачи уже завершены.

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

Task<Task<T>> WhenAny (IEnumerable<Task<T>> tasks)

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

Метод WhenAny возвращает значение типа Task<Task<T>>. Это означает, что по завершении задачи вы получаете объект типа Task<T>.
Он представляет первую из завершившихся задач и поэтому гарантированно находится в состоянии «завершен». Но почему нам возвращают объект Task, а не просто значение типа T?

Чтобы мы знали, какая задача завершилась первой, и могли отменить все остальные и дождаться их завершения.

Task<Task<Image>> anyTask = Task.WhenAny(tasks);
Task<Image> winner = await anyTask;
Image image = await winner; // Этот оператор всегда завершается синхронно

AddAFavicon(image);
foreach (Task<Image> eachTask in tasks)
<
if (eachTask != winner)
<
await eachTask;
>
>

Нет ничего предосудительного в том, чтобы обновить пользовательский интерфейс, как только будет получен результат первой завершившейся задачи (winner), но после этого необходимо дождаться остальных задач, как сделано в коде выше. При удачном стечении обстоятельств все они завершатся успешно, и на выполнении программы это никак не отразится. Если же какая-то задача завершится с ошибкой, то вы сможете узнать причину и исправить ошибку.

Отмена асинхронных операций связывается с типом CancellationToken

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

При вызове метода ThrowIfCancellationRequested отмененного объекта CancellationToken возбуждается исключение типа OperationCanceledException.

Библиотека Task Parallel Library знает, что такое исключение представляет отмену, а не ошибку, и обрабатывает его соответственно. Например, в классе Task имеется свойство IsCanceled , которое автоматически принимает значение true, если при выполнении async-метода произошло исключение OperationCanceledException.

Удобной особенностью подхода к отмене, основанного на маркерах CancellationToken , является тот факт, что один и тот же маркер можно распространить на столько частей асинхронной операции, сколько необходимо, – достаточно просто передать его всем частям.
Неважно, работают они параллельно или последовательно, идет ли речь о медленном вычислении или удаленной операции, – один маркер отменяет всё.

До первого await не происходит ничего интересного.
Async не планирует выполнение метода в фоновом потоке. Единственный способ сделать это – воспользоваться методом Task.Run , который специально предназначен для этой цели, или чем-то подобным.

В приложении с пользовательским интерфейсом это означает, что код до первого await работает в потоке пользовательского интерфейса.
А в веб-приложении на базе ASP.NET – в рабочем потоке ASP.NET.

Часто бывает, что выражение в строке, содержащей первый await, содержит еще один async-метод .
Поскольку это выражение предшествует первому await, оно также выполняется в вызывающем потоке.
Таким образом, вызывающий поток продолжает «углубляться» в код приложения, пока не встретит метод, действительно возвращающий объект Task .

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

К счастью, в большинстве случаев он выполняется недолго, но важно помнить, что одно лишь наличие ключевого слова async не гарантирует отзывчивости интерфейса.

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

Поэтому нам не нужно точно знать, в каком потоке началось исполнение, достаточно иметь соответствующий объект SynchronizationContext .

В классе SynchronizationContext есть важный метод Post , который гарантирует, что переданный делегат будет исполняться в правильном контексте.

Мы знаем, что код, предшествующий первому await , исполняется в вызывающем потоке, но что происходит, когда исполнение вашего метода возобновляется после await ?

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

Для достижения такого эффекта используется класс SynchronizationContext .

Важно.
В момент приостановки метода при встрече оператора await текущий контекст SynchronizationContext сохраняется.
Далее, когда метод возобновляется, компилятор вставляет вызов Post , чтобы исполнение возобновилось в запомненном контексте.

В классе Task имеется свойство Result , обращение к которому блокирует вызывающий поток до завершения задачи.

Его можно использовать в тех же местах, что await , но при этом не требуется, чтобы метод был помечен ключевым словом async или возвращал объект Task .
И в этом случае один поток занимается – на этот раз вызывающий (то есть тот, что блокируется).

Большинство написанных вами async-методов возвращают значение типа Task или Task<T> . Цепочка таких методов, в которой каждый ожидает завершения следующего, представляет собой асинхронный аналог стека вызовов в синхронном коде.

Компилятор C# прилагает максимум усилий к тому, чтобы исключения, возбуждаемые в этих методах, вели себя так же, как в синхронном случае. В частности, блок try-catch , окружающий ожидаемый async-метод, перехватывает исключения, возникшие внутри этого метода

async Task Catcher()
<
try
<
await Thrower();
>
catch (AlexsException)
<
// Исключение будет обработано здесь
>
>

async Task Thrower()
<
await Task.Delay(100);
throw new AlexsException();
>

Для этого C# перехватывает все исключения, возникшие в вашем async-методе . Перехваченное исключение помещается в объект Task , который был возвращен вызывающей программе. Объект Task переходит в состояние Faulted. Если задача завершилась с ошибкой, то ожидающий ее метод не возобновится как обычно, а получит исключение, возбужденное в коде внутри await .

В async-методе оно возбуждается там, где находится оператор await , а не в точке фактического вызова метода. Это становится очевидным, если разделить вызов и await .

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *