Выбрать главу

Объект, на который указывает ссылка при вызове 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 изменилось и ожидающие потоки могут работать с данным объектом.

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

Делегаты, регистрация callback делегата в пуле рабочих потоков

В коде метода InitIfNecessary класса SynchronizationAttribute используются упомянутые в заголовке данного раздела сущности. Познакомимся с их применением в процессе разбора следующего примера:

using System;

using System.Threading;

public class Test {

     private AutoResetEvent _myEvent;

     private int _count = 0;

     public Test() {

          Console.WriteLine("»> Test constructor thread = " +

                    Thread.CurrentThread.GetHashCode() +

                    " IsPoolThread = " +

                    Thread.CurrentThread.IsThreadPoolThread);

           _myEvent = new AutoResetEvent(false);

          WaitOrTimerCallback myCallBackDelegate =

               new WaitOrTimerCallback(this.MyCallBack);

          ThreadPool.RegisterWaitForSingleObject Х

               _myEvent,

               myCallBackDelegate,

               null,

               100,

                false);

     }

      public int count {

             get { return _count; }

              set { _count = value;}

      }

      private delegate String MyDelegate ();

      private void MyCallBack (Object state, bool timedOut) {

             Console.WriteLine("»> MyCallback thread = " +

                                Thread.CurrentThread.GetHashCode() +

                                " IsPoolThread = " +

                                Thread.CurrentThread.IsThreadPoolThread);

              MyDelegate hello = new MyDelegate(MyHello);

              count++;

              Console.WriteLine(hello() + " Count = " + count +

                               " timedOut = " + timedOut);

       }

       private String MyHello() {

              return "Test_" + count +": ";

       }

       public void NewEvent() {

              _myEvent.Set();

       }

}

public class MyApp {

      public static void Main () {

              Console.WriteLine("ЮЮ> MyApp thread = " +

                                Thread.CurrentThread.GetHashCode() +

                                " IsPoolThread = " +

                                Thread.CurrentThread.IsThreadPoolThread);

               Test test = new Test();

               test.NewEvent();

               Thread.Sleep(500);

               test.NewEvent();

               Thread.Sleep(1000);

      }

}

Опишем прежде всего семантику нашего приложения.

Метод Main выполняется в основном потоке приложения. Все приложение в целом завершается по завершении этого метода (после возвращения из вызова функции Thread.Sleep (1000)). Параллельно с выполнением основного потока несколько раз успевает выполниться метод MyCallBack класса Test. Заметим, что этот метод выполняется так называемыми рабочими потоками, извлекаемыми системой из пула рабочих потоков. Рабочий поток не может пережить основной поток и по завершении последнего завершается и текущий рабочий поток.