Интернет-журнал "Домашняя лаборатория", 2007 №6 - Вязовский
Шрифт:
Интервал:
Еще раз про атрибут синхронизации
Эта глава продолжает изучение кода атрибута синхронизации из Rotor, рассмотрение которого было начато в предыдущей главе. Там мы рассмотрели основные механизмы, связанные с определением контекста и домена синхронизации, в которых будет размещен новый объект — экземпляр класса, которому приписан атрибут SynchronizationAttribute. Это конструкторы (четыре варианта) и методы IsContextOK и GetPropertiesForNewContext класса SynchronizationAttribute. Теперь мы сосредоточимся на самом алгоритме синхронизации и попутно рассмотрим несколько важных понятий, связанных с программированием в CLR.
Инициализация свойства синхронизации в домене синхронизации
Начнем с метода InitIfNecessary класса SynchronizationAttribute:
internal virtual void InitlfNecessary() {
lock(this) {
if (_asyncWorkEvent == null) {
_asyncWorkEvent = new AutoResetEvent(false);
_workltemQueue = new Queue();
_asyncLcidList = new ArrayList();
WaitOrTimerCallback callBackDelegate =
new WaitOrTimerCallback(this.DispatcherCallBack);
ThreadPool.RegisterWaitForSingleObject {
_asyncWorkEvent,
callBackDelegate,
null,
_timeOut,
false);
}
}
}
Данный метод вызывается при формировании каждого нового контекста синхронизации, однако делает он что-либо только в том случае, когда этот контекст начинает собой новый домен синхронизации. В этом случае инициализируется свойство синхронизации данного контекста, которое одновременно будет и свойством синхронизации всего домена синхронизации. При включении в этот домен синхронизации нового контекста синхронизации новое свойство синхронизации не создается и его инициализация не требуется.
В коде данного метода мы сталкиваемся с рядом ранее не рассмотренных понятий, таких как критические секции, делегаты, пул рабочих потоков, события. Прежде чем двигаться дальше, рассмотрим упомянутые понятия.
Критические секции
Если атрибут синхронизации позволяет управлять синхронизацией декларативно, то критическая секция обеспечивает решение данной проблемы в рамках парадигмы процедурного программирования. И судя по всему, более эффективно, так как при этом нет необходимости создавать контексты, перехватчики, преобразовывать вызовы в сообщения и обратно из сообщение формировать вызовы.
Рассмотрим несколько примеров.
Ранее мы уже рассматривали консольное серверное приложение MyServer, поддерживающее некоторый банковский счет. Клиентские приложения могли параллельно делать вклады на этот счет. Синхронизация обеспечивалась за счет использования атрибута синхронизации SynchronizationAttribute, который приписывался классу Account, и наследования этого класса от класса ContextBoundObject.
Теперь мы обеспечим синхронизацию за счет использования критических секций.
Простейший способ связан с приписыванием методу Add атрибута [MethodImpl (MethodImplOptions.Synchronized)]. Данный атрибут запретит вод в тело метода Add какого-либо потока, если этот метод уже выполняется в другом потоке. В данном случае мы полностью полагаемся на компилятор, который должен обеспечить требуемую функциональность.
…….
namespace MyServer {
……
public class Account: MarshalByRefObject,
IAccumulator, IAudit {
…….
[MethodImpl(MethodImplOptions.Synchronized)]
public void Add(int sum) {
_sum += sum;
}
…….
}
}
Заметим, ЧТО теперь достаточно наследования класса Account от класса MarshalByRefObject, так как привязка экземпляра этого класса к контексту более не нужна.
Использование атрибута [MethodImpl (MethodImplOptions.Synchronized)] конечно удобно, однако и накладывает на программиста определенные ограничения:
• Критическая секция охватывает все тело метода
Можно представить ситуацию, когда критичная операция, требующая защиты от параллельного выполнения в нескольких потоках, выполняется в данном методе только в некоторых случаях (в зависимости от значений входных аргументов). В этом случае включение всего метода в критическую секцию неоправдано и будет снижать общую эффективность системы.
• Нет возможности запретить параллельный доступ к совместно используемым объектам Предположим, в данном методе выполняется работа с некоторой очередью (экземпляр класса Queue). Конечно, благодаря наличию атрибута [MethodImpl (MethodImplOptions.Synchronized)] В рамках данного метода два потока не смогут параллельно работать с этой очередью и целостность данных будет обеспечена. Однако, ничто не запрещает какому-то другому потоку обратиться к этой же самой очереди в процессе выполнения какого-либо другого метода. Вот тут и возможны нарушения целостности, т. к. между различными потоками, выполняющими параллельно различные методы, нет никакой коммуникации.
Указанные выше проблемы решаются при использовании класса Monitor.
……
namespace MyServer {
…….
public class Account: MarshalByRefObject,
IAccumulator, IAudit {
……
public void Add(int sum) {
…….
Monitor.Enter(this);
try {
_sum += sum;
}
finally {
Monitor.Exit(this);
}
……
}
……
}
}
Вызов статического метода Monitor.Enter () помечает начало критической секции, а вызов метода Monitor.Exit () — ее конец. Аргумент в методе Enter представляет собой ссылку на некоторый объект. В данном случае это ссылка на экземпляр класса Account, на котором и вызван метод Enter, однако ничто не мешает указать ссылку на какой-либо другой объект.
Объект, на который указывает ссылка при вызове Enter, начинает играть роль "эстафетной палочки". Поток, которому удалось вызвать Monitor.Enter (obj), входит в данную критическую секцию, и никакой другой поток не получит ответа от вызова Monitor.Enter (obj), пока первый поток не вызовет Monitor.Exit (obj). Все потоки, сделавшие вызов Monitor.Enter (obj), находятся в одной очереди потоков готовых к выполнению, и эта очередь связана с объектом obj.
Использование блока try и включение вызова Monitor.Exit (obj) в блок finally способствует повышению надежности программирования. Если даже после входа в критическую секцию будет сгенерировано какое-то исключение, вызов Monitor.Exit (obj) будет выполнен в любом случае, и очередной готовый к выполнению поток, заблокированный при вызове Monitor.Enter (obj), начнет выполняться.
Хотя, как указывалось ранее, в качестве "эстафетной палочки" можно использовать любой объект, разумно использовать именно тот объект, ради безопасного доступа к которому и была сформирована данная критическая секция. В этом случае (если такой же подход будет использован при формировании всех критических секций) два различных потока не будут параллельно выполнять критичные для целостности данных операции над одним и тем же объектом.
Компилятор для C# допускает использование конструкции lock (obj) {} для задания критической секции. При этом неявно используется тот же класс Monitor:
……
namespace MyServer {
…….
public class Account: MarshalByRefObject,
IAccumulator, IAudit {
…….
public void Add(int sum) {
lock(this) {
_sum += sum;
}
}
…….
}
}
Имеются еще два метода класса Monitor, которые используются в коде атрибута синхронизации. Это Monitor.Wait
Поделиться книгой в соц сетях:
Обратите внимание, что комментарий должен быть не короче 20 символов. Покажите уважение к себе и другим пользователям!