|
Программирование >> Разработка устойчивых систем
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, который автоматически организует подсчет ссылок и вызывает
|
© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки. |