Глава.30 .Гибридные.конструкции.синхронизации.потоков
Проснувшись, поток, выполняющий метод
Thread1
, снова проверяет условие
в цикле. Если оно все еще не соблюдено, он опять вызывает метод
Wait
. В противном
случае он обрабатывает данные и, в конце концов, вызывает метод
Exit
, снимая
блокировку и давая доступ другим потокам возможность получить ее. Таким образом
данный паттерн позволяет проверить несколько формирующих сложное условие
переменных при помощи простой логики синхронизации (всего одной блокировки),
а несколько ожидающих потоков могут разблокироваться без нарушения какой-
либо логики, хотя при этом возможна напрасная трата процессорного времени.
Ниже приведен пример безопасной в отношении потоков очереди, которая за-
ставляет несколько потоков встраивать элементы в очередь и удалять их. Обратите
внимание, что потоки, пытающиеся удалить элемент из очереди, блокируются до
момента, пока элемент не становится доступным для обработки.
internal sealed class SynchronizedQueue {
private readonly Object m_lock = new Object();
private readonly Queue m_queue = new Queue();
public void Enqueue(T item) {
Monitor.Enter(m_lock);
// После постановки элемента в очередь пробуждаем
// один/все ожидающие потоки
m_queue.Enqueue(item);
Monitor.PulseAll(m_lock);
Monitor.Exit(m_lock);
}
public T Dequeue() {
Monitor.Enter(m_lock);
// Выполняем цикл, пока очередь не опустеет (условие)
while (m_queue.Count == 0)
Monitor.Wait(m_queue);
// Удаляем элемент из очереди и возвращаем его на обработку
T item = m_queue.Dequeue();
Monitor.Exit(m_lock);
return item;
}
}
асинхронная синхронизация
Мне не сильно нравятся все эти конструкции синхронизации потоков, использую-
щие примитивы в режиме ядра. Ведь они нужны для блокирования потоков, в то
883
Асинхронная.синхронизация
время как создание потока обходится слишком дорого, чтобы потом он не работал.
Для наглядности рассмотрим пример.
Представьте веб-сайт, к которому клиенты обращаются с запросами. Посту-
пивший запрос начинает обрабатываться потоком из пула. Пусть клиент хочет
безопасным в отношении потоков способом изменить данные на сервере, поэтому
он получает блокировку на чтение-запись для записи. Представим, что блокирова-
ние длится долго. За это время успевает прийти еще один клиентский запрос, для
которого пул создает новый поток, пытающийся получить блокировку на чтение-
запись для чтения. Запросы продолжают поступать, пул создает дополнительные
потоки, и все эти потоки блокируются. Все свое время сервер занимается созда-
нием потоков и не может остановиться! Такой сервер вообще не может нормально
масштабироваться.
Все становится только хуже, когда поток записи снимает блокировку, и одновре-
менно запускаются все заблокированные потоки чтения. В результате относительно
небольшому количеству процессоров нужно как-то обработать все это множество
потоков. Windows попадает в ситуацию постоянного переключения контекста.
В результате весь объем работы выполняется не так быстро, как мог бы.
Многие из проблем, решаемые при помощи описанных в этой главе конструкций,
намного успешнее решаются средствами класса
Task
, рассмотренного в главе 27.
К примеру, возьмем класс
Barrier
: для работы на каждом этапе можно было бы
создать группу заданий (объектов
Task
), а после их завершения ничто не мешает
нам продолжить работу с дополнительными объектами
Task
. Такой подход имеет
целый ряд преимуществ в сравнении с конструкциями, описанными в этой главе:
Задания требуют меньше памяти, чем потоки, кроме того, они намного быстрее
создаются и уничтожаются.
Пул потоков автоматически распределяет задания среди доступных процессо-
ров.
По мере того как каждое задание завершает свой этап, выполнявший его поток
возвращается в пул, где может заняться другой работой, если таковая имеется.
Пул потоков видит все задания сразу и поэтому может лучше планировать их
выполнение, сокращая количество потоков в процессе, а значит, и количество
переключений контекста.
Блокировки популярны, но при удержании в течение долгого времени они
создают серьезные проблемы с масштабированием. Было бы очень полезно иметь
асинхронные конструкции синхронизации, в которых ваш код сообщает о том, что
он хочет получить блокировку. Если получить ее не удалось, он просто возвращает
управление для выполнения другой работы (вместо блокирования на неопреде-
ленное время). Затем, когда блокировка станет доступной, выполнение кода воз-
обновляется, и он может получить доступ к ресурсу, защищенному блокировкой. Эта
идея появилась у меня в процессе решения серьезных проблем масштабируемости
884
Do'stlaringiz bilan baham: |