Программирование >>  Разработка устойчивых систем 

1 ... 172 173 174 [ 175 ] 176 177 178 ... 196


546 Глава И Многопоточное программирование

try {

Thread h1gh(new SimplePriorities): high.setPr1or1ty(High): fordnt 1 =0: i < 5: {

Thread low(new SimplePrioritiesd)):

low.setPr1or1ty(Low):

} catch(Synchron1zation Exception& e) { cerr e.whatO endl:

} III:-

Оператор переопределяется для вывода основных параметров задачи: идентификатора, приоритета и значения countDown.

Как видите, приоритет потока high устанавливается на максимальном уровне, а приоритеты всех остальных потоков находятся на минимальном уровне. Класс Executor в этом примере не используется, поскольку для задания приоритетов необходим прямой доступ к программным потокам.

Внутри функции SimplePriorities::run() 100 ООО раз выполняются относительно дорогостоящие вещественные вычисления с суммированием и делением типа double. Переменная d объявлена подвижной (volatile), чтобы компилятор не пытался применять оптимизацию. Без этих вычислений эффект от назначения приоритетов остался бы незамеченным (попробуйте закомментировать цикл for с вещественными вычислениями). А так очевидно, что планировщик отдает большее предпочтение потоку high (по крайней мере, в системе Windows). Так как вычисления происходят достаточно долго, планировщик успевает вмещаться и переключить потоки с учетом приоритета, отдавая предпочтение потоку high.

Функция getPriorityO возвращает приоритет существующего потока, а функция setPriorityO позволяет сменить его в любой момент (а не только перед запуском потока, как в примере SimplePriorities.cpp).

Система приоритетов в значительной степени зависит от конкретной операционной системы. Например, Windows 2000 поддерживает семь уровней приоритета, а в системе Solaris фирмы Sun предусмотрено аж 2 уровней. Существует только один переносимый вариант системы приоритетов очень большой гранулярности, например. Low, Medium и High, как в библиотеке ZThread.

Совместное использование ограниченных ресурсов

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

Но в многопоточных приложениях нам уже приходится учитывать возможность того, что два и более программных потока попытаются одновременно использовать общий ресурс. Такие проблемы делятся на две категории. Во-первых, необходимые ресурсы могут не существовать. В С++ программист полностью контроли-



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

Во-вторых, одновременное обращение к общему ресурсу может породить конфликт между потоками. Если не позаботиться о предотвращении таких конфликтов, два потока могут одновременно изменить состояние одного банковского счета, вывести данные на один принтер, отрегулировать состояние одного клапана

ИТ. д.

в этом разделе рассматривается проблема исчезновения объектов во время их использования и проблема конфликта при обращениях к общим ресурсам. Вы познакомитесь со средствами решения этих проблем.

Гарантия существования объектов

Управление памятью и ресурсами занимает особое место в С++. При написании любых программ на С++ программист выбирает между созданием объектов в стеке и в куче (с помощью оператора new). В однопоточной программе жизненный цикл объектов легко отслеживается, и попытки использования ранее уничтоженных объектов встречаются крайне редко.

В примерах, приводимых в этой главе, объекты Runnable создаются в куче оператором new. Но обратите внимание: ни один из этих объектов не уничтожается явно. Тем не менее из выходных данных видно, что библиотека отслеживает каждую задачу и в конечном счете удаляет ее (об этом свидетельствует вызов деструкторов для объектов задач). Это происходит при выходе из Runnable::run() - возврат из run() означает, что задача прекращает свое существование.

Однако попытка возложить ответственность за уничтожение задачи на программный поток порождает проблемы. Поток не может знать, потребуется ли другому потоку обратиться к этому объекту Runnable, поэтому объект Runnable может быть уничтожен преждевременно. Для решения этой проблемы в библиотеке ZThreads организуется автоматический подсчет ссылок на задачи. Задача продолжает существовать до тех пор, пока счетчик ссылок на нее не упадет до нуля; в этот момент задача удаляется. Отсюда следует, что объекты задач всегда должны удаляться динамически, поэтому они не могут создаваться в стеке. Вместо этого задачи всегда создаются оператором new, как во всех примерах этой главы.

Нередко также приходится заботиться о том, чтобы другие объекты продолжали существовать до тех пор, пока они могут использоваться задачами. В противном случае эти объекты могут выйти из области видимости до завершения задач. Если это произойдет, попытки обращения к несуществующим объектам приведут к программным сбоям. Рассмотрим простой пример:

: СИ:Incrementer.cpp

Уничтожение объектов до завершения программных потоков

может вызвать серьезные проблемы.

{L} ZThread

#1 ncl ude <1ostream>

#1nclude zthread/Thread.h

#1nclude zthread/ThreadedExecutor.h

using namespace ZThread:

using namespace std:

class Count { enum { SZ = 100 }:



548 Глава И Многопоточное программирование

int n[SZ]: public: void increment О { forCint i = 0: i < SZ: i++) n[i]++:

class Incrementer : public Runnable {

Count* count: public:

Incrementer(Count* c) : count(c) {} void runO { for(int n = 100: n > 0: n--) {

Thread::sleep(250):

count->increment():

int mainO {

cout This will cause a segmentation fault! endl: Count count: try {

Thread t0(new Incrementer(&count)): Thread tKnew Incrementer(&count)): } catch(Synchronization Exception& e) { cerr e.whatO endl:

} III:-

Класс Count на первый взгляд кажется лишним, но если использовать вместо массива простую переменную int, то компилятор может разместить ее в регистре, и эта память останется доступной после выхода объекта Count из области видимости (пусть это и незаконно с технической точки зрения). Это затруднило бы обнаружение недействительных обращений к памяти. Конкретный результат зависит от компилятора и операционной системы; попробуйте заменить п простой переменной типа int и посмотрите, что произойдет. В любом случае, если Count содержит массив int, компилятор вынужден разместить данные в стеке, а не в регистре.

Incrementer - простая задача, использующая объект Count. Внутри main() задачи Incrementer выполняются достаточно долго для того, чтобы объект Count вышел из области видимости, и задачи попытались обратиться к несуществующему объекту. При этом происходит сбой программы.

Чтобы уладить проблему, необходимо позаботиться о том, чтобы любые объекты продолжали существовать в течение всего времени использования этих объектов задачами (если бы объекты не требовались разным задачам, их можно было бы включить непосредственно в класс задачи и тем самым связать жизненный цикл объекта с жизненным циклом задачи). Поскольку в нашем случае жизненный цикл объекта не должен определяться статической областью видимости, мы размещаем объект в куче. А чтобы объект не был уничтожен, пока он требуется другим объектам, следует применить механизм подсчета ссылок.

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



1 ... 172 173 174 [ 175 ] 176 177 178 ... 196

© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки.
Яндекс.Метрика