Добавить в цитаты Настройки чтения

Страница 224 из 372

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

Критические секции

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

Рассмотрим несколько примеров.

Ранее мы уже рассматривали консольное серверное приложение 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 () и 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 мы можем управлять очередностью входа различных потоков в данную критическую секцию.