Шрифт:
Интервал:
Закладка:
Рассмотрим следующую модификацию предыдущего примера:
…….
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. Заметим, что этот метод выполняется так называемыми рабочими потоками, извлекаемыми системой из пула рабочих потоков. Рабочий поток не может пережить основной поток и по завершении последнего завершается и текущий рабочий поток.
Данное консольное приложение после запуска прежде всего выводит на консоль информацию о потоке, выполняющем код функции Main. Это хеш потока и информация о том, является ли данный поток рабочим потоком. Очевидно, что в данном случае значение последнего параметра должно быть равно false так как текущий поток является основным потоком приложения.
Далее вызывается конструктор класса Test. Здесь также выводится информация о потоке — том потоке, который выполняет код конструктора. Этот тот же самый основной поток приложения.
Далее в конструкторе создается экземпляр myEvent события типа AutoResetEvent. События в .NET еще не обсуждались в данном курсе, но в данном случае используется событие специального типа, реализованное в системе. В связи с этим пока будет достаточно рассмотреть только это событие и только в контексте данного примера.
Для работы с пулом потоков мы регистрируем с помощью статического метода ThreadPool.RegisterWaitForSingleObject делегат myCallBackDelegate специального известного системе типа WaitOrTimerCallback. Делегат является некоторым аналогом указателя на функцию (в данном случае на MуCаllBаск), которая должна иметь сигнатуру, заданную при объявлении делегата. В случае делегата типа WaitOrTimerCallback возвращаемое значение должно отсутствовать (void), первый аргумент должен иметь тип System.Object и может использоваться для передачи вызываемой функции произвольных