Потоки
Поток — фактически выполняемая ветвь кода программы. Один или более потоков, запущенных внутри процесса, делают возможным сосуществование многочисленных выполняемых ветвей в рамках одного процесса. Например, используя потоки, программа может модифицировать интерфейс пользователя частичными результатами работы одного потока во время проведения вычислений другим потоком. Все потоки в одном и том же процессе совместно используют среду процесса так, чтобы все они могли обращаться к памяти процесса.
Операционная система планирует процессорное время, занимаемое потоками. Для процессов и прикладных областей (прикладные области обсуждаются в этой главе позже) планирование процессорного времени не выполняется. Потокам периодически дается ограниченный период процессорного времени. Это делается для того, чтобы они могли совместно использовать процессор. Потокам с более высоким приоритетом отрезки времени будут предоставляться более часто, чем потокам с более низким приоритетом. После того, как проходит некоторое время, выполняющийся поток предоставляет шанс запуска другому потоку. Когда потоку снова предоставляется процессорное время, он продолжает выполнение с той точки, в которой его работа была приостановлена.
Потоки поддерживают контекст, который сохраняется и восстанавливается, когда планировщик операционной системы переключает потоки. Контекст потока содержит регистры центрального процессора и стек, в которых хранится состояние выполняющегося кода.
Класс System: : Threading:: Thread (Система::Организация поточной обработ-ки::Поток) моделирует выполняющийся поток. Объект Thread (Поток), который представляет текущий выполняющийся поток, может быть найден с помощью статического свойства Thread :: CurrentThread.
Исключая случаи, когда код выполняется на многопроцессорной машине или когда какой-нибудь поток пробует занять время процессора, в то время как другой поток на однопроцессорной машине пребывает в состоянии ожидания некоторого события типа ввода-вывода, использование нескольких потоков не приводит к ускорению решения вычислительных задач. Благодаря потокам, однако, удается сократить время ответа сие--темы в тех задачах, где требуется взаимодействие с пользователем. При использовании слишком большого количества потоков производительность может уменьшиться. Это происходит из-за накладных расходов на управление потоками и из-за соперничества между конкурирующими потоками, которое также приводит к затратам вычислительных ресурсов центрального процессора.
Чтобы помочь читателю лучше разобраться в работе потоков, мы предоставим многошаговый пример Threading (Организация поточной обработки), в котором используются сборки Customer (Клиент) и Hotel (Гостиница). Сначала мы ознакомимся с программой, а затем выполним резервирование. Итак, для начала рассмотрим шаг 0.
Потоки .NET выполняются как делегаты, определенные классом ThreadStart. Делегат ничего не возвращает (void) и не имеет никаких параметров.
public _gc _delegate void ThreadStart();
// сборщик мусора - делегат ThreadStart ();
Класс NewReservation имеет общедоступный (public) метод MakeReservation, который определяет функцию потока. Так как функция потока не принимает никаких параметров, то в качестве данных она может использовать только поля экземпляра NewReservation.
Делегат потока создается и передается в качестве параметра для конструктора, который создает объект Thread (Поток). Метод Start (Пуск) объекта Thread (Поток) вызывается для того, чтобы начать выполнение потока. Когда мы будем рассматривать модель программирования асинхронных событий, мы покажем, как передать параметры делегату потока. Программа теперь имеет два потока: первоначальный, который выполнял код запуска потока, и поток, который мы только что создали и который попытается забронировать места в гостинице.
public _gc class NewReservation
// класс сборщика мусора NewReservation
{
public:
void MakeReservation()
{
Console::WriteLine(
"Thread {0} starting.", // Запускается поток {О}.
Thread::CurrentThread-> // Поток
GetHashCode().ToString());
ReservationResult *result =
hotelBroker->MakeReservation(
customerld, city, hotel, date, numberDays);
// customerld, город, гостиница, дата, numberDays
}
};
Затем в методе Main (Главный) следующий код запускает поток, используя при этом делегат:
NewReservation *reservel =
new NewReservation(customers, hotelBroker); // клиенты
reservel->customerld = 1;
reservel->city = "Boston"; // город = "Бостон";
reservel->hotel = "Presidential"; // гостиница = "Президентская";
reservel->sdate = "12/12/2001";
reservel->numberDays = 3;
// создать делегат для потоков
ThreadStart *threadStartl = new ThreadStart(
reservel,
reservel->MakeReservation);
Thread *threadl = new Thread(threadStartl); // новый Поток
Console::WriteLine(
"Thread {0} starting a new thread.",
// "Поток {0} запускает новый поток. "
Thread::CurrentThread-> // Поток
GetHashCode().ToString());
threadl->Start ();
Чтобы заставить первоначальный поток ожидать завершения работы второго потока, вызывается метод Join (Объединить) объекта Thread (Поток). Первоначальный поток теперь заблокирован (находится в состоянии ожидания), пока не завершит работу поток, резервирующий место в гостинице. Результат запроса резервирования места выводится на консоль резервирующим потоком.
// Блокировать этот поток, пока не завершится рабочий поток
threadl->Join(); // Объединение
Console::WriteLine("Done!"); // Завершен
Синхронизация потоков
Приложение может создать несколько потоков. Сейчас мы рассмотрим код на шаге 1 из примера Threading (Организация поточной обработки). Теперь несколько запросов о резервировании делаются одновременно.
NewReservation *reservel =
new NewReservation(customers, hotelBroker); // клиенты
NewReservation *reserve2 =
new NewReservation(customers, hotelBroker); // клиенты
// создать делегат для потоков
ThreadStart *threadStartl = new ThreadStart(
reservel,
reservel->MakeReservation);
ThreadStart *threadStart2 = new ThreadStart(
reserve2,
reserve2->MakeReservation);
Thread *threadl = new Thread(threadStartl); // новый Поток
Thread *thread2 = new Thread(threadStart2); // новый Поток
Console::WriteLine(
"Thread {0} starting a new thread.",
// "Поток {0} запустил новый поток. "
Thread::CurrentThread-> // Поток
GetHashCode().ToString());
threadl->Start(); // Пуск
thread2->Start();// Пуск
// Блокировать этот поток, пока не завершится рабочий поток
threadl->Join(); // Объединение
thread2->Join(); // Объединение
Проблема с нашей системой резервирования мест в гостинице состоит в том, что нет никакой гарантии, что один поток не будет влиять на результаты другого. Потоки выполняются только на маленьком временном интервале до того, как уступают процессор другому потоку. Поэтому они могут быть прерваны при выполнении любой операции, над которой работали, если отведенный для них временной промежуток закончился. Например, выполнение потока может быть прервано во время изменения структуры данных. Если другой поток попробует использовать информацию в этой структуре данных или модифицировать ее, то результаты этих операций будут противоречивы и неправильны, или может произойти аварийный отказ программы. (Например, в случае, когда ссылки на устаревшие структуры данных не были еще модифицированы, может произойти необрабатываемое исключение).
Рассмотрим несколько участков в коде, где происходит заказ места в гостинице и там, где происходит резервирование свободных мест — именно в этих точках могут возникнуть трудности подобного рода. Исследуем код для Broker: : Reserve (Брокер::Резерв) в Broker. h. Сначала сделаем проверку существующих заказов для данной гостиницы и для данной даты, чтобы узнать, есть ли свободные номера. Тогда, если есть в наличии свободный номер, он резервируется. Мы добавили вызов метода Thread: :Sleep (Поток-Режим ожидания) между кодом, который проверяет имеющиеся в распоряжении номера и кодом, который резервирует свободный номер. Вскоре будет объяснено, для чего это было сделано.
// Проверить, есть ли номера для всех дат
for (int i = day; i < day + numDays; i++)
{
if (numCust[i, unitid] >= unit->capacity)
{
result->Reservation!d = -1; // результат
result->Comment = "Room not available";
// результат-> Комментарий = "Номера не доступны";
return result; // результат
}
}
Console::WriteLine(
"Thread {0} finds the room is available in
Broker::Reserve",
// "Поток {0} находит, что номер доступен в
// Брокер:: Резерв ",
Thread::CurrentThread-> // Поток GetHashCode{).ToString());
Thread::Sleep(0); // Поток:: Бездействие
// Резервировать номер для требуемых дат
for (int i = day; i < day + numDays; i++)
numCust[i, unitid] += 1;
Этот код может привести к противоречивым результатам! Один из потоков может быть прерван сразу после того, как обнаружит последний имеющийся в распоряжении номер, но прежде, чем он получит шанс сделать заказ. Другой поток, которому предоставляется отрезок процессорного времени, может найти тот же самый доступный номер гостиницы и сделать заказ. Когда первый поток запускается снова, он начинает работу с точки, где был прерван, и также закажет тот же самый последний номер в гостинице.
Чтобы смоделировать возникновение такой ситуации, на этом шаге (шаг 1) примера Threading (Организация поточной обработки) поместим вызов метода Thread:: Sleep (Поток::Режим ожидания) между кодом, который проверяет имеющиеся в распоряжении номера и кодом, который резервирует свободный номер. Вызов Sleep (0) заставляет поток прекратить выполнение и уступить остаток своего временного отрезка.
Чтобы удостовериться в том, что мы наблюдаем именно возникновение проблемы соперничества между потоками, мы имеем в своем распоряжении всего один номер в целой гостинице! Затем мы подготавливаем нашу программу так, чтобы два потока пробовали резервировать единственный номер в гостинице на ту же самую дату. Рассмотрим код в подпрограмме Main (Главная), который все это подготавливает:
hotelBroker->AddHotel(
"Boston", // "Бостон",
"Presidential", // "Президентская",
1, // только один номер во всей гостинице!
(Decimal) 10000); // (Десятичное число)
NewReservation *reservel =
new NewReservation(customers, hotelBroker); // клиенты
reservel->customerld = 1;
reservel->city = "Boston"; // город = "Бостон";
reservel->hotel = "Presidential"; // гостиница = "Президентская";
reservel->sdate = "12/12/2001";
reservel->numberDays = 3;
NewReservation *reserve2 =
new NewReservation(customers, hotelBroker); // клиенты
reserve2->customerld = 2;
reserve2->city = "Boston"; // город = "Бостон";
reserve2->hotel = "Presidential"; // гостиница = "Президентская";
reserve2->sdate = "12/13/2001";
reserve2->numberDays = 1;
Выполнение программы даст приведенный ниже результат:
Added Boston Presidential Hotel with one room.
Thread 3 starting a new thread.
Thread 5 starting.
Thread 6 starting.
Reserving for Customer 2 at the Boston Presidential Hotel on
12/13/2001 12:00:00 AM for 1 days
Reserving for Customer 1 at the Boston Presidential Hotel on
12/12/2001 12:00:00 AM for 3 days
Thread 6 entered Broker::Reserve
Thread 6 finds the room is available in Broker::Reserve
Thread 5 entered Broker::Reserve
Thread 5 finds the room is available in Broker::Reserve
Thread 6 left Broker::Reserve
Reservation for Customer 2 has been booked
Reservationld = 1
Thread 5 left Broker::Reserve
Reservation for Customer 1 has been booked
Reservationld = 2
ReservationRate = 10000
ReservationCost = 30000
Comment = OK
ReservationRate = 10000
ReservationCost = 10000
Comment = OK
Done!
Перевод такой:
Добавлена Бостонская Президентская гостиница с одним номером.
Поток 3 стартовал новый поток.
Поток 5 стартовал.
Поток 6 стартовал.
Резервирование для Клиента 2 в Бостонской Президентской гостинице на
12/13/2001 12:00:00 AM в течение 1 дня
Резервирование для Клиента 1 в Бостонской Президентской гостинице на
12/12/2001 12:00:00 AM в течение 3 дней
Поток 6 ввел Брокер:: Резерв
Поток 6 находит, что номер доступен в Брокер::Резерв
Поток 5 ввел Брокер::Резерв
Поток 5 находит, что номер доступен в Брокер::Резерв
Поток 6 выходит из Брокер::Резерв
Резервирование для Клиента 2 было заказано
Reservationld = 1
Поток 5 выходит из Брокер::Резерв
Резервирование для Клиента 1 было заказано
Reservationld = 2
ReservationRate = 10000
ReservationCost = 30000
Комментарий = OK
ReservationRate = 10000
ReservationCost = 10000
Комментарий = OK
Сделано!
К сожалению, оба клиента добиваются резервирования последнего (единственного) номера на 13 декабря! Обратите внимание на то, как один из потоков выполняет метод Reserve (Резерв) и находит, что номер доступен прежде, чем прекратит свою работу. Затем другой поток выполняет Reserve (Резерв) и также обнаруживает свободный номер прежде, чем закончится отведенный для него промежуток времени. Потом оба потока заказывают тот же самый номер гостиницы.
Операционные системы предоставляют средства синхронизации взаимодействия нескольких потоков и процессов, получающих доступ к совместно используемым ресурсам. Каркас .NET Framework предусматривает несколько механизмов предотвращения конфликтов между потоками.
Каждый объект в каркасе .NET Framework может использоваться в качестве синхронизирующей секции кода (критической секции). В пределах такой секции одновременно может выполняться только один поток. Если один поток уже выполняется внутри такой синхронизирующей секции кода, любые другие потоки, пытающиеся получить доступ к данной секции, блокируются (и ждут) до тех пор, пока выполняющийся поток не покинет эту секцию кода.
Синхронизация с помощью мониторов
Класс System: :Threading: :Monitor (Система::Организация поточной об-работки::Монитор) позволяет потокам синхронизировать доступ к объектам, чтобы избежать нарушения целостности данных и неопределенного поведения вследствие соперничества между потоками. Шаг 2 из примера Threading (Организация поточной обработки) демонстрирует использование класса Monitor (Монитор) с указателем this экземпляра HotelBroker.
ReservationResult *Reserve(Reservation *res) // Резервирование
{
Console::WriteLine(
"Thread {0} trying to enter Broker::Reserve",
// "Поток {0} пытается войти в Брокер::Резерв",
Thread::CurrentThread-> // Поток
GetHashCode() .ToString ()); Monitor::Enter(this); // Монитор::Войти
. . . (спорный код потока здесь) Monitor::Exit(this); // Выход
return result; // результат
}
Потоку, который первым вызовет метод Monitor: : Enter (this), будет разрешено выполнить метод Reserve (Резерв), потому что он овладеет замком метода Monitor (Монитор) с помощью указателя this. Потоки, пробующие выполнить те же действия, должны будут ожидать, пока первый поток не разблокирует замок с помощью метода Monitor: :Exit (this). После этого и другие потоки смогут вызвать Monitor::Enter(this) и овладеть замком.
Поток может вызывать Monitor: :Enter (Монитор::Войти) несколько раз, но каждый вызов должен быть скомпенсирован вызовом Monitor: :Exit (Монитор::Выйти). Если поток пытается овладеть замком, но не хочет заблокировать себя, то он может использовать метол Monitor::TryEnter.
Теперь, когда мы обеспечили синхронизацию, случай, идентичный испытанному в шаге 1, не приведет к повторному резервированию одного и того же гостиничного номера на одно и то же время. Обращаем внимание читателя на то, сколько времени второй поток не может начать выполнение метода Reserve (Резерв) до тех пор, пока первый поток, который начал выполнение этого метода раньше, не покинет критическую секцию. Это решает нашу проблему конкуренции между потоками и устанавливает правило, согласно которому один и тот же номер гостиницы не может быть забронирован двумя клиентами на ту же самую дату.
Added Boston Presidential Hotel with one room.
Thread 3 starting a new thread.
Thread 5 starting.
Thread б starting.
Reserving for Customer 2 at the Boston Presidential Hotel on
12/13/2001 12:00:00 AM for I days
Thread 6 trying to enter Broker::Reserve
Thread 6 entered Broker::Reserve
Thread 6 finds the room is available in Broker::Reserve
Thread 6 left Broker::Reserve
Reservation for Customer 2 has been booked
Reservationld = 1
Reserving for Customer 1 at the Boston Presidential Hotel on
12/12/2001 12:00:00 AM for 3 days
Thread 5 trying to enter Broker::Reserve
Thread 5 entered Broker::Reserve
Reservation for Customer 1 could not be booked
Room not available
ReservationRate = 10000
ReservationCost = 10000
Comment = OK
Done !
Пере вод такой:
Добавлена Бостонская Президентская гостиница с одним номером.
Поток 3 стартовал новый поток.
Поток 5 стартовал.
Поток 6 стартовал.
Резервирование для Клиента 2 в Бостонской Президентской гостинице на
12/13/2001 12:00:00 AM в течение 1 дня
Поток 6 пытается войти в Брокер::Резерв
Поток 6 вошел в Брокер::Резерв
Поток 6 находит, что номер доступен в Брокер::Резерв
Поток 6 вышел из Брокер::Резерв
Резервирование для Клиента 2 было заказано
Reservationld = 1
Резервирование для Клиента 1 в Бостонской Президентской гостинице на
12/12/2001 12:00:00 AM в течение 3 дней
Поток 5 пытается войти в Брокер::Резерв
Поток 5 вошел в Брокер::Резерв
Резервирование для Клиента 1 не могло быть заказано
Номера не доступны
ReservationRate = 10000
ReservationCost = 10000
Комментарий = OK
Сделано!
Уведомление с помощью мониторов
Поток, овладевший замком монитора Monitor (Монитор), может, не покидая блока синхронизации, ожидать сигнал от другого потока, который выполняет операцию синхронизации на том же самом объекте. Поток вызывает метод Monitor::Wait (Монитор::Ожидать) и освобождает замок. Потом, когда он получит уведомление от другого потока, то повторно овладевает замком в целях синхронизации.
Поток, овладевший замком монитора Monitor (Монитор), может послать уведомление другому потоку, ожидающему разрешения на доступ к тому же самому объекту с помощью методов Pulse (Импульс, Сигнал) или PulseAll. Важно, что во время посылки сигнала поток переходит в состояние ожидания. В противном случае, если сигнал будет послан, но поток не подождет некоторое время, то другой поток будет ждать вечно и никогда не сможет получить уведомление. Такое поведение не похоже на событие возврата в исходное состояние, обсуждаемое позже в этой главе. Если много потоков ожидают сигнал, метод Pulse (Импульс, Сигнал) поместит только один поток в очередь готовых для выполнения. PulseAll поместит их все в эту очередь.
Поток, пославший импульс, больше не владеет замком монитора Monitor (Монитор), но и не блокируется; он может продолжать выполнение. Так как он больше не заблокирован, но не владеет замком, то, чтобы избежать взаимоблокировки (дедлока) или состояния гонок, этот поток должен пробовать повторно овладеть замком (с помощью методов Monitor: :Enter (Монитор::Войти) или Wait (Ожидать)), перед выполнением любых потенциально опасных действий.
Пример PulseAll иллюстрирует применение методов Pulse (Импульс, Сигнал) и PulseAll. Пример во время выполнения генерирует следующую выдачу:
First thread: 2 started.
Thread: 5 started.
Thread: 5 waiting.
Thread: 6 started.
Thread: 6 waiting.
Thread 5 sleeping.
Done .
Thread 5 awake.
Thread: 5 exited.
Thread 6 sleeping.
Thread 6 awake.
Thread: 6 exited.
Пере вод такой:
Первый поток: 2 стартовал.
Поток: 5 стартовал.
Поток: 5 в состоянии ожидания.
Поток: 6 стартовал.
Поток: 6 в состоянии ожидания.
Поток 5 бездействует.
Сделано.
Поток 5 активный.
Поток: 5 вышел.
Поток 6 бездействует.
Поток 6 активный.
Поток: 6 вышел.
Класс X содержит поле "о" типа Ob j ect (Объект), которое будет использоваться в качестве синхронизирующего замка.
Класс также содержит метод Test (Испытание), который используется в качестве делегата потока. Метод овладевает синхронизирующим замком, а затем ждет уведомления. Когда он получит уведомление, то бездействует в течение половины секунды, а затем вновь освобождает замок.
Метод main (главный) создает два потока, которые используют метод X: :Test (Х::Испытание) в качестве делегата своего потока и совместно используют тот же самый объект, предназначенный для синхронизации. Затем он бездействует в течение 2 секунд, чтобы позволить потокам произвести запросы ожидания и освободить замки. Потом метод main (главный) вызывает метод PulseAll, чтобы уведомить оба ожидающих потока и освободить замки. В конечном счете, каждый поток повторно овладевает замком, выводит сообщение на консоль, и в последний раз освобождает замок.
_gc class X
// класс сборщика мусора X
{ private: // частный
Object *o; // Объект public:
X(Object *o) // Объект
{
this->o = о;
}
void Test () // Испытание
{
try
{
long threadld =
"Thread->GetHashCode () ; // Поток
console::WriteLine(
"Thread: {0} sta.ted.",
threadld.ToString()); // "Поток: {0} стартовал."
Monitor::Enter(о1, // Монитор::Войти
Console : : Write1" jre (
'"In.-'ad: tO) waiting.",
threadld.ToString()); // "Поток: {0} ожидает."
Monitor::Wait(о); // Монитор::Ждет
Console::WriteLine( "Thread {0} sleeping.",
threadld.ToString()); // "Поток {0} бездействует."
Thread::Sleep(500); // Поток::Бездействовать
Console::WriteLine( "Thread {0} awake.",
threadld.ToString()); // "Поток {0} активный. "
Monitor::Exit (о); // Выход
Console::WriteLine( "Thread: {0} exited.",
threadld.ToString()); // "Поток: {0} вышел."
}
catch(Exception *e) // Исключение
{
long threadld =
Thread::CurrentThread->GetHashCode();
Console::WriteLine(
"Thread: {0} Exception: {!}", // "Поток: {0} Исключение: {1} "
threadld.ToString(), e->Message); // Сообщение Monitor::Exit(о); // Выход
}
}
};
_gc class Classl
// класс сборщика мусора Classl
{
public:
static Object *o = new Object;
// статический Объект *о = новый Объект;
static void Main()
{
Console::WriteLine(
"First thread: {0} started.",
// "Первый поток: {0} стартовал. ",
Thread::CurrentThread->GetHashCode().ToString() ) ;
X *a = new X(o); X *b = new X(o);
ThreadStart *ats = new ThreadStart(a, X::Test); // Испытание
ThreadStart *bts = new ThreadStart(b, X::Test); // Испытание
Thread *at = new Thread(ats); // новый Поток
Thread *bt = new Thread(bts); // новый Поток
at->Start(); // Начало bt->Start (); // Начало
// Бездействовать, чтобы позволить другим потокам ждать
// объект перед Импульсом (Pulse) Thread::Sleep(2000);
// Бездействие Monitor::Enter (о);
// Монитор::Войти
Monitor::PulseAll(о);
//Monitor::Pulse(о);
Monitor::Exit(о);
// Выход
Console::WriteLine("Done.");
// Сделано
}
};
Только один поток сможет завершить свою работу, если закомментировать вызов PulseAll и убрать комментарий с вызова метода Pulse (Импульс, Сигнал), потому что другие потоки никогда не смогут стать в очередь готовых потоков. Если удалить Sleep (2000) из главной подпрограммы main (главная), то другие потоки заблокируются навсегда, потому что посылка сигнала происходит до того, как потоки получат шанс вызвать метод Wait (Ожидать), и, следовательно, они никогда не получат уведомления. Методы Wait (Ожидать), Pulse (Импульс, Сигнал) и PulseAll могут использоваться для координирования использования несколькими потоками синхронизационных замков.
Метод Thread: : Sleep (Поток::Режим ожидания) приводит к приостановке выполнения текущего потока на указанный период времени. Вызов Thread: : Suspend (Поток::Приостановить) блокирует выполнение потока до вызова Thread: :Resume (Поток::Продолжить) другим потоком. Поток также может быть заблокирован, если он ожидает завершения другого потока (Thread: : Join (Поток::Объединить)). Этот метод использовался в примерах Threading (Организация поточной обработки) так, чтобы главный поток мог ждать завершения выполнения запросов резервирования. Поток может также блокироваться при ожидании синхронизирующего замка (в критической секции).
Вызов Thread: : Interrupt (Поток::Прерывание) для заблокированного потока приводит к его пробуждению. Поток получит ThreadlnterruptedException. И если он не перехватывает это исключение, то среда времени выполнения это исключение перехватит и уничтожит поток.
Если, в качестве последней надежды, нужно уничтожить поток напрямую, необходимо для этого потока сделать вызов метода Thread: :Abort (Поток:прекратить). Метод Thread: :Abort (Поток::Прекратить) приводит к запуску исключения Thread-AbortException. Это исключение не может быть перехвачено, но оно приведет к выполнению всех блоков finally (наконец). Кроме того, Thread: :Abort (Поток:: Прекратить) не приводит к пробуждению ожидающего потока.
Поскольку для выполнения блоков finally (наконец) может потребоваться время, или потоки могут находиться в состоянии ожидания, то потоки, выполнение которых прекращается, могут быть завершены не сразу. Поэтому, если нужно убедиться в том, что выполнение потока завершено, необходимо ожидать завершения потока, вызвав метод Thread: : Join (Поток::Объединить).
Классы синхронизации
Каркас .NET Framework содержит классы, представляющие собой стандартные в Win32 объекты синхронизации. Все эти классы являются производными от абстрактного класса WaitHandle. Этот класс имеет статические методы Wait All и WaitAny, которые позволяют ожидать сигнал от нескольких (или только от одного) объектов синхронизации. Он также содержит метод экземпляра WaitOne, который позволяет ожидать сигнал отданного экземпляра. Способ приема сигнала объектом зависит от конкретного типа объекта синхронизации, который является производным от WaitHandle.
Объект Mutex (Взаимное исключение) используется для синхронизации процессов. Мониторы и критические секции работают только в пределах одного процесса. AutoResetEvent и ManualResetEvent используются для того, чтобы просигнализировать, произошло ли какое-нибудь событие. AutoResetEvent остается в сигнальном состоянии, пока ожидающий поток не будет освобожден. ManualResetEvent остается в сигнальном состоянии, пока его состояние не установлено в несигнальное с помощью метода Reset (Сброс). Следовательно, много потоков могут получить сигнал с помощью такого события. В отличие от использования мониторов, код не должен ожидать сигнала прежде, чем устанавливается сигнал сброса события, которое посылает сигнал потоку.
Каркас предоставляет классы для разрешения некоторых стандартных задач организации поточной обработки. Методы класса Interlocked (Сблокированный) разрешают атомарные операции с совместно используемыми значениями. Это операции типа приращения, декремента, сравнения и обмена. ReaderWriterLock используется, чтобы предоставить доступ для одиночной записи и множественного чтения структур данных. Класс ThreadPool позволяет управлять пулом рабочих потоков.
Автоматическая синхронизация
Атрибуты можно использовать для синхронизации доступа к методам экземпляра (нестатическим) и нестатическим полям класса. Доступ к статическим полям и методам не синхронизируется. Чтобы использовать эту возможность нужно создать класс, производный от класса System: :ContextBoundObject и применить к нему атрибут Synchronization (Синхронизация). Этот атрибут не применяется к отдельному полю или методу.
Данный атрибут находится в пространстве имен System :: Runtime :: Remoting :: Contexts (Система::Время выполнения:: Remoting::Контексты). Он описывает синхронизационные требования для экземпляра класса, к которому применяется. В конструктор SynchronizationAttribute можно передать одно из четырех значений, являющихся статическими полями класса SynchronizationAttribute: NONSUPPORTED, SUPPORTED (ПОДДЕРЖИВАЕМЫЙ), REQUIRED (ТРЕБУЕМЫЙ), REQUIRES_NEW. Пример Threading (Организация поточной обработки) на шаге 3 иллюстрирует, как это сделать.
using namespace System :: Runtime :: Remoting :: Contexts;
// использование пространства имен
// Система :: Время выполнения :: Remoting :: Контексты;
// SynchronizationAttribute :: REQUIRED (ТРЕБУЕМЫЙ) - 4
[Synchronization(4) ]
// [Синхронизация (4)]
public _gc _abstract class Broker :
// сборщик мусора - абстрактный класс Broker:
public ContextBoundObject
{
};
Общеязыковая среда времени выполнения CLR, чтобы удостовериться в том, что поток, в котором выполняются методы объекта, синхронизирован должным образом, должна отслеживать требования объекта по организации поточной обработки. Эти требования называются контекстом объекта. Если один объект должен быть синхронизирован, а другой нет, то говорят, что они находятся в двух отдельных контекстах. Общеязыковая среда времени выполнения CLR должна овладеть замком синхронизации от имени кода, когда поток, который выполняет не нуждающийся в синхронизации метод объекта, начинает выполнять нуждающийся в синхронизации метод другого объекта. Общеязыковая среда времени выполнения CLR знает, что в этом случае нужно сделать, потому что она может сравнить требования первого объекта к организации поточной обработки с требованиями второго объекта, просто сравнивая их контексты.
Об объектах, разделяющих одинаковое состояние, говорят как о живущих в том же самом контексте. Например, два объекта, которые не должны быть синхронизированы, могут совместно использовать один и тот же контекст. ContextBoundOb j ect и контексты обсуждаются более подробно в разделе, посвященном контекстам.
Используя это интуитивное понятие контекста, мы можем теперь объяснить назначение различных атрибутов синхронизации Synchronization (Синхронизация). NOT_SUPPORTED означает, что класс не может поддерживать синхронизацию своих методов экземпляров и нестатических полей и поэтому не должен создаваться в контексте синхронизации. REQUIRED (ТРЕБУЕМЫЙ) значит, что класс требует синхронизации доступа к своим методам и полям экземпляров. Однако, если поток уже синхронизирован, он может использовать существующий замок синхронизации и жить в существующем контексте синхронизации. REQUIRES_NEW означает, что требуется не только синхронизация, но также и доступ к методам и полям экземпляра должен происходить с уникальным замком синхронизации и в отдельном контексте. Атрибут SUPPORTED (ПОДДЕРЖИВАЕМЫЙ) применяется, если класс не требует синхронизации доступа к методам и полям экземпляра; для него не нужно создавать новый контекст.
Можно также передать булев флажок в конструктор, чтобы указать, требуется ли реентерабельность. Если флажок установлен (т.е. реентерабельность требуется), синхронизируются обратные вызовы из методов. В противном случае синхронизируются только вызовы методов.
Используя атрибут Synchronization (Синхронизация) в методе Brocker :: Reserve (Брокер :: Резерв), можно отказаться от Monitor :: Enter (Монитор :: Войти) и Monitor :: Exit (Монитор :: Выйти).
Так же, как и в шаге 2, в этом примере иллюстрируется попытка дважды зарезервировать последний свободный номер в гостинице. Вот как выглядит выдача при выполнении этого примера:
Added Boston Presidential Hotel with one room.
Thread 13 starting a new thread.
Thread 28 starting.
Thread 29 starting.
Reserving for Customer 1 at the Boston Presidential Hotel on
12/12/2001 12:00:00 AM for 3 days
Thread 28 entered Broker::Reserve
Thread 28 finds the room is available in Broker::Reserve
Thread 28 left Broker::Reserve
Reservation for Customer 1 has been booked
Reservationld = 1 ReservationRate = 10000
ReservationCost = 30000
Comment = OK
Reserving for Customer 2 at the Boston Presidential Hotel on
12/13/2001 12:00:00 AM for 1 days
Thread 29 entered Broker::Reserve
Thread 29 left Reserve.
Reservation for Customer 2 could not be booked
Room not available
Done !
Пере вод такой:
Добавлена Бостонская Президентская гостиница с одним номером.
Поток 13 стартовал новый поток.
Поток 28 стартовал.
Поток 29 стартовал.
Резервирование для Клиента 1 в Бостонской Президентской гостинице
на
12/12/2001 12:00:00 AM в течение 3 дней
Поток 28 вошел в Брокер::Резерв
Поток 28 находит, что номер доступен в Брокер::Резерв
Поток 28 вышел из Брокер::Резерв
Резервирование для Клиента 1 было заказано
Reservationld = 1
ReservationRate = 10000
ReservationCost = 30000
Комментарий = OK
Резервирование для Клиента 2 в Бостонской Президентской гостинице на
12/13/2001 12:00:00 AM в течение 1 дня
Поток 29 вошел в Брокер::Резерв
Поток 29 вышел из Резерв.
Резервирование для Клиента 2 не могло быть заказано
Номер не доступен
Сделано!
Как и в предыдущем случае, второй поток не может выполнить метод Reserve (Резерв), пока поток, который начал выполнение первым, не закончится. Номер резервируется только раз.
Отличие этого автоматического подхода заключается в том, что синхронизируется доступ ко всем методам класса независимо от того, нуждаются они в этом или нет. Доступ к другим совместно используемым данным с помощью других методов данного класса также блокируется. Такое поведение получается независимо от желания программиста.
Обращаем внимание читателя на то, что в любой определенный момент только один поток может выполнять какой-нибудь метод класса. Предположим, что мы добавляем к классу еще один метод под названием CancelReservation. Если какой-нибудь поток вызвал метод CancelReservation, то блокируются все другие потоки, делающие попытки выполнять другие методы класса, включая и метод MakeReservation.
Для программы, которая производит резервирование номеров в гостинице, это желательное поведение, так как нежелательно, чтобы метод MakeReservation пытался использовать структуру данных в момент ее изменения. В некоторых ситуациях, однако, такое поведение нежелательно и уменьшает производительность из-за ненужного блокирования. Увеличение числа конфликтов может стать помехой для масштабирования, так как блокируются не только определенные, нуждающиеся в синхронизации области.
Использовать атрибуты проще, чем критические секции. Не нужно заботиться о подробностях правильной реализации синхронизации. С другой стороны, в результате мы получаем поведение, которое уменьшает масштабируемость. Разные приложения и различные части одного и того же приложения могут получать выгоду от использования того подхода, который в данном конкретном случае имеет больше смысла.
CompEbook.ru Железо, дизайн, обучение и другие