Интернет-журнал "Домашняя лаборатория", 2007 №6
Шрифт:
……
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 и Monitor.Pulse .
Рассмотрим следующую модификацию предыдущего примера:
…….
namespace MyServer {
…….
public class Account: MarshalByRefObject,
IAccumulator, IAudit {
…….
public void Add(int sum) {
lock(this) {
Console.WriteLine (Thread.CurrentThread.GetHashCode };
int s = _sum;
Thread.Sleep(1);
_sum = s + sum;
if (_sum == 5) {Monitor.Wait(this);}
if (_sum == 505) {Monitor.Pulse(this);}
}
}
……
}
Напомним, что данный фрагмент кода выполняется на сервере MyServer.ехе, к которому параллельно могут обращаться несколько клиентов. Каждый клиент (приложение MуАрр) посылает на сервер 100 раз по 5 условных единиц.
Выводя на консоль хеш потока, мы можем отследить чередование рабочих потоков в очереди готовых к выполнению потоков. Сохранение текущей величины счета в локальной переменной s и вызов Thread.Sleep (1) используются для более явного выявления эффектов, связанных с многопоточностью.
Как правило (если в предыдущем фрагменте кода закомментировать строки с вызовами Monitor.Wait и Monitor.Pulse), один и тот же поток может несколько раз подряд войти в данную критическую секцию и положить на счет очередные 5 условных единиц, прежде чем выделенный ему квант времени закончится и начнет исполняться другой рабочий поток. После нескольких циклов вновь начинает работать первый поток и так далее. Используя методы Wait и Pulse класса Monitor мы можем управлять очередностью входа различных потоков в данную критическую секцию.
Как только первый поток входит в нашу критическую секцию, он выводит на консоль свой идентификатор, запоминает текущее значение счета в локальной переменной и засыпает на 1 миллисекунду. Пробудившись, этот поток обновляет счет (его величина становится равной 5).
В связи с выполнением условия _sum == 5 выполняется вызов Monitor.Wait (this). В этот момент первый поток освобождает объект this и становится в очередь ожидания. Эта еще одна, связанная с объектом очередь (наряду с очередью потоков, готовых к выполнению). Разница между ними состоит в следующем. Очередной поток из очереди готовых к выполнению потоков начинает выполняться, если текущий исполняемый поток завершил выполнение критической секции (вызвал Monitor.Exit (this), то есть освободил объект this). Потоки из очереди ожидания становятся в очередь потоков готовых к выполнению, если текущий исполняемый поток вызвал Monitor.Pulse (this), сигнализируя тем самым, что состояние объекта this изменилось и ожидающие потоки могут работать с данным объектом.