/
Author: Труб И.И.
Tags: языки программирования программирование компьютерные технологии компьютерное моделирование язык программирования c++
ISBN: 5-469-00893-2
Year: 2006
Text
Илья Труб
ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ
МОДЕЛИРОВАНИЕ
НА C++
УЧЕБНЫЙ КУРС .
Освойте самый популярный язык
программирования на практике
Универсальная схема
построения моделирующих
программ на C++
Имитационные модели:
^ППТЕР
от проектирования
до реализации
brkFinishdnt
гае "normal
Постановка и решение
задач оптимизации
систем
СЕРИЯ
УЧЕБНЫЙ КУРС
£ППТЕР’
Илья Труб
ОБЪВЮНО-ОРИЕНТИРОВАННОЕ
МОДЕЛИРОВАНИЕ
НА C++
УЧЕБНЫЙ КУРС
к
^ППТЕР'
Москва Санкт-Петербург • Нижний Новгород Воронеж
Новосибирск • Ростов-на-Дону - Екатеринбург - Самара
Киев • Харьков Минск
2006
ББК 32.973-018.1я7
УДК 004.43(075)
Т77
Труб И. И.
Т77 Объектно-ориентированное моделирование на C++: Учебный курс. — СПб.:
Питер, 2006. — 411 с.: ил.
ISBN 5-469-00893-2
В книге рассмотрено применение объектно-ориентированной методологии программирования
на языке C++ в задачах моделирования систем и сетей обслуживания. Общая схема построения
моделирующих программ проиллюстрирована разнообразными примерами. Рассмотрены все
этапы проектирования моделирующей программы, приведены подробно прокомментированные
тексты программ, проанализированы результаты моделирования. Издание предназначено для
студентов и преподавателей, программистов, аспирантов, специалистов в области системного
анализа и моделирования.
ББК 32.973-018.1я7
УДК 004.43(075)
Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было
форме без письменного разрешения владельцев авторских прав.
Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как
надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не
может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за
возможные ошибки, связанные с использованием книги.
ISBN 5-469-00893-2
© ЗАО Издательский дом «Питер», 2006
Содержание
Об этой книге................................................12
Предисловие..................................................14
От издательства..............................................18
Часть 1. Теоретические основы
Глава 1. Аналитический и имитационный подходы
к моделированию.............................................20
1.1. Основные понятия теории очередей.
Классификация Кендалла—Башарина.............................21
1.2. Характеристики функционирования и производительности....26
1.3. Простейший пример аналитической модели.
Вычисление характеристик ...................................28
1.3.1. Абсолютный приоритет (PR) ............................31
1.3.2. Относительный приоритет...............................31
1.4. Имитационное моделирование систем.......................33
1.4.1. Статистическая обработка результатов моделирования....39
1.4.2. На каком языке моделировать?..........................42
Выводы.......................................................53
Глава 2. Краткий обзор объектных возможностей языка C++......54
2.1. Классы расширяют C++новыми типами данных .... 55
2.2. Объявление классов в C++ ...............................56
2.3. Дружественные функции и классы..........................59
2.4. Наследование классов в C++ .............................60
2.5. Виртуальные функции . . 61
2.6. Абстрактные классы.............. . ....................64
2.7. Операции new и delete...................................68
6 Содержание
2.8. Перегрузка операций......................................68
2.9. Обработка исключений ....................................72
2.10. Шаблоны.................................................74
Выводы........................................................77
Глава 3. Генерация вероятностных распределений ...............78
3.1. Метод обратной функции...................................79
3.2. Равномерное распределение................................81
3.3. Уравнение F 1 (и) = х имеет решение, являющееся
элементарной функцией....................................... 82
3.4. Функция F_,(u) не является элементарной..................84
3.5. Функция F(x) не является элементарной....................87
3.6. Распределения фазового тина. Композиция ГСЧ..............91
Выводы........................................................94
Глава 4. Простейший пример: моделирование
микроволновой печи.............................................95
4.1. Описание.................................................96
4.2. Проектирование класса Печь...............................96
4.3. Печью управляет человек..................................99
4.4. Человека имитирует объект ......... 100
Выводы ......................................................104
Глава 5. Общая схема построения объектных
моделирующих программ ........................................105
5.1. Объектный анализ .................................... 106
5.2. Модельное время...................................... 107
5.3. Основной цикл.......................................... 108
5.4. Параллельное моделирование и порядок обхода объектов ...109
5.5. Сбор статистики ............ . . .....................114
5.6. Пример моделирования на C++с помощью событийной схемы .... 114
Выводы ......................................................118
Часть 2. Моделирование и анализ экспериментов
на практике
Глава б. Регулярный входной поток, отсутствие очередей, естественный
отсчет времени: моделирование больничной палаты . . . 120
6.1. Описание.............................................. 121
6.2. Классы и объекты........................................121
6.3. События и методы .......................................123
Содержание 7
6.4. Анализ результатов.........................................129
Выводы .........................................................137
Глава 7. Последовательное обслуживание с блокировками
и ограниченным буфером: производственная
поточная линия..................................................138
7.1. Описание системы ..........................................139
7.2. Модельное время............................................140
7.3. Классы и объекты...........................................141
7.4. События и методы ..........................................144
7.5. Анализ результатов....................................... 151
7.6. Аналитический подход.......................................160
Выводы .........................................................163
Задание для самостоятельной работы .............................163
Глава 8. Последовательное обслуживание с возвращениями:
производственная линия с пунктами технического контроля
и настройки.....................................................164
8.1. Описание системы ..........................................165
8.2. Модельное время............................................166
8.3. Классы и объекты...........................................166
8.3.1. Класс Пункт Контроля.....................................168
8.3.2. Класс Пункт Настройки....................................168
8.4. События и методы ..........................................169
8.5. Анализ результатов.........................................177
Выводы ......................................•..................181
Задания для самостоятельной работы..............................182
Глава 9. Замкнутая система с неоднородными каналами:
моделирование грузовых автоперевозок............................183
9.1. Описание системы ..........................................184
9.2. Модельное время............................................185
9.3. Классы и объекты...........................................185
9.3.1. Класс Heavy Саг..........................................187
9.3.2. Класс Fuller.............................................187
9.3.3. Класс Emptier............................................188
9.4. События и методы ..........................................188
9.5. Анализ результатов.........................................201
Выводы .........................................................205
Задания для самостоятельной работы..............................205
8 Содержание
Глава 10. Замкнутая система с раздельными очередями
и приоритетами: моделирование работы карьера...........207
10.1. Описание системы...........................................208
10.2. Модельное время .......................................... 209
10.3. Классы и объекты...... 209
10.4. События и методы......................................... 210
10.5. Анализ результатов.........................................221
Выводы ..........................................................226
Задания для самостоятельной работы...............................226
Глава 11. Система управления запасами
с неудовлетворенным спросом............................228
11.1. Описание системы...........................................229
11.2. Модельное время .................................. ........ 229
11.3. Классы и объекты.......................................... 230
11.4. События и методы....................................... 231
11.5. Анализ результатов . 237
Выводы ..........................................................244
Задания для самостоятельной работы...............................244
Глава 12. Очереди с разнотипными заявками:
работа порта.....................................................245
12.1. Описание системы...........................................246
12.2. Модельное время ...........................................246
12.3. Классы и объекты................... . . . ✓................247
12.3.1. Класс Tanker......................................... 248
12.3.2. Производный класс Tanker4................................249
12.3.3. Класс Tug ...............................................249
12.3.4. Класс Port .... 250
12.4. События и методы . 250
12.5. Анализ результатов.........................................262
Выводы ........................................................ 265
Задания для самостоятельной работы ... 265
Глава 13. Прерывание обслуживания с возвратом в очередь:
моделирование работы станка с поломками..........................266
13.1. Описание системы........................ .... .........267
13.2. Модельное время ...............j...........................268
13.3. Классы и объекты...........................................268
Содержание 9
13.4. События и методы....................................... 270
13.5. Анализ результатов........................................276
Выводы ...................................................... 278
Задания для самостоятельной работы..............................279
Глава 14. Сетевое планирование и анализ проектов................280
14.1. Описание системы..........................................281
14.2. Модельное время ........................................ 282
14.3. Классы и объекты......................................... 284
14.4. События и методы........................................ 285
14.5. Анализ результатов........................................290
14.6. Criticality и cruciality ............................... 292
14.7. Обойдемся без @RISK?......................................295
Выводы..........................................................301
Задания для самостоятельной работы..............................301
Глава 15. Моделирование расписаний: участок дороги
с односторонним движением.......................................302
15.1. Описание системы..........................................303
15.2. Модельное время ..........................................303
15.3. Классы и объекты..........................................304
15.4. События и методы..........................................305
15.5. Немного фантазии..........................................310
15.6. Как оптимизировать?.......................................312
15.7. Метод покоординатного спуска..............................314
15.8. Анализ результатов........................................315
Выводы .........................................................318
Задания для самостоятельной работы..............................318
Глава 16. Раздельные очереди ограниченной длины с переходами
заявок: банк для водителей со сменой
подъездных полос................................................319
16.1. Описание системы..........................................320
16.2. Модельное время ..........................................320
16.3. Классы и объекты..........................................321
16.4. События и методы..........................................322
16.5. Анализ результатов........................................329
Выводы ....................................................... 334
Задания для самостоятельной работы..............................334
10 Содержание
Глава 17. Групповое обслуживание с несколькими этапами
и двойной очередью: работа оптового магазина.........335
17.1. Описание системы........................................336
17.2. Модельное время ........................................336
17.3. Классы и объекты........................................336
17.4. События и методы........................................339
17.5. Анализ результатов......................................347
Выводы .......................................................351
Задания для самостоятельной работы............................352
Глава 18. Дискретно-непрерывное моделирование:
работа камерной печи.................................353
18.1. Описание системы........................................354
18.2. Как моделировать систему?...............................354
18.3. Модельное время ........................................356
18.4. Классы и объекты........................................357
18.5. События и методы........................................358
18.6. Анализ результатов......................................365
Выводы .......................................................368
Задания для самостоятельной работы............................369
Глава 19. Замкнутая система с регулирующим объектом:
танкерный флот, обслуживающий
нефтеочистительную установку..................................370
19.1. Описание системы........................................371
19.2. Модельное время ........................................371
19.3. Классы и объекты........................................372
19.4. События и методы........................................374
19.5. Анализ результатов......................................383
Выводы .......................................................385
Задания для самостоятельной работы............................386
Приложение 1. Реализация класса PaLata
с помощью контейнера List из библиотеки STL
(к главе б)...................................................387
Приложение 2. Вывод условия
существования стационарного режима (к главе 8) .... 390
Содержание 11
Приложение 3. Реализация класса Emptier с помощью контейнера
Priority Queue из библиотеки STL (к главе 10)................392
Приложение 4. Реализация time-driven-подхода
. для календарно-событийной программы (к главе 5) . . . . 395
Список литературы и источников из Интернета..................400
Дополнительная литература....................................406
Алфавитный указатель.........................................408
Об этой книге
Практика давно доказала высокую эффективность применения моделирования в
задачах анализа и синтеза систем самого разного назначения. Разработаны мето-
дология построения моделирующих комплексов и инструментальные средства
моделирования; построен теоретический фундамент модельного эксперимента.
Все эти «технические детали» достаточно подробно описаны в научной и учеб-
но-методической литературе. Казалось бы, остается только изучить и понять ме-
ханизм их работы, и исследователю обеспечено обладание одним из самых мощ-
ных методов проектирования.
Однако, к сожалению (а может быть к счастью), это не так: задача моделирова-
ния во многом остается задачей творческой. Элементы творчества присутствуют
на начальных этапах моделирования, когда формируется замысел модели и ре-
шается задача оценки ее адекватности по отношению к еще не созданной систе-
ме. На грани науки и искусства находится, несмотря на его «индустриализа-
цию», программирование модели.
Если техническим приемам и основам теории можно обучить традиционными
методами, то творчеству можно научить только с помощью образцов творчества,
демонстрируя и поясняя их. В этом — суть данной книги.
Вероятно, в ней есть недостатки, которые обнаружит внимательный читатель.
Но в ней есть главное достоинство: она обобщает личный творческий опыт авто-
ра как в области программирования, так и в области исследования моделей. Чи-
татель найдет в ней примеры того, как решаются проблемы моделирования на
всех этапах создания и применения моделей, начиная от проектирования клас-
сов и выделения состояний и событий и заканчивая планированием имитацион-
ного эксперимента и оценкой достоверности полученных результатов.
В книге изложена методология объектно-ориентированного программирования
применительно к задачам дискретно-событийного моделирования. Это опреде-
ляет широкий читательский адрес — от начинающих программистов, которые
приступают к изучению языка C++, до опытных программистов-практиков, ко-
торые могут с большой пользой для себя сопоставить свои взгляды на объектно-
ориентированное программирование и методологию его применения для моде-
лирования с подходом автора. По моему мнению, многочисленные примеры,
приведенные в книге, убедительно доказывают, что, не имея под рукой никаких
специальных программных пакетов и библиотек, можно в короткие сроки соз-
дать достаточно эффективную модель.
Содержание книги охватывает фрагменты нескольких дисциплин федеральной
компоненты государственного стандарта по направлению «Информатика и вы-
числительная техника».
Древе Юрий Георгиевич,
доктор технических наук, профессор кафедры «Управляющие интеллек-
туальные системы» Московкого инженерно-физического института, член
научно-методической комиссии УМО Минобразования РФ
Моей жене Наталье
Предисловие
Думается, нет необходимости доказывать выгоды и преимущества объектно-ори-
ентированного подхода к проектированию и разработке программного обеспече-
ния. На эту тему написаны сотни книг и тысячи статей, во многих из которых
вопрос изложен блестяще. Еще лучше говорят сами за себя результаты труда та-
лантливых программистов-разработчиков. Поэтому сейчас при изложении объ-
ектной концепции в целом вряд ли уже можно сказать что-то новое или сделать
это лучше, чем общепризнанные мировые корифеи, такие, например, как Гради
Буч или Бьерн Страуструп.
Но человек, освоивший новый для себя инструмент, стремится как можно ско-
рее попробовать его в работе, в настоящем деле, чтобы, во-первых, убедиться в
том, что инструмент этот действительно хорош, а во-вторых — ощутить собст-
венный рост как специалиста, как профессионала, расширившего диапазон сво-
их возможностей.
По этой причине большое значение приобретает рассмотрение сфер приложения
объектного программирования и предметных областей, в которых его приме-
нение может оказаться наиболее эффективным и имеет явные преимущества по
сравнению, например, с процедурным подходом. Как всякое мощное и, кроме
того, тонкое оружие, концепция объектно-ориентированного программирования
(ООП) способна принести пользу только тогда, когда находится в руках грамот-
ного человека, полностью осознающего, что и как он хочет сделать и что полу-
чить в итоге, и применяется прицельно, по назначению, там, где это действительно
необходимо. Не всегда ООП лучше обычного процедурного программирования.
Как известно, принципиальное различие двух подходов заключается в том, что счи-
тать первичным — объект или процесс. Если идейным стержнем программы являет-
ся процесс, вряд ли применение ООП может принести какую-то пользу и повысить
наглядность кода, эффективность разработки и сопровождения. К программам, где
первичен процесс, относится, например, большая часть программного обеспечения
по реализации различных численных методов, особенно итерационных. Главным
здесь является процесс счета, а точки разностной схемы вряд ли могут подойти на
роль объектов. Именно поэтому большой популярностью у математиков до сих пор
пользуются библиотеки численных методов, написанные на Фортране — классиче-
ском «языке процессов». Однако существуют такие разделы прикладного про-
граммного обеспечения, где высокая эффективность и наглядность применения
Предисловие 15
объектных средств разработки не вызывает сомнений. Примером является имита-
ционное моделирование сложных систем, в частности, систем с очередями.
Применению ООП в этой области и посвящена настоящая книга. Большое коли-
чество аналитических результатов, полученных в теории массового обслужива-
ния, не удовлетворяет и малой доли потребностей практики. В условиях идеали-
зированной математической модели эти результаты носят скорее объясняющий,
чем предсказательный характер. При ослаблении же ограничений на исходные
данные (произвольные распределения вероятностей, нестандартные дисципли-
ны обслуживания) объем вычислений, необходимых для получения аналитиче-
ского решения, становится вполне сравнимым с затратами на проведение имита-
ционного эксперимента, а зачастую намного превосходит их.
Но основная ценность имитационного моделирования заключается в том, что
здесь можно отбросить практически все ограничения и моделировать систему в
условиях, предельно близких к реальности. Поэтому только разумное сочетание
аналитического и имитационного подходов к моделированию позволяет полно-
стью выявить характерные особенности сложной системы, ее «узкие места»,
и выработать обоснованные рекомендации по ее синтезу.
Как показано в главах 4 и 5, применение ООП позволяет построить эффектив-
ную и предельно общую схему имитационного моделирования сложных систем
обслуживания с очередями. В этой прикладной области объектный подход в
полной мере обнаруживает те свои преимущества, ради достижения которых он,
собственно, и был разработан: естественность и наглядность процесса програм-
мирования, первичность данных по отношению к процессам и главное — лег-
кость сопровождения и дальнейшей модификации моделирующей программы,
если система претерпевает изменения. В одной из первых работ [58], где было
дано формальное определение понятия «объектно-ориентированное моделиро-
вание», сказано: «Когда говорят об объектно-ориентированной концепции опи-
сания систем, то подразумевают программное обеспечение в виде совокупности
дискретных объектов, заключающих в себе как структуру данных, так и реализа-
цию поведения. С другой стороны, имитационную модель можно рассматривать
как некоторое множество сущностей, взаимодействующих друг с другом. С этой
точки зрения можно даже заключить, что имитационная модель всегда объектно
ориентирована, так как объектная парадигма — это вполне естественный способ
моделирования, если отобразить реальные сущности на объекты». Конкретные
примеры на самые разнообразные темы, подробно рассмотренные во второй час-
ти книги, подтверждают эти слова.
Основу книги составили материалы различных лекционных и факультативных
курсов, которые автор читал на факультете вычислительной техники и инфор-
матики Донецкого государственного технического университета, факультете со-
временных компьютерных информационных технологий Донецкого института
проблем искусственного интеллекта (Украина, 1995—1997 гг.) и факультете ин-
формационных технологий Сургутского государственного университета (1997-
2003 гг.), лабораторного практикума по ООП, проводимого в этих вузах, а также
его собственных исследований, проводимых на протяжении последних лет. Со-
держание книги имеет ярко выраженную практическую направленность. Теоре-
тические основы имитационного моделирования, включая статистический ана-
16 Предисловие
лиз, исчерпывающе изложены в таких превосходных книгах, как [60], и недавно
вышедшей в русском переводе в серии «Классика CS» [17]. Поэтому в данной
книге основной акцент сделан на то, как смоделировать объектными средствами
конкретную систему. Задачи, рассмотренные в главах 6-19, носят учебный ха-
рактер. Их условия взяты из известной книги [33], в которой они решаются с по-
мощью иного инструмента моделирования, ныне устаревшего. Использование
C++ позволило решить эти задачи в обобщенной постановке и поставить ряд но-
вых задач исследовательского характера.
Автор постарался достичь такого уровня детализации изложения, чтобы, с одной
стороны, не перегружать его излишними техническими подробностями, обычно
являющимися сугубо внутренним делом разработчика, а с другой — сделать все
имитационные эксперименты и их результаты полностью воспроизводимыми
читателем, имеющим в своем распоряжении компилятор C++. Для каждой зада-
чи подробно описан и по возможности обоснован процесс объектного проекти-
рования, приведены прокомментированные протоколы классов, составлен план
проведения имитационных экспериментов и проанализированы полученные
численные результаты. Исследователь системы может поставить гораздо боль-
ше вопросов, чем рассмотрено в книге (средняя длина очереди, вероятность от-
каза и т. д.). Автор и не преследовал цель досконально исследовать каждую из
рассматриваемых систем, так как в этом случае объем книги стал бы невообрази-
мо большим. Он лишь стремился показать внутреннюю логику объектного про-
ектирования модели, реализацию взаимосвязи объектов и других особенностей
системы, разработать тот «ствол», на который впоследствии легко можно «наве-
сить» путем локализованного изменения кода любые вопросы, касающиеся пове-
дения системы (например, не только вычисление средних длин очереди и времен
пребывания заявок в системе, но и оценку их дисперсии). Часть таких вопросов
предложена читателям в виде упражнений для самостоятельной работы. В боль-
шинстве случаев они носят проблемный характер, и далеко не всегда автор зна-
ет, что должно получиться в итоге.
Реализация изложенной в книге объектной концепции моделирования сложных
систем возможна на любом языке, допускающем работу с объектами: C++, Java,
Delphi, Cache Object Script и др. Автор выбрал C++ по причине его фундамен-
тальности, а также значительного опыта программирования на этом языке.
Представленные в книге примеры программ не ориентированы на какую-либо
конкретную реализацию C++ и могут быть откомпилированы с помощью любо-
го компилятора, реализующего стандарт ANSI/ISO C++, например пакета GNU
g++ в среде ОС Linux. Сам автор при работе над книгой использовал среду Dev-
С++ (Bloodshed Software, www.bloodshed.net), version 4.9.8.0 с базовым компиля-
тором g++ version 3.2. Для построения графиков и гистограмм использовался па-
кет Grapher, version 2.04 (Golden Software Inc.), для выполнения некоторых рас-
четов — пакет Maple 6.01 (Waterloo Maple Inc.), для редактирования рисунков —
Adobe Photoshop 5.5 (Adobe Systems, Inc.).
Главы 6 и 10 дополнены приложениями, демонстрирующими альтернативную реа-
лизацию имитационных моделей с помощью библиотечных классов STL — List
(приложение 1) и Priority Queue (приложение 3). В базовых вариантах программ
использован шаблон, разработанный автором для обработки списков. Читателю
предлагается самому оценить преимущества использования контейнерных классов
Предисловие 17
STL, потому что именно для программ имитационного моделирования систем
с очередями этот инструментарий мог бы быть очень полезен. Однако наряду с
преимуществами классы STL имеют и определенные недостатки. К ним относятся,
во-первых, заранее оговоренные разработчиками особенности алгоритмов обра-
ботки контейнеров и дисциплин обслуживания, которые не всегда совпадают с тем,
что нам нужно (см. приложение 3), во-вторых, существенное увеличение (в рас-
смотренных примерах — от 100 до 150 Кбайт) размера.исполнимого .ехе-файла.
Анализ того, какие именно классы STL могли бы быть использованы в прочих мо-
делях, приведенных в книге, и дальнейшее встраивание их в программный код яви-
лись бы прекрасным практикумом по освоению STL, но это уже тема другой книги.
ПРИМЕЧАНИЕ В листингах приводимых в книге программ в большом количестве используются
операции выделения и освобождения памяти. Для сокращения объема листин-
гов автор не включил в них код проверки успешного завершения этих операций
и обработки исключительных ситуаций, возникающих при невозможности выде-
лить или освободить заказанную память. Такое усовершенствование кода без
труда может быть выполнено заинтересованным читателем.
Ограниченный объем книги не позволил включить в нее ряд интересных задач
из различных разделов теории вычислительных систем, таких как организация
веб-кластеров и оптимизация выполнения запросов, маршрутизация, потоки
данных, программные каналы, кэш-память, конвейерные вычисления и др. Вви-
ду сложности этих задач имитационные модели являются для них одним из ос-
новных инструментов исследования, позволяющим получить содержательные
результаты. Автор рассчитывает посвятить этой тематике отдельную книгу.
Для понимания материала, изложенного в книге, читатель должен иметь опре-
деленную подготовку, впрочем, больших знаний не требуется. Ему достаточно
ознакомиться с основами ООП и теории массового обслуживания, а также полу-
чить некоторые начальные сведения из курсов математического анализа, числен-
ных методов и теории вероятностей в объеме, предлагаемом в любом техниче-
ском вузе. Желателен также хотя бы небольшой практический опыт разработки
и отладки программ на C++, в частности, готовность работать с указателями
и массивами указателей на объекты.
Автор надеется, что книга будет полезна студентам старших курсов и аспиран-
там, использующим объектный подход для разработки прикладного программ-
ного обеспечения, а также специалистам в области системного анализа и модели-
рования систем вероятностными методами.
Несмотря на то что я являюсь единственным автором книги, многие люди в той
или иной мере способствовали ее появлению. Мне хотелось бы выразить искрен-
нюю благодарность моей жене Наталье — прекрасному математику и замечатель-
ному человеку, за ее понимание, терпение, моральную поддержку и полезные
советы; своему недавно отметившему 80-летие Учителю, ветерану Великой Оте-
чественной войны, профессору Донецкого национального технического универси-
тета (ДонНТУ, как этот университет называется в настоящее время) Льву Петро-
вичу Фельдману, благодаря которому я сформировался как специалист; моим
дорогим родителям; моим бывшим студентам и аспирантам — Ольге Цукановой,
18 Предисловие
Наталье Пановой, Альмире Садыковой, Анастасии Сафиной, Олесе Киселевой
и Антону Медведю, которые в разные годы принимали участие в работе над рас-
смотренными в книге задачами. Огромную помощь оказал мне великолепный зна-
ток компьютерной графики инженер-программист Антон Юрьевич Николаевич,
без которого я никогда не справился бы так быстро с подготовкой векторных ри-
сунков. Моя коллега по работе в ДонНТУ Татьяна Васильевна Михайлова помог-
ла мне в уточнении ссылок на первоисточники, которыми я не располагал.
Я благодарен рецензентам книги — проректору по научной работе Сургутского
государственного педагогического института профессору Сергею Арнольдовичу
Инютину и профессору Московского инженерно-физического института Юрию
Георгиевичу Древсу, которые, несмотря на крайнюю занятость, прочитали руко-
пись и дали очень ценные для меня отзывы.
Эта книга никогда бы не увидела свет без поддержки таких замечательных лю-
дей, как руководитель направления Издательского дома «Питер» Анатолий Ни-
колаевич Адаменко и заведующий кафедрой АСОиУ Астраханского государст-
венного технического университета Валерий Викторович Лаптев.
Анатолий Николаевич был первым рецензентом присланных в издательство ма-
териалов, на протяжении нескольких месяцев мы с ним решали многочисленные
вопросы, связанные с принятием книги в план издательства. Его положительная
оценка, помощь, доброжелательность и оптимизм придавали уверенность в успе-
хе начатого дела.
Валерий Викторович, будучи блестящим и общепризнанным знатоком C++
(и многих других областей программирования), любезно согласился взять на
себя нелегкий труд рецензента и научного редактора. Его ценные замечания и
полезные советы позволили значительно улучшить отдельные места программ-
ного кода, внести четкость и ясность в формулировки и, как результат, повысить
качество книги в целом.
Большая благодарность всем сотрудникам издательства, принимавшим участие
в подготовке книги к изданию, в особенности редактору — Нине Рощиной.
Наконец, было бы несправедливо обойти вниманием замечательное предприятие
ООО «Сургутгазпром», где я имею честь работать. Проявляя настоящую заботу
о своих сотрудниках, оно предоставляет им все возможности для профессио-
нального роста и творческого труда.
От издательства
Ваши замечания, предложения и вопросы отправляйте по адресу электронной
почты comp@piter.com (издательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
Все исходные тексты, приведенные в книге, вы можете найти по адресу
http://www.piter.com/download.
Подробную информацию о наших книгах вы найдете на веб-сайте издательства:
http://www.piter.com.
Часть I
Теоретические основы
Глава 1
Аналитический
и имитационный подходы
к моделированию
Основные вопросы, рассматриваемые
в данной главе:
□ Какие системы мы будем
моделировать: примеры,
классификация, дисциплины
обслуживания
□ Что вычисляется при моделировании:
типовые характеристики систем
□ Аналитический подход
к моделированию: сильные и слабые
стороны
□ Имитационная модель: сильные
и слабые стороны
□ Языки и среды имитационного
моделирования: краткий обзор
1.1. Основные понятия теории очередей. Классификация Кендалла—Башарина 21
1.1. Основные понятия теории очередей.
Классификация Кендалла—Башарина
Дадим общее описание и классификацию систем с очередями. Читатель, знако-
мый с теорией очередей в объеме любой из книг [20], [35], [57], [69], [83], [88]
или какой-либо другой, может пропустить или бегло пролистать первые два раз-
дела.
Типичная система массового обслуживания (СМО) состоит из буфера для ожи-
дания в очереди конечного или бесконечного размера и одного или нескольких
одинаковых обслуживающих устройств, которые будем для краткости называть
серверами. Сервер в определенный момент времени может обслуживать только
одну заявку и, следовательно, быть в занятом или свободном состоянии. Если в
момент поступления новой заявки все серверы заняты, заявка ставится в оче-
редь, если в буфере есть свободные места, и ждет своего выполнения. Когда об-
служивание заявки на сервере завершается, одна из заявок, находящихся в оче-
реди, выбирается для обслуживания. Выбор производится в соответствии с
некоторой дисциплиной, или расписанием. Элементарная теория очередей рас-
сматривает входные потоки, описываемые последовательностью случайных ве-
личин — интервалов времени между прибытиями {Л], А2,...}. Эти величины пред-
полагаются независимыми и одинаково распределенными и образуют в пределе
случайный процесс, известный как процесс восстановления. Предметом изуче-
ния таких процессов является количество переходов из одного состояния в дру-
гое, произошедших в течение заданного времени. Функция распределения для
входного потока может быть непрерывной или дискретной. Средняя длитель-
ность интервала между прибытиями заявок обозначается Е[Л] = ТА и является
величиной, обратной к средней интенсивности входного потока, обозначаемой X,
X =1/7; (рис. 1.1).
Рис. 1.1. Схема системы с очередью и одним сервером
Наиболее изученным и удобным для расчетов является экспоненциальное рас-
пределение интервалов, соответственно, входной поток в этом случае имеет рас-
пределение Пуассона. Последовательность (В}, В2,...} длительностей обслуживания,
называемых еще длинами заявок, также должна быть описана. Будем считать и ее
множеством независимых случайных величин с произвольной функцией распре-
деления. Среднее время обслуживания обозначим Е[В] - Тв, обратная ему вели-
чина — интенсивность обслуживания р: р = 1/Ув. Вот простейшие примеры си-
стем с очередями.
22 Глава 1. Аналитический и имитационный подходы к моделированию
О Супермаркет. Как долго покупатели должны дожидаться своей очереди к кассе?
Что произойдет в час пик? Достаточно ли кассовых аппаратов?
О Производственный цех. На станках изготовляют несколько типов деталей.
Какой эффект даст добавление еще одного станка? Следует ли назначать
приоритеты разным типам деталей?
О Передача данных. В сетях передачи данных пакеты пересылаются от одного
маршрутизатора к другому. На каждом из них пакеты могут быть буферизо-
ваны, если немедленная их передача по каким-то причинам невозможна. Если
буфер заполнен, пакеты теряются. Какова доля потерянных пакетов? Каков
оптимальный размер буфера?
О Автостоянка. Администрация супермаркета планирует построить перед ним
площадку для парковки автомобилей. Какую площадь следует под нее отвес-
ти?
О Центр приема звонков в страховой компании. Дает консультации по телефо-
ну относительно условий страхования. Сотрудники центра поделены на груп-
пы, каждая группа обслуживает клиентов только из определенного региона.
Как долго придется ждать клиенту появления свободного оператора? Доста-
точно ли число телефонных линий? Хватает ли операторов? Удачно ли поде-
лена на регионы охватываемая компанией географическая местность?
О Сбор пошлины. Водители должны заплатить пошлину за проезд через мост.
Достаточно ли пунктов для сбора установлено перед мостом?
Процессы восстановления, лежащие в основе приведенных примеров, хорошо изу-
чены и почти всегда могут быть исчерпывающе описаны аналитически. Однако
существуют реальные ситуации, когда поток запросов на обслуживание не обра-
зует процесса восстановления. Прибытие новых заявок может зависеть от теку-
щей длины очереди, состояния сервера, алгоритмов маршрутизации. Это особен-
но характерно для современных вычислительных систем, реализующих весьма
нетривиальные дисциплины обслуживания. Приведем примеры таких систем
[53], [56], [57], [70], [87].
Рассмотрим множество из N станков, функционирующих независимо друг от
друга. В случайный момент времени любой из них может выйти из строя и по-
требовать некоторого времени на ремонт. После завершения ремонта станок во-
зобновляет работу. Предполагается, что одновременно ремонтироваться могут
только т машин, т < N. Ставится задача найти распределение времени безава-
рийной работы всей системы.
Рассмотрим разгрузочную установку, к которой прибывают железнодорожные
составы с углем с различных шахт. В определенный момент времени установка
может обслуживать только один состав, и время разгрузки одного состава рас-
пределено экспоненциально с параметром ц. Иногда во время работы разгрузчик
выходит из строя. Время его безаварийной работы распределено экспоненциаль-
но с параметром т), а время ремонта — экспоненциально с параметром 0. Разгруз-
ка состава, прерванная поломкой разгрузчика, возобновляется сразу же по завер-
шении ремонта. Пустой состав возвращается на шахту для новой загрузки углем.
Время движения поезда до шахты и обратно распределено экспоненциально
1.1. Основные понятия теории очередей. Классификация Кендалла—Башарина 23
с параметром X. Требуется определить, как распределено количество составов,
полностью разгруженных за определенное время?
S рабочих станций используют сервер для обработки транзакций. Время, тре-
бующееся рабочей станции на формирование запроса к серверу, называется вре-
менем обдумывания. Время от момента генерации транзакции до полного ее
завершения сервером (и одновременно сообщения этого факта пользователю ра-
бочей станции) называется временем отклика.
Распределение времени отклика является одной из важнейших характеристик
таких систем — оно показывает, сколько времени пользователю придется ожи-
дать ответ на посланный им запрос. С решения этой задачи [44], собственно,
и началось применение методов теории очередей в теории вычислительных сис-
тем (рис. 1.2).
Терминалы
Рис. 1.2. Замкнутая вычислительная система с одним сервером
Как нетрудно заметить, рассмотренные ранее три примера имеют общее свойст-
во: поток запросов порождается конечным числом источников нагрузки. Новые
запросы на обслуживание могут исходить только от незанятого источника, кото-
рый не ожидает в данный момент завершения обслуживания своего предыдуще-
го запроса. Свободный источник генерирует запросы независимо от состояния
других источников. Модели с конечным числом источников, как правило, более
трудны для анализа, чем модели с бесконечным их числом.
Существует множество моделей для расчета производительности систем с очере-
дями в стационарном режиме. Стационарный режим — это такой режим работы
системы, при котором первые и вторые моменты характеризующих систему слу-
чайных процессов (текущее число заявок в системе, длительности пребывания
заявок в очереди и др.) не зависят от времени, то есть отсутствует тренд средних
24 Глава 1. Аналитический и имитационный подходы к моделированию
и дисперсий процессов. Модели учитывают предположение об источниках на-
грузки, распределение времени обслуживания и применяемую дисциплину. В за-
висимости от содержательного наполнения системы можно при ее описании
употреблять термины запрос, заявка, требование, потребитель, клиент, машина,
устройство, сервер, сообщение, работа. Мы будем широко пользоваться этими
терминами при моделировании систем во второй части книги.
При описании элементарных систем с очередями общепринятой стала нотация
Кендалла—Башарина. В этой нотации система массового обслуживания (СМО)
кодируется пятью позициями: A/B/m/K/N, где А обозначает распределение ин-
тервалов во входном потоке, В — распределение времени обслуживания, т — ко-
личество серверов, К — емкость системы, то есть максимально возможное число
заявок в ней, N — количество источников нагрузки. Поля А нВ могут принимать
следующие значения: М — экспоненциальное распределение (буква М симво-
лизирует марковское свойство или свойство отсутствия последействия); Ек —
^-фазное распределение Эрланга; Hk — ^-фазное гиперэкспоненциальное рас-
пределение; D — детерминированная величина; G — произвольное распределе-
ние; GI — произвольное распределение с независимыми интервалами входного
потока.
В связи с возрастающей сложностью высокопроизводительных вычислительных
систем все больший интерес представляют модели, в которых входные потоки
заявок не являются независимыми. Примерами таких потоков, к которым уже
нельзя применить символ GI, являются пуассоновские с марковским случайным
управлением [59], [66], [70], [72].
Дисциплина, иди стратегия, обслуживания задает правило, согласно которому
заявка выбирается из очереди на обработку в тот момент, когда сервер становит-
ся доступен. Вот перечень наиболее распространенных дисциплин:
О FCFS (First Come — First Served) — обслуживание в порядке поступления.
Предполагается, что эта дисциплина используется по умолчанию, если ниче-
го иного не сказано;
О LCFS (Last Come — First Served) — из очереди выбирается последняя при-
бывшая заявка;
О SIRO (Service in Random Order) — заявка выбирается из очереди случайным
образом;
О RR (Round Robbin) — обслуживание заявки прерывается, если оно не завер-
шено в течение выделенного кванта времени, и заявка возвращается в оче-
редь, следующая заявка выбирается в соответствии с FCFS (рис. 1.3). Это
действие повторяется до тех пор, пока обслуживание заявки не завершится;
О SIF (Shortest Job First) — первой обслуживается заявка наименьшей длины.
Эта дисциплина предполагает, что время обслуживания каждой заявки из-
вестно заранее и минимизирует среднее время ожидания. Для первых трех
дисциплин среднее время ожидания при прочих одинаковых параметрах одно
и то же;
О PS (Processor Sharing) — предельный случай Round Robbin, когда размер
кванта стремится к нулю. Все заявки обслуживаются одновременно, очередь
1.1. Основные понятия теории очередей. Классификация Кендалла—Башарина 25
как таковая отсутствует. Ресурс сервера делится поровну между всеми заяв-
ками, поэтому время обслуживания увеличивается;
О IS (Infinite Server) — число серверов настолько велико, что очередь никогда
не образуется;
О Static Priorities — выбор заявки зависит от приоритетов, которые назначают-
ся заявкам. Внутри класса заявок с одинаковым приоритетом применяется
дисциплина FCFS;
О Dynamic Priorities — выбор зависит от динамических приоритетов, которые
меняются с течением времени;
О Preemption — прерывание обслуживания заявки, если поступила заявка с бо-
лее высоким приоритетом, и немедленный захват сервера.
Рис. 1.3. Дисциплина обслуживания RR
Дисциплины Static Priorities и Dynamic Priorities называют также относитель-
ным приоритетом, a Preemption — абсолютным приоритетом. В случае абсолют-
ных приоритетов возможны три варианта дальнейших действий для прерванной
заявки:
О обслуживание возобновляется с места прерывания;
О обслуживание начинается сначала, длина заявки сохраняется;
О обслуживание начинается сначала, длина заявки разыгрывается заново.
В качестве примера нотации Кендалла—Башарина рассмотрим выражение
M/G/X - LCFS preemptive resume (PR),
описывающее СМО с пуассоновским входным потоком, произвольным распре-
делением длин заявок и единственным сервером. Дисциплина выбора заявок из
очереди — LCFS, где вновь прибывшая заявка прерывает обработку текущей
и заменяет ее на сервере. Выполнение прерванной заявки возобновляется толь-
ко после полного завершения выполнения всех заявок, прибывших после нее.
26 Глава 1. Аналитический и имитационный подходы к моделированию
Заметим, что такая схема в теории массового обслуживания математически яв-
ляется плодотворной и используется, например, для вывода уравнения распре-
деления длительности периода занятости в форме преобразования Лапласа [20].
Строка M/G/Y/K/N обозначает систему с конечным числом источников пуас-
соновского потока, произвольным распределением длин заявок и единственным
сервером. В ней циркулирует N заявок, и они принимаются к обслуживанию в
том и только в том случае, если число заявок на сервере не превышает К. Отверг-
нутые заявки возвращаются к своим источникам и начинают новый период об-
думывания. Если К = N, четвертая позиция в нотации оставляется пустой —
M/G/Х/ /N.
Наконец, заметим, что простейшая и наиболее хорошо изученная система соот-
ветствует нотации М/М/\. Такая система описывается пуассоновским входным
потоком, экспоненциальным временем обслуживания и имеет единственный
сервер. Нотации же GI/G/т соответствует предельно общая система с независи-
мым произвольно распределенным входным потоком, произвольно распределен-
ной длительностью обслуживания и произвольным количеством серверов.
1.2. Характеристики функционирования
и производительности
Поскольку СМО относится к динамическим системам, показатели ее произво-
дительности меняются со временем. Однако если стационарный режим сущест-
вует, основные показатели можно считать неизменными и по их значениям
оценивать, насколько хорошо система работает. Существование стационарного
режима означает, что все переходные процессы в системе завершены, ведет она
себя стабильно и показатели измерения производительности зависят только от
длительности интервала наблюдения, но не от расположения этого интервала на
оси времени.
При построении аналитической модели математик обычно оперирует такими по-
нятиями, как марковский процесс, пространство состояний, вероятности перехо-
дов, преобразования Лапласа, производящие функции и т. д. Инженер-практик,
для которого в конечном итоге все это делается, не обязан быть знакомым с эти-
ми понятиями и, скорее всего, не поймет их. В принципе, он может даже не
знать, что такое вероятность. Но его обязательно интересует конечный резуль-
тат — значения показателей, наиболее важные из которых перечислены далее.
О Распределение числа заявок в системе рк. Средние значения остальных харак-
теристик обычно выводятся именно из набора вероятностей рк. Это вероят-
ность того, что в системе находятся k заявок. Если модель представляет собой
набор уравнений, то переменными являются именно рк.
О Коэффициент загрузки р. Если СМО содержит один сервер, то р — это доля
времени, в течение которого сервер был занят обслуживанием. В случае, если
нет ограничений ни на количество источников нагрузки, ни на длину очереди
к серверу, коэффициент загрузки также может быть найден как отношение
1.2. Характеристики функционирования и производительности 27
среднего времени обслуживания к средней длине интервала между прибы-
тиями заявок или как отношение интенсивностей входного потока и обслу-
живания, то есть р = Х/ц. Для случая нескольких серверов р — это в среднем
отношение числа занятых серверов к их общему количеству. Если в системе
всего N серверов, то интенсивность обслуживания равна Np и р = X/(7Vp)
(рис. 1.4).
Значение р используется для формулирования условия существования ста-
ционарного режима: р < 1. Это означает, что среднее число заявок, прибыв-
ших за единицу времени, должно быть меньше, чем число заявок, которое
может быть обработано за единицу времени. Для открытых систем, под кото-
рыми будем понимать системы с внешними входными и выходными потока-
ми, р, кроме того, есть вероятность того, что в случае отсутствия поломок сер-
веров система не простаивает без работы, то есть в ней есть хотя бы одна
заявка. Поэтому существование стационарного режима для открытых систем
интерпретируется еще и как периодическое опустошение системы, так как
р0 = 1 - р > 0 [20, с. 35].
О Интенсивность выходного потока у определяется как среднее число заявок,
покидающих сервер в единицу времени. Для открытых систем без потерь, ко-
гда ни одна из поступивших заявок не может покинуть систему без обслужи-
вания, в стационарном режиме у равно интенсивности внешнего входного по-
тока; для систем с ограниченным буфером или конечным числом источников
это обычно не так.
О Время отклика Т — полное время, которое заявка проводит в системе от мо-
мента поступления до момента завершения обслуживания.
О Время ожидания W — время, которое заявка проводит в очереди в ожидании
обслуживания. Нетрудно заметить, что T=W+B, где В — длина заявки. Так
как W и В — случайные величины, для них можно вычислить средние значе-
ния. Тогда Г = №ср + 1/р. Целью построения аналитической модели часто
бывают также получение функций распределения Fw(x) и Е/х) времени ожи-
дания и времени пребывания соответственно.
О Средняя длина очереди Q — математическое ожидание для количества зая-
вок, находящихся в очереди.
О Среднее число заявок в системе L — вычисляется по набору вероятностей рк
как математическое ожидание дискретной случайной величины, Lrp = ^kpk.
k
О Распределение длительности периода занятости и его среднее значение, а также
распределение и среднее количество заявок, обслуженных за период занято-
сти. Это наиболее трудновычислимые характеристики, поскольку их нельзя
получить по вероятностям рк. Здесь требуется более сложный математиче-
ский аппарат и тонкие вероятностные рассуждения [20], [24].
Пара значений Zcp и связана очень важной в теории очередей зависимостью,
называемой законом Литтла: Lcp = ХТ^,. Эта зависимость справедлива для всех
дисциплин обслуживания и для произвольного вида систем, соответствующих
нотации GI/G/m. Существует строгое доказательство закона Литтла [77], однако
28 Глава 1. Аналитический и имитационный подходы к моделированию
его справедливость, по крайней мере для дисциплины FCFS, интуитивно ясна из
следующих соображений.
Помеченная заявка, покидающая систему, оставляет в ней в среднем Zcp заявок.
Но это в точности те заявки, которые поступили в систему после помеченной, за
время ее пребывания в системе. А так как в среднем это время равно 7^р> то сред-
нее число поступивших в систему заявок за время пребывания в ней помеченной
заявки как раз и равно ХТ^,. Другая, более интересная интерпретация дана в [52].
Представим себе, что все заявки платят 1 рубль за 1 единицу времени пребыва-
ния в системе. Эти деньги система может «заработать» двумя способами. Пер-
вый — позволить заявкам платить «непрерывно» на протяжении всего времени.
Тогда за единицу времени заработок составит в среднем Lcp рублей. Второй спо-
соб — заплатить целиком всю сумму в момент ухода из системы. В состоянии
равновесия число заявок, покидающих систему и прибывающих в нее в единицу
времени, статистически равны. Поэтому заработок составит ).Тср рублей за еди-
ницу времени, и очевидно, что суммы в обоих случаях должны совпасть.
Перечисленные показатели являются общими для очень широкого класса си-
стем массового обслуживания. В зависимости от описания конкретной системы
ее исследователя могут интересовать и другие показатели, естественным обра-
зом вытекающие из логики ее работы, например среднее время отклика &-раун-
довой заявки при дисциплине Round Robbin [50].
1.3. Простейший пример аналитической
модели. Вычисление характеристик
Аналитический подход к исследованию систем с очередями проиллюстрируем
на примере простейшей системы М/М/1. Заявки обслуживаются в порядке по-
1.3. Простейший пример аналитической модели. Вычисление характеристик 29
ступления. Будем предполагать, что выполненяется условие р = Х/р < 1, так как
в противном случае очередь растет до бесконечности.
Экспоненциальное распределение позволяет предельно просто описать состоя-
ние в момент времени t — как число заявок в системе (то есть текущая длина
очереди плюс одна обслуживаемая заявка). Нам не нужно помнить ни когда по-
ступила последняя заявка, ни когда было начато происходящее в данный момент
обслуживание. Так как экспоненциальное распределение обладает свойством от-
сутствия последействия, эта информация избыточна — мы и так можем полно-
стью предсказать дальнейшее поведение системы.
Обозначим pn(t) вероятность того, что в момент времени t в системе находится п
заявок, п = 0, 1... Основываясь на свойствах экспоненциального распределения,
можно получить для p„(t) следующую систему дифференциально-разностных
уравнений [20, с. 75]:
Ро(0 = -^о(0 + иР1(0; (1П
Р^(О = ^п-1(О-(^ + м)Ри(О + МР„+1(О> « = 1, 2...
Эти уравнения легко выписать и непосредственно, исходя из условия баланса:
разность потоков, вводящего процесс в состояние п и выводящего процесс из
него, равна производной от функции, описывающей вероятность этого состоя-
ния. Несмотря на предельную простоту моделируемого случайного процесса, ре-
шить систему уравнений (1.1) очень трудно. Утомительная процедура решения
показана, например, в [20]. Окончательный вид выражения для p„(t) представля-
ет собой бесконечный ряд из модифицированных бесселевых функций. Таким
образом, уже для самой простой системы с очередью трудно получить выраже-
ния, описывающие ее поведение во времени. Для других систем это сделать
практически невозможно. Поэтому теория очередей рассматривает в основном
поведение систем в стационарном режиме, что позволяет получить гораздо более
удобные выкладки и более наглядные результаты.
В стационарном случае при t -> со р'л(() -> 0, ир„(() -> рп, где рп называются ста-
ционарными вероятностями. Система дифференциальных уравнений (1.1) при
этом превращается в систему линейных уравнений
-1Р.+РР, =°;
ЛРп-1 -(*- + ЮР„+МР„+1 =0, п = 1, 2...,
для которой условие баланса формулируется так: входной поток в состояние ра-
вен выходному (рис. 1.5).
Рис. 1.5. Диаграмма состояний для системы М/М/1
30 Глава 1. Аналитический и имитационный подходы к моделированию
Из свойства полной вероятности следует ^рл =1, называемое условием нормы-
л«0
ровки.
Существует несколько способов решить систему (1.2). Рассмотрим, например, ре-
шение, основанное на рекуррентных соотношениях. Выразим из уравнения для
п = 0 вероятность рр pf = рр0. Подстановка в уравнение для п = 1 дает: р2 = р2р0.
Методом математической индукции легко показать, что последовательная под-
становка дает общее решение: р„ = р”р0, п = 0, 1, 2... Вероятность р0 вычисляется
из условия нормировки и равна р0 = 1 - р. Окончательно р„ = (1 - р)р".
Зная вероятности р„, можно вывести выражения для среднего числа заявок в си-
стеме и среднего времени пребывания. Получим:
ч=Ё^=т£- <13)
1-р
и по формуле Литтла
41 И(1-Р)
Методы теории очередей хороши тем, что любой результат всегда можно прове-
рить грубой прикидкой, здравым смыслом, то есть увидеть, не допущена ли
в рассуждениях и выводе принципиальная ошибка. Ясно, что как Zcp, так и Тср
должны возрастать с увеличением нагрузки на сервер, выражаемой значением р.
Формула (1.3) показывает, что это действительно так, и при стремлении р к еди-
нице LCf> и Тср стремительно возрастают. Такая зависимость этих величин от р ха-
рактерна почти для всех систем с очередями.
Средняя длина очереди QcP может быть вычислена вычитанием среднего числа
обслуживаемых на сервере заявок из Zcp:
Qep = 4P- Р = Р2/(1 - Р)-
Среднее время ожидания получается вычитанием из среднего времени отклика
среднего времени обслуживания:
Столь же легко можно выполнить анализ средних, если учесть наличие приори-
тетов у заявок. Предположим для простоты, что существуют заявки только двух
типов, 1 и 2, прибывающих по закону Пуассона с интенсивностями и Х2 соот-
ветственно. Длины заявок обоих типов распределены экспоненциально с одина-
ковым средним 1/р. Предположим, что Pi + р2 < 1. где р, = Х,/ц, — коэффициент
загрузки для заявок i-ro типа. Тип 1 имеет более высокий приоритет по отноше-
нию к типу 2.
1.3. Простейший пример аналитической модели. Вычисление характеристик 31
1.3.1. Абсолютный приоритет (PR)
Если обслуживается заявка типа 2 и в это время прибывает заявка типа 1, обслу-
живание прерывается и сервер переключается на вновь прибывшую заявку. Если
заявок типа 1 в системе нет, сервер возобновляет обслуживание прерванной за-
явки с точки прерывания.
Пусть случайная величина L, обозначает число заявок г-го типа в системе и 5, —
время пребывания в системе заявки г-го типа. Вычислим для них средние значе-
ния. Для заявок типа 1 заявки типа 2 как бы не существуют. Поэтому можем вос-
пользоваться готовыми формулами (1.3):
Так как остаточное время обслуживания не зависит от времени, уже проведенно-
го заявкой на сервере, и для всех заявок одинаково распределено, общее число
заявок в системе не зависит от порядка обслуживания. Оно будет таким же, как
если бы все заявки обслуживались в порядке поступления. Поэтому
Pi +Р2
1-Pi -Р2’
E(Ll) + E(L2) =
откуда вычитанием получаем:
E(L2)= P1 + р2---^1-
1-Р1-Р2 1-Р1
Р2_______
(1-Р1 )(1-P1 -Р2 )
Применив формулу Литтла, имеем
E(S2) = =------------------.
Х2 ц(1-Р1)(1-Р1-р2)
1.3.2. Относительный приоритет
Теперь рассмотрим ситуацию, когда заявки типа 1 не могут прерывать обслужи-
вание заявок типа 2. Среднее время пребывания для заявок типа 1 равно
е(7]) = е(Л)1 + 1+Р21.
ИР Р
Последнее слагаемое отражает тот факт, что если заявка типа 1 застает на обслу-
живании заявку типа 2, она должна дождаться завершения ее обслуживания.
Здесь Р2 — коэффициент загрузки для заявок второго типа и одновременно веро-
ятность того, что сервер занят обслуживанием такой заявки. По формуле Литтла
E(Lt) = тогда
Д(7])^р(1 + р2); £(Д) = Р1(1 + р2).
1-Pi 1-Pi
32 Глава 1. Аналитический и имитационный подходы к моделированию
Для заявок типа 2 получим вычитанием:
E(L ~Рг))
2 (1-Р.Х1-Р. -р2) ’
и по формуле Литтла
Е(Т2) = (1~Р'(1~Р' ~Рг»
2 ц(1-Р1)(1-Р1 -р2)
Более тонкими методами, с применением аппарата производящих функций и пре-
образований Лапласа можно получить функции распределения для времени
пребывания заявок в системе М/М/Х, времени ожидания и периода занятости.
Мы не будем рассматривать эти методы, поскольку изучение математической
теории очередей не входит в задачу книги.
Подведем некоторые итоги. Нами рассмотрен пример математического анализа
простейшей системы обслуживания. Простейшей она считается потому, что ее
описание включает все мыслимые ограничения и идеализации, которые предель-
но облегчают построение аналитической модели. К настоящему времени теория
очередей является вполне сложившимся разделом математики, а именно — тео-
рии случайных процессов, в котором получено огромное количество результа-
тов, многие из них стали классическими. Большинство книг по теории очередей
построены по принципу постепенного перехода от изучения системы М/М/Х
к рассмотрению более сложных систем; при каждом переходе к новой системе
какое-то ограничение отбрасывается или ослабляется. Из этих книг нетрудно
видеть, что отказ даже от небольшого ограничения почти всегда приводит к зна-
чительному усложнению модели. Уже для такой сравнительно простой системы,
как M/G/X, с обслуживанием в порядке поступления решение возможно полу-
чить только в форме преобразований (в виде производящей функции или преоб-
разования Лапласа), обращение которых представляет собой отнюдь не простую
самостоятельную задачу численного анализа. Многие же современные работы,
особенно посвященные приложениям к анализу вычислительных систем, зачас-
тую развивают математический аппарат столь далеко, что оказываются недо-
ступными для специалистов-практиков, а полученные результаты имеют лишь
теоретическое значение. И самое главное — несмотря на высокий уровень мате-
матического искусства авторов и затраченные огромные усилия, «коэффициент
полезного действия» результатов остается невысоким, так как предположения,
положенные в основу модели, все равно слишком далеки от реальности. Модели,
для которых удается получить аналитическое решение, сравнительно просты и
адекватно описывают только простые, идеализированные системы или отдель-
ные части сложных систем.
Построение модели, описывающей конкретную систему, является искусством,
а не наукой, и требует достижения разумного компромисса между количеством
подробностей системы, включенных в модель, и простотой этой модели. Иными
словами, включение большего числа деталей позволит исследователю больше и
точнее узнать о поведении реальной системы, но только в том случае, если ему
удастся решить полученную модель. Заметим, что при этом погрешность числен-
1.4. Имитационное моделирование систем 33
ного решения не должна быть сравнимой с погрешностью, которую внесло от-
брасывание несущественных, с точки зрения аналитика, факторов при построе-
нии модели. С другой стороны, игнорирование большего числа подробностей
системы приводит к повышению вероятности решить модель, но предполагает
готовность к тому, что полученные результаты скорее выявят принципиальные
качественные зависимости поведения системы, но будут весьма далеки от реаль-
ных количественных характеристик. Искусство моделирования — это искусство
поддержания баланса между точностью и наглядностью. Невозможно дать уни-
версальные рекомендации, какую информацию о системе следует включать в мо-
дель, а какую нет, — это в каждом конкретном случае решают эксперт предмет-
ной области и инженер-математик, лучше всего, если они совмещены в одном
лице.
Важно отметить следующее. Аналитический результат, если его удается полу-
чить, позволяет не только рассчитать показатели системы, но и оценить чувстви-
тельность ее поведения к изменению любого из входных параметров, например
коэффициента загрузки, и сразу дать ответ на вопрос: «А что будет, если...?» Что
же может предложить нам для этого имитационный подход? Обсудим этот во-
прос в следующем разделе.
1.4. Имитационное моделирование
систем
Можно дать следующее определение понятия модель: это такое описание, кото-
рое исключает несущественные подробности и учитывает наиболее важные осо-
бенности системы. А такое определение дает К. Шеннон: «Модель является
представлением объекта, системы или понятия (идеи) в некоторой форме, от-
личной от формы их реального существования» [43]. Моделирование же можно
определить как методологию изучения системы путем наблюдения отклика мо-
дели на искусственно генерируемый входной поток. К. Шеннон пишет так:
«Имитационное моделирование есть процесс конструирования модели реальной
системы и постановки экспериментов на этой модели с целью либо понять пове-
дение системы, либо оценить (в рамках ограничений, накладываемых некоторым
критерием или совокупностью критериев) различные стратегии, обеспечиваю-
щие функционирование данной системы...» Имитационное моделирование явля-
ется экспериментальной и прикладной методологией, имеющей следующие цели:
О описать поведение системы;
О построить теории и гипотезы, которые могут объяснить наблюдаемое пове-
дение;
О использовать эти теории для предсказания будущего поведения системы,
то есть тех воздействий, которые могут быть вызваны изменениями в системе
или изменениями способов ее функционирования.
А вот что пишет о понятии «модель» Терри Уильямс (Terry Williams) в недавно
вышедшей монографии [90]. Сначала он приводит определение из словаря «Collins
English Dictionary»: «Модель — это упрощенное представление или описание
34 Глава 1. Аналитический и имитационный подходы к моделированию
системы или сложной сущности, направленное на то, чтобы облегчить вычисле-
ния и прогнозирование». Затем следуют пояснения:
О модель представляет или описывает нечто, существующее в действитель-
ности;
О модель упрощает это «нечто»;
О создание модели преследует цель, как правило, выполнить некоторые вычис-
ления и предсказать, как это «нечто» будет себя вести.
Автор замечает, что фотографический снимок не является моделью, так как, удов-
летворяя двум первым пунктам, не удовлетворяет третьему. И далее он пишет:
«Модель представляет или описывает упрощенное восприятие реальной систе-
мы, используя формальный, теоретически обоснованный язык концепций и их
отношений. Это облегчает оперирование сущностями системы, понимание си-
стемы и управление ею. Моделирование бессмысленно, если оно не влияет на при-
нятие решения. Этим принципом следует руководствоваться как при построе-
нии модели, так и непосредственно при самом моделировании».
Авторы методологической работы [80] сформулировали основные факторы, влия-
ющие на принятие правильного решения по результатам моделирования:
О Адекватное понимание решаемой задачи. Если задача не полностью определе-
на и недостаточно четко описана, очень мало шансов, что ее решение прине-
сет какую-либо пользу. Это фундаментальное утверждение относится ко всем
задачам, а не только к моделированию.
О Корректная модель. Это первостепенный фактор для технически или эконо-
мически эффективного решения, если брать всю задачу в целом. Ошибки
в модели, если они не выявлены, скорее всего, приведут к принятию резуль-
татов, основанных на неверной модели. Стоимость такого типа ошибок обыч-
но очень высока. Даже если ошибка обнаружена, но это произошло на позд-
них этапах проекта, стоимость исправлений включает также и повторное
прохождение всех предшествующих этапов.
О Корректная программа. Программирование — последний этап разработки,
и корректная программа может быть написана только по корректной модели.
Аргументы в пользу корректности программы такие же, что и для модели.
О Планирование эксперимента. Разработка модели и программы должна отра-
жать цели, для которых выполняется моделирование. Для получения требуемых
ответов программе нужно правильно задать вопросы, то есть спланировать
последовательность вычислительных экспериментов с полным пониманием
проблемы.
О Интерпретация результатов. Никакая моделирующая программа не дает от-
вета со стопроцентной достоверностью. Результаты моделирования получа-
ются на основе обработки случайных чисел, поэтому для их правильного по-
нимания требуется применение статистических методов.
Таким образом, заключают авторы работы [80], моделирование — это больше,
чем просто программа. Достижение целей моделирования требует пристального
внимания ко всем указанным факторам.
1.4. Имитационное моделирование систем 35
Объектом нашего анализа выступают модели систем с очередями, которые явля-
ются логическим, а не физическим представлением рассматриваемых систем. Хотя,
с формальной точки зрения, концепция моделирования не зависит от существо-
вания компьютера, только наличие высокопроизводительной вычислительной
машины делает моделирование действительно полезным практическим инстру-
ментом.
Искусством моделирования можно овладеть только через опыт — оно не может
быть приобретено пассивным образом. Пусть, например, целью построения мо-
дели является оценка объема денежной массы, которую нужно заложить в стоя-
щий в магазине банкомат для обслуживания клиентов в течение дня. Для этого
нужно учесть день недели, товарооборот и уровень цен в магазине, оснащенность
населения карточками этого банка. Но понятно, что вряд ли понадобится инфор-
мация о колебании температуры воздуха в помещении магазина или о среднем
росте его продавцов. Менее очевидным является утверждение о том, что интен-
сивность обращений к банкомату зависит от распределения по предприятиям
жителей близлежащих домов и от того, по каким числам на этих предприятиях
перечисляют зарплату на карточки. Наконец, опытный эксперт знает, что если
входной поток описывается распределением Пуассона, а клиенты в случае заня-
тости банкомата сразу же, не ожидая, переходят к другому банкомату, то только
среднее время обслуживания влияет на производительность системы. Поэтому
ясно, что тип модели, выбранной для описания какой-либо реальной системы,
зависит от уровня знаний и опыта эксперта.
Типовая последовательность имитационного моделирования включает следу-
ющие этапы:
О концептуальный', разработка концептуальной схемы и подготовка области ис-
ходных данных;
О математический', разработка математических моделей и обоснование методов
моделирования;
О программный: выбор средств моделирования и разработка программных мо-
делей;
О экспериментальный: проверка адекватности и корректировка моделей, пла-
нирование вычислительных экспериментов, непосредственно моделирование,
интерпретация результатов.
Имитационное моделирование на компьютере, в принципе, позволяет проанали-
зировать любую реальную систему произвольной сложности. Концептуально,
промоделировать сложную систему так же легко, как и простую разница будет
состоять только в объеме программного кода. Имитационная модель может
учесть любой нюанс в дисциплине обслуживания всего лишь путем небольшой
модификации текста одной-двух процедур, а в аналитической модели это может
потребовать коренной переделки всех уравнений, сделать модель необозримо
сложной или оказаться вообще невозможным. Этот факт отражает как силу, так
и слабость имитационной методологии. С одной стороны, имитационное моде-
лирование дает метод анализа, применимый в тех случаях, когда математиче-
ская модель чрезмерно сложна и позволяет аналитику получить более точные
36 Глава 1. Аналитический и имитационный подходы к моделированию
результаты. Но с другой стороны, имитационная модель не позволяет глубоко
заглянуть в сущность системы, выявить ее «изюминки» и законы, по которым
она живет, построить качественные зависимости между «входом» и «выходом»,
как это позволяет сделать математическая модель, если ее, конечно, удалось ре-
шить. То, что при взгляде на математический результат видно сразу, при имита-
ционном моделировании может быть выявлено только в результате постановки
значительного количества экспериментов (еще говорят «прогонов»).
Методология имитационного моделирования обладает большой общностью. Рас-
сматриваемый в этой книге анализ систем с очередями — лишь одно из бесчис-
ленного множества ее применений. Формулы, конечно же, более красивы и при-
влекательны, но, во-первых, как уже было сказано, они не всегда могут быть по-
строены, во-вторых, что важнее, разработка математической модели требует
специальных знаний и высокого мастерства, которыми специалист-практик об-
ладает далеко не всегда. Этим объясняется тенденция прикладных специалистов
использовать преимущественно имитационные модели, которые математик мо-
жет расценить как «силовой прием». Конечно, оптимальной является ситуация,
когда аналитик владеет обоими подходами и может использовать их в разумном
сочетании.
Главная и наиболее очевидная цель имитационного моделирования — выяснить,
как повлияют на производительность отдельные изменения конфигурации си-
стемы или увеличение нагрузки на нее. Процесс моделирования включает три
фазы [82]. На фазе валидации строится базовая модель существующей системы,
проверяются и обосновываются предположения, лежащие в ее основе. На фазе
проектирования модель используется в прогностических целях для предсказа-
ния влияния различных модификаций на производительность. На фазе верифи-
кации реальная производительность модифицированной системы сравнивается с
результатами моделирования. Взятые вместе, эти три фазы образуют модельный
цикл (рис. 1.6).
Фаза валидации начинается с описания модели и включает выбор тех ресурсов
и элементов деятельности, которые будут представлены; выявление особенно-
стей системы, которые требуют внимания; выбор структуры модели; процедуры
расчета необходимых показателей по результатам имитационного эксперимента.
Далее в реально функционирующей системе проводятся замеры входных пара-
метров, которые послужат рабочим материалом для модели, а также замеры про-
изводительности, результаты которых будут сравниваться с выходными данны-
ми модели для оценки ее точности. Модель проверяется, в результате чего может
потребоваться внести в нее изменения. Значимые различия между выходными
данными системы и модели свидетельствуют об изъянах модели — какое-то до-
пущение оказалось некорректным, какие-то факторы проигнорированы неправо-
мерно. Но и отсутствие таких различий еще не гарантирует того, что модель су-
меет правильно предвидеть влияние количественных и качественных изменений
в системе. Уверенность в ее прогностических возможностях можно получить
двумя путями. Первый — это повторная валидация при разных наборах входных
данных. Так, если нужно узнать, как повлияет на улучшение пропускной способ-
ности дороги открытие еще одной полосы движения, можно перекрыть одну из
1.4. Имитационное моделирование систем 37
имеющихся полос, а для того чтобы выяснить, насколько улучшится производи-
тельность компьютера при добавлении еще одной платы памяти, — снять одну
из действующих плат. Второй способ — выполнить фазу верификации.
Рис. 1.6. Фазы модельного цикла
На фазе проектирования входные параметры меняются в соответствии с моди-
фикацией системы, эффективность которой мы хотим проверить с помощью мо-
дели. Это довольно сложный и ответственный процесс, ведь необходимо пра-
вильно сформулировать вопрос для модели. Результаты затем анализируются,
их отличия от выходных данных исходной модели и представляют собой эффект
от модификации системы.
38 Глава 1. Аналитический и имитационный подходы к моделированию
На фазе верификации измерения снимаются с обновленной системы, и снова про-
водится сравнение. Производительность системы сравнивается с данными моде-
лирования. Наблюдаемые различия могут объясняться двумя причинами:
О либо при составлении модели упущены некоторые ее свойства, что дает о себе
знать не всегда, а лишь при стечении определенных обстоятельств;
О либо система отреагировала на изменения совсем не так, как прогнозирова-
лось в модели.
Кроме того, точность выходных данных модели не может быть лучше точности,
с которой заданы входные параметры. Понимание и правильная оценка причин
возникающих различий имеют решающее значение для принятия окончательно-
го решения — можно пользоваться такой моделью или нельзя. Лаконичную и
четкую характеристику этой фазы дал К. Шеннон: «Непосредственное экспери-
ментирование с системой обычно состоит в варьировании некоторых ее парамет-
ров. При этом поддерживая все остальные параметры неизменными, наблюдают
результаты эксперимента».
Модельный цикл отнюдь не является строго последовательным процессом. Между
отдельными составляющими фаз валидации и проектирования могут существо-
вать жесткие зависимости. Может потребоваться совместимость между описа-
нием модели, замерами данных и методикой оценки модели. Достижение такой
совместимости и ее согласование с конкретными целями моделирования явля-
ются по своей сущности процессами итерационными. Авторы работы [82] акцен-
тируют внимание еще на нескольких важных аспектах методологии моделиро-
вания:
О ясное понимание целей моделирования может облегчить построение модели
и процесс ее «прокрутки»;
О описание нагрузки системы, в частности входных потоков и длительностей
обслуживания, является плохо формализуемым процессом, которому свойст-
венны приблизительность и неточность. Что, например, следует понимать под
фразой «типичная нагрузка»? Достичь большей гибкости помогает иерархи-
ческое описание нагрузки — от перечисления всех источников до рассмотре-
ния отдельных заявок на обслуживание;
О анализ чувствительности модели помогает ответить на вопрос: в какой мере
«идеализированные» предположения, сделанные при построении модели, бро-
сают тень на ее выводы? Существуют две формы анализа: проверка устойчи-
вости выходных данных к вариации предположения (небольшим отклонениям
на входе должны соответствовать небольшие отклонения на выходе) и под-
становка предельных значений, для которых результат по смыслу очевиден.
Исторически первое применение имитационного моделирования восходит к фран-
цузскому естествоиспытателю Буффону, который в 1777 г. сформулировал зада-
чу, известную ныне как «игла Буффона»: игла длиной а бросается случайным
образом на лист бумаги, расчерченный параллельными равноотстоящими друг
от друга на расстояние d прямыми (а < d ). Какова вероятность р того, что игла
пересечет одну из прямых? Точное решение р = 2a/(nd ). На основании часто-
ты р, выявленной в результате экспериментов, Буффон вычислил число л. Это
1.4. Имитационное моделирование систем 39
стало первым примером реализации замечательной идеи — вычисления детер-
минированной величины по случайным данным эксперимента. Идея стала прак-
тической методологией с появлением вычислительных машин, программно гене-
рирующих данные путем выполнения логических операций.
Рассмотрим аспекты применения имитационного моделирования с помощью
компьютера к построению и анализу моделей систем с очередями. Для таких
систем моделирование является дискретно-событийным, то есть состояние си-
стемы претерпевает только дискретные изменения с течением времени (хотя во
второй части будут рассмотрены и системы, имеющие непрерывные параметры).
Попытаемся проиллюстрировать относительную силу и слабость имитацион-
ного моделирования как противоположности математического анализа систем
с очередями.
Имитационная модель включает описание структуры изучаемой системы. При
выполнении программы генерируются времена прибытия, времена обслужива-
ния и другие случайные величины, диктуемые логикой работы системы. Иными
словами, в имитационном эксперименте искусственно порожденные входные
данные «пропускаются» через логическую структуру, чей ответ «подражает» от-
вету реальной системы на такие же входные данные. Отсюда, требуется уметь
генерировать входные данные с любыми заранее заказанными статистическими
характеристиками, то есть требуются соответствующие алгоритмы, в частности
генератор случайных чисел с заданной функцией распределения. Детали самой
моделирующей программы зависят от операционной платформы и используемо-
го языка. В частности, исследователь должен сделать выбор между языком про-
граммирования общего назначения (C++, Java, C# и др.) и специализированным
языком моделирования.
Результат имитационного эксперимента по своей природе носит статистический
характер, и поэтому для его интерпретации требуется определенный статисти-
ческий анализ. Это наиболее критический участок в разработке модели, потому
что зачастую, глядя на выходные данные, трудно сказать, какой общностью они
обладают, и сделать выводы. Мы можем только заметить, что произошло в этом
конкретном случае. Вновь дадим слово К. Шеннону: «Подобно всем мощным
средствам, существенно зависящим от искусства их применения, имитационное
моделирование способно дать либо очень хорошие, либо очень плохие результа-
ты. Оно может либо пролить свет на решение проблемы, либо ввести в заблуж-
дение» [43]. Обсудим указанные аспекты имитационных моделей.
1.4.1. Статистическая обработка результатов
моделирования
Планирование любого статистического эксперимента и интерпретация его ре-
зультатов являются весьма сложными задачами. Очень важно написать и отла-
дить программу, но, чтобы это принесло пользу, нужно еще правильно и в нуж-
ном порядке «задать» ей вопросы и уметь сделать из результатов ее выполнения
правильные и надежные выводы. Универсального решения возникающих здесь
проблем не существует, все зависит от искусства аналитика, умеющего удачно
40 Глава 1. Аналитический и имитационный подходы к моделированию
сочетать собственную интуицию и методы математической статистики. Подроб-
ное изложение этих методов не входит в задачу книги, с ними можно ознако-
миться, например, в [1], [16], [18]. Здесь мы лишь проиллюстрируем существо
возникающих проблем.
Рассмотрим модель с пуассоновским входным потоком, s обслуживающими уст-
ройствами и потерей блокированных заявок, известную как модель Эрланга с
потерями. Довольно просто написать программу, например, на С или Pascal,
которая генерировала бы необходимые распределения и подсчитывала число
прибывших и потерянных заявок. Для простоты предположим, что время обслу-
живания заявок является детерминированным и нас интересует вероятность по-
тери прибывшей заявки в стационарном режиме. Статистически эту вероятность
можно рассматривать как отношение числа заявок, заставших все устройства за-
нятыми, к общему числу прибывших заявок, когда стационарный режим достиг-
нут. Естественный вопрос, интересующий в данном случае аналитика: какой вид
имеет функциональная зависимость указанной вероятности от параметров сис-
темы, таких, например, как интенсивность входного потока, среднее время об-
служивания и количество устройств? Но еще раньше нужно ответить на другой
вопрос: как скоро установится стационарный режим, сколько тактов модельного
времени для этого потребуется? Исходя из теории, стационарный режим может
быть достигнут только асимптотически, при устремлении времени «в бесконеч-
ность», если только в начальный момент времени он уже не существует. Но та-
кое предположение не соответствует реальности, так как моделирование нужно
начинать из некоторого определенного состояния, например, когда все устройст-
ва свободны. К сожалению, дать здесь какие-то рекомендации без математи-
ческой модели невозможно, но ее наличие, скорее всего, сделает имитационное
моделирование ненужным. На практике обычно поступают просто: при расчете
итоговых показателей отбрасывают первые сто (тысячу, десять тысяч) входных
заявок.
Сделаем допущение, что нам известно, сколько должно поступить заявок для
приведения модели к стационарному режиму. Сколько еще прибытий нужно
сгенерировать, чтобы обрести уверенность в том, что наблюдаемая доля потерян-
ных заявок достаточно близка к истинной вероятности потерь? Иными словами,
насколько велик должен быть объем выборки, чтобы она была репрезентатив-
ной? И наконец, что лучше — провести один длительный эксперимент или не-
сколько более коротких? Так или иначе, при постановке имитационного экспе-
римента на эти вопросы нужно ответить, и предположим, что ответы получены.
Что же мы имеем? Можем ли мы утверждать, что вычисленная в результате экс-
перимента доля потерянных заявок при заданных параметрах s и 1 - это и есть
значение функции потерь в данной точке плоскости B(s; X)?
Таким образом, мы видим, что числовое значение, которое нам дает имитацион-
ный эксперимент, подвержено влиянию трех типов ошибок:
О ошибки, возникающей из-за допущения того, что стационарный режим до-
стигается за некоторое конечное время;
О ошибки конечности объема выборки, так как будто бы после вхождения в ста-
ционарный режим генерируется лишь конечное число входных заявок;
1.4. Имитационное моделирование систем 41
Q ошибки, возникающей из-за того, что программные генераторы случайных чисел
на самом деле генерируют псевдослучайные последовательности.
Если придерживаться прагматической точки зрения и считать, что длительность
моделирования велика настолько, а генератор случайных чисел написан искус-
ным настолько, что всеми этими ошибками можно пренебречь, то и в этом слу-
чае мы получаем всего лишь одну точку на кривой, В(х; X). Принципиально не
вызывает проблем, но на деле довольно утомительно и дорого проводить доста-
точное количество «прогонов», чтобы получить график B(s; X) хотя бы для одно-
го значения х. Чтобы получить несколько графиков, нужно повторить этот про-
цесс для каждого х.
Теперь предположим, что постоянное время обслуживания мы заменили случай-
ным, например описываемым экспоненциальным распределением. Увеличение
дисперсии входной величины увеличит дисперсию выходной, поэтому для полу-
чения представительных данных потребуется уже больше модельного времени.
Но мы никогда не сможем точно определить, действительно ли при сохранении
одного (или двух) моментов входной случайной величины мы получили такую
же кривую (то есть она зависит не от вида распределения, а только от момен-
тов) или же эти кривые только похожи, а их математическая сущность все-таки
разная. Или, например, мы исследуем зависимость некоторой величины У11ЫХ от
входных параметров ц и г2. Предположим, что результаты экспериментов позво-
ляют заподозрить, что зависимость на самом деле имеет вид Квых(г1/г2). Как ни
заманчива была бы такая гипотеза, доказать ее только имитационным путем все
равно не удастся. И еще одно замечание. Как уже было сказано, вероятность по-
тери оценивается как отношение числа потерянных заявок к общему их числу за
большой период времени. Но интуитивно ясно (и можно строго доказать), что ее
состоятельной оценкой является еще и такая: отношение суммы длительностей
периодов времени, когда все устройства были заняты, к общему времени моде-
лирования. Но хотя в теории эти значения должны быть равны, при имитацион-
ном эксперименте почти наверняка они будут разными. И снова никто не ска-
жет, принципиальна ли эта разница или на самом деле ее нет. А интуиция в
таких случаях часто подводит. Все эти замечания наглядно выявляют слабость
имитационного моделирования в сравнении с аналитическим и говорят о том,
что его следует использовать в том и только том случае, если упрощенная мате-
матическая модель исчерпала свои возможности, а более точную построить
нельзя.
Повысить эффективность имитационного моделирования можно с помощью из-
вестного уже давно метода регенерации [57]. Этот метод ставит в соответствие
бесконечно повторяющемуся состоянию свойство, согласно которому дальней-
шее поведение системы, находящейся в этом состоянии, не зависит от предысто-
рии. Эволюцию системы можно рассматривать как последовательность циклов,
которые статистически независимы и одинаково распределены. Каждый цикл
заканчивается возвратом в состояние регенерации. Ценность метода состоит в
том, что он выделяет последовательность независимых случайных величин из
коррелированных случайных величин и, следовательно, допускает применение
центральной предельной теоремы. Следуя ее классической формулировке, если
42 Глава 1. Аналитический и имитационный подходы к моделированию
Rlt R2.. R„ — независимые, одинаково распределенные случайные величины
с конечными математическим ожиданием E(R) и дисперсией D(R), и если выбо-
рочное среднее значение /?ср(н) = (Rt + R2 +...+ R^)/n получено по выборке объе-
мом п, то
/л/ДСК)
У nE(R)
->Ф(0
при п > оо, где Ф(Г) =
dx — нормальное распределение. Применение
метода регенерации часто помогает в проблемных ситуациях, рассмотренных ра-
нее.
1.4.2. На каком языке моделировать?
Языки моделирования — это высокоуровневые проблемно-ориентированные
языки, призванные облегчить написание моделирующих программ. Они содер-
жат ряд конструкций и функций, являющихся общими для всех моделей, такие
как генераторы случайных чисел, которые отсутствуют в универсальных языках.
Универсальные языки более гибки в использовании, и программы на них могут
достичь большей производительности. С другой стороны, написать моделиру-
ющую программу на специализированном языке проще. Выбор типа языка и
конкретного языка внутри этого типа зависит от ряда факторов, таких как дос-
тупность программного обеспечения, умение программировать и характеристи-
ки системы, которую нужно моделировать.
Как следует из названия этой книги, в качестве инструмента моделирования
в ней используется C++. Однако для того чтобы изложение материала не греши-
ло односторонностью, необходимо рассказать и об альтернативных вариантах —
специализированных языках и средах моделирования, а также обосновать, поче-
му автор отдает свои симпатии C++. Сразу заметим, что число таких языков и
инструментальных средств довольно велико, а количество посвященных им пуб-
ликаций просто огромно. Поэтому в рамках одной главы невозможно дать де-
тальный обзор того, что могло бы составить тему нескольких книг. Автор лишь
попытался кратко рассмотреть те системы, с которыми в той или иной степени
знаком сам. В списке литературы приведены ссылки на книги и источники из
Интернета, из которых заинтересованный читатель может составить более пол-
ное представление о предмете. Отметим, что интересный обзор с примерами
программ и классификация программного обеспечения, так или иначе связанно-
го с моделированием, содержатся в [60, гл. 4].
Сначала перечислим языки, истоки которых восходят к далеким уже 60-70-м го-
дам прошлого столетия. Самый популярный язык моделирования прошлых лет —
GPSS (General Purpose Simulation System). Этот язык очень удобен для модели-
рования систем с очередями. И сейчас есть люди, не признающие никаких дру-
1.4. Имитационное моделирование систем 43
гих языков моделирования, кроме GPSS. К достоянию прошлого можно отнести
также языки SIMSCRIPT и GASP (General Activity Simulation Program). GA5>P
представляет собой пакет подпрограмм Фортрана, специально предназначенных
для моделирования. К достоинствам GASP можно отнести то, что он, обладая
свойствами языка моделирования, не требовал от программиста, знакомого с
Фортраном, изучения совершенно нового языка. Версия GASP IV позволяла
также строить модели, в которых состояния могли изменяться во времени не-
прерывно. Из литературных источников прошлых лет отметим [62], где дается
общий обзор языков моделирования и подробно описаны GPSS, SIMSCRIPT
и SIMULA. Большой популярностью пользовалась «библия» GPSS [46] — эту
книгу, выпущенную в русском переводе очень небольшим тиражом, в бытность
автора студентом можно было получить в институтской библиотеке лишь по фор-
ме обслуживания читального зала и только по предварительной записи. GPSS
описан также в [55], своим создателем — в [68] и вместе с SIMSCRIPT — в [67].
GPSS до сих пор имеет большое количество поклонников и является весьма вос-
требованным инструментом. Ему посвящены целые порталы в Интернете [95],
[96], а за последние годы вышли несколько книг, посвященные описанию совре-
менных GPSS-систем, в частности GPSS World (Minuteman Software, Северная
Каролина, США) [6], [25], [34], [39], [40], пришедшей на смену GPSS V, которая
использовалась еще на машинах серии ЕС. SIMSCRIPT описан своими разра-
ботчиками в [74]. GASP IV рассмотрен опять-таки своим автором Аланом Приц-
кером в [81], а в 1984 г. вышла в русском переводе его книга [33], посвященная
языку SLAM II — расширению GASP IV с возможностью моделировать сети
очередей. Многие примеры из этой книги обсуждаются с точки зрения их реали-
зации средствами C++ во второй части данной книги.
Заслуживает упоминания и пакет прикладных программ RESQ. Он состоит из
двух частей — аналитической и моделирующей — и предназначен для исследова-
ния сетевых моделей, которые возникают в теории вычислительных систем при
оценке их производительности. RESQ описан в [84]. Хорошо известна также об-
зорная работа [85], в которой обсуждаются и языки моделирования.
Перейдем к более близким нам временам. Пакет DSPNexpress [75], [102], [91],
разработанный в университете Дортмунда, в Германии, содержит набор эффек-
тивных численных методов анализа детерминированных и стохастических сетей
Петри в переходном и стационарном режимах. С помощью пакета можно моде-
тировать случайные процессы в дискретно-событийных стохастических систе-
мах с детерминированными или экспоненциально описанными событиями, то есть
марковские и полумарковские процессы. Программное обеспечение состоит из
шести модулей, взаимодействующих посредством Unix-сокетов. Эти модели реа-
лизуют различные алгоритмы расчета марковских процессов, вычисление веро-
ятностей переходов, решение систем уравнений. На рис. 1.7 изображен пример
рабочего экрана DSPNexpress, взятый из [76]. На экране изображен один сервер
с ограниченным буфером, периодически выходящий из строя и ремонтируемый.
Модель названа MMPPqueue, так как входной поток описывается пуассоновским
потоком с марковским случайным управлением (Markov Modulated Poisson
Process). Каждая позиция и каждый переход имеют названия (например, Source,
Arrive, Decision, Service). В узле FreeBuffers размещаются К заявок. Пуассоновский
hh Глава 1. Аналитический и имитационный подходы к моделированию
поток представлен переходом Arrive, его интенсивность регулируется маркиров-
кой позиции LOW, реализующей случайное управление. После поломки сервера
частичное обслуживание теряется, и заявка стартует на сервере заново после его
восстановления. Интервалы безаварийной работы распределены экспоненциаль-
но, продолжительность ремонта постоянна.
Рис. 1.7. Пример экрана DSPNexpress
Еще один пакет — MOSEL (Modeling Specification and Evaluation Language) —
также разработан в Германии, в университетах Эрлангена и Нюрнберга, и явля-
ется средством моделирования производительности и надежности вычислитель-
ных, производственных систем и систем связи [104]. Язык моделирования —
главная составляющая пакета MOSEL Если система описана на языке MOSEL,
программное обеспечение выполняет моделирование автоматически. Результаты
собираются в текстовом файле или могут быть представлены графически с по-
мощью специальной утилиты. Ядро MOSEL состоит из набора языковых конст-
рукций, задающих состояния и переходы. Дополнительные синтаксические пра-
вила задают условия, при которых переходы разрешены или запрещены.
Переходы могут происходить мгновенно или после некоторой задержки, описы-
ваемой непрерывной функцией распределения. Помимо экспоненциального,
можно задать распределения Парето, Вейбулла, нормальное, равномерное и др.
Возможны также детерминированные переходы. Язык MOSEL несложен для по-
нимания и содержит много ключевых слов и управляющих структур из языков
1.4. Имитационное моделирование систем 45
общего назначения, что облегчает знакомство с ним. Типичная программа состо-
ит из следующих секций:
О объявление параметров. Содержит описание констант и параметров системы;
О определение компонентов (секция узлов). Каждый узел может содержать не-
которое число работ и иметь заданную емкость;
О определение переходов (секция правил). Здесь описывается динамическое
поведение системы. Правило состоит из условия и действия;
О результаты. Описывает, какие характеристики интересуют аналитика (веро-
ятности состояний, среднее время отклика и др.);
О графика. Необязательная секция с описанием желаемого графического пред-
ставления результатов.
Архитектура моделирующей оболочки MOSEL изображена па рис. 1.8.
Дискретно-событийное
моделирование
Методы численного
анализа
Результаты
(стандартные ) (для данной задачи ) (результаты) ... (файлы)
( sys {res, igl} ]
Рис. 1.8. Архитектура MOSEL
46 Глава 1. Аналитический и имитационный подходы к моделированию
Анализ производительности и надежности, выполняемый с ее помощью, состоит
из следующих этапов:
1. Обследование реально действующей системы и ее высокоуровневое описание
на языке спецификаций.
2. Автоматическая трансляция модели в коды специфической подсистемы.
3. Загрузка нужной подсистемы оболочкой MOSEL.
В зависимости от структуры модели, в частности от типов вероятностных рас-
пределений, анализ проводится одним из следующих способов:
1. Генерация пространства состояний, если удалось построить модель средства-
ми теории очередей.
2. Подготовка модели для имитационного эксперимента (Discrete Event Simu-
lation), если описание аналитическими средствами невозможно.
3. Численный расчет аналитической модели или выполнение имитационного моде-
лирования.
4. Вычисление заказанных характеристик и их запись в файл специального фор-
мата; синтаксический разбор выходных данных и генерация читаемого файла
с результатами для пользователя. В случае, если требуется графическое пред-
ставление, создается файл формата IGL.
Пример моделирования производственной линии с помощью MOSEL приведен
в [54].
В середине 90-х годов в Германии появилась и система PEPSY (Performance Eva-
luation and Prediction System for Queueing Networks), предназначенная для рас-
чета сетей с очередями. PEPSY доступны открытые и замкнутые сети с несколь-
кими классами заявок. Маршрутизация моделируется заданием маршрутных
вероятностей, изменение класса заявки при движении ее в сети не допускается.
Возможные типы узлов и дисциплины обслуживания в них, моделируемые с по-
мощью PEPSY, таковы:
М/М/т - FCFS, M/G/l - PS, M/G/ъ, M/G/i - LCFS, -/ G/m - FCFS;
узлы с дисциплиной относительного или абсолютного приоритета. Длина заявки
задается значением ее первого и второго моментов, то есть интенсивности обслу-
живания и квадрата вариации.
PEPSY реализует более 50 методов исследования стохастических сетей [73]:
О расчет производительности через вероятности состояний;
О расчет производительности для замкнутых сетей методом нормализующей
константы;
О методы, основанные на декомпозиции сетей;
О аппроксимация мультипликативными формами, диффузионная аппрокси-
мация;
О анализ средних.
Для всех методов расчета итоговая таблица отдельного узла содержит следу-
ющие результаты:
1.4. Имитационное моделирование систем 47
О интенсивность выходного потока;
О коэффициент загрузки;
О среднее время отклика;
О среднее число заявок;
О среднее время ожидания;
О среднюю длину очереди.
В каждом узле результаты представлены отдельно для классов заявок и в сово-
купности для всех классов. Работать с PEPSY можно как с помощью командной
строки Unix в ASCII-терминале, так и в специальной оконной оболочке, реали-
зованной в X-Windows.
Еще одна система имитационного моделирования — Arena — разработана компа-
нией Systems Modeling Corporation, ее первая версия появилась в 1993 г. [97],
[99]. Arena снабжена удобным объектно-ориентированным интерфейсом и обла-
дает возможностями адаптации к разным предметным областям. Основа техно-
логий Arena — язык моделирования SIMAN и система Cinema Animation. Про-
цесс моделирования организован следующим образом. Сначала пользователь шаг
за шагом строит в визуальном редакторе модель, затем система генерирует по
ней код на SIMAN, после чего автоматически запускается анимационный модуль
Cinema Animation. Интерфейс включает в себя также средства для работы с дан-
ными — электронные таблицы, ODBC, OLE и др.
Масштабная и многоплановая система ДАСИМ (Диалоговая автоматизирован-
ная система имитационного моделирования) на Unix-платформе с синтакси-
чески развитым языком моделирования разработана в Украине специалистами
Одесского политехнического института [36], [100]. ДАСИМ может применяться
для проектирования локальных и распределенных сетей ЭВМ с коммутацией
пакетов, специализированных мультипроцессорных систем и вычислительных
комплексов, выбора протоколов множественного доступа к каналам связи, ис-
следования широкого класса сетей массового обслуживания большой размерно-
сти, а также производственных процессов и автоматических линий.
ДАСИМ не требует программирования ни на одном из этапов создания модели,
так как ее язык является непроцедурным, его конструкции носят описательный,
а не предписывающий характер. Основное понятие языка ДАСИМ — это поня-
тие предложения. Каждое предложение имеет свой синтаксис, описанный в доку-
ментации. Типы предложений и их назначение приведены в табл. 1.1.
Структурная схема моделирующего алгоритма изображена на рис. 1.9.
Основной принцип моделирования в ДАСИМ — календарно-событийный (event-
driven). Для каждого устройства генерируются время наступления ближайшего
события и само событие, которое ставится в очередь, управляемую календарем.
В процессе слежения управляющая программа просматривает календарь и выби-
рает из него событие, имеющее самое раннее время наступления. После этого
системное время становится равным времени наступления выбранного из кален-
даря события, то есть время изменяется скачком. Альтернативой этому методу
является пошаговое тактирование (можно сказать — непрерывное изменение)
времени (time-driven) без ведения календаря событий. Именно такой подход
применяется в последующих главах этой книги.
48 Глава 1. Аналитический и имитационный подходы к моделированию
Таблица 1.1. Типы предложений в ДАСИМ
Наименование предложения Назначение
Устройство Входящий поток Механизм обслуживания Очередь Идентификация обслуживающего устройства Описание входящих потоков заявок и их свойств Задание дисциплины формирования очередей Описание структуры очередей и ограничений, накладываемых на процесс их формирования
Механизм подключения Время обслуживания Задание стратегии выбора обслуживающего устройства Задание длительности интервалов времен обработки заявок на устройстве
Время отказа Механизм дообслуживания Задание времен отказа и восстановления устройства Описание стратегии обработки заявки, во время обслуживания которой поступил отказ
Выходящий поток Транспортная сеть Описание свойств выходящего потока Задание структуры транспортной сети и механизмов ограничения нагрузки на логические соединения
Время моделирования Задание длительности времени исследования модели
Рис. 1.9. Моделирующий алгоритм ДАСИМ
1.4. Имитационное моделирование систем 49
Для имитации процессов функционирования системы во времени моделиру-
ющий комплекс ДАСИМ включает в себя следующие средства (табл. 1.2).
Таблица 1.2. Моделирующие средства ДАСИМ
Управление календарем Управление информационной базой объекта и взаимодействие с ней
Порождение события Синхронизация процессов
Планирование наступления событий на заданное время Поиск объектов
Установка событий в календарь Генерация случайных чисел
Удаление событий из календаря Сбор и статистическая обработка результатов моделирования
Событийный метод моделирования пользуется несколько большей популярно-
стью у разработчиков, чем временной. Как отмечено в [89], основная причина
этого заключается в противоречивых требованиях к выбору шага изменения вре-
мени Af: из соображения точности он должен быть как можно меньше, что сни-
жает производительность программы и увеличивает длительность моделирова-
ния. Однако, как будет показано в главе 5, временной метод более надежен при
переходе к параллельному моделированию и распараллеливается более естест-
венным образом, чем событийный.
В [90] обсуждаются три пакета моделирования динамических систем, в которых
представление систем основано на диаграммах. Это Stella/iThink (High Perfor-
mance Systems, Inc., 1996), Powersim [101] (Modeldata AS, Bergen, Norway, 1993 и
позднее), Vensim (Ventane Systems Inc., 1998 и позднее). Подробный перечень су-
ществующих пакетов визуального моделирования, предлагаемых на рынке про-
граммного обеспечения, можно найти в [98].
Наконец, нельзя не отметить, что большое количество моделирующих систем,
языков и библиотек разрабатываются на академическом уровне — в стенах уни-
верситетских лабораторий и научных центров. Эти работы являются достоянием
не очень широкого круга специалистов и обсуждаются на страницах специализи-
рованных журналов и на всевозможных тематических конференциях. Некоторые
из выдвигаемых в этих работах идей и методологий в конечном итоге не прохо-
дят проверку временем и отвергаются научным сообществом, другие же показы-
вают свою жизнеспособность и выходят на уровень коммерческого использова-
ния. Количество таких научных проектов очень велико. Вот далеко не полный
перечень названий, встречавшихся за последние 10 лет в мировой печати в об-
ласти компьютерных наук: ABST, ADDSIM, Arachne, CSAM, Dose, GLOMOSIM,
GOLOG, HOPE, EDPEPPS, MARS, Modes, Nexus, OPPSI, PAMELA, PARSEVAL,
POEMS, PORTS, POSS, PRISM, PROSIT, SHIFT, SimPack, Swift, Tactus, Tardis
и множество других.
Все чаще появляются работы, в которых выдвигается концепция универсальной
моделирующей среды. Назревшую необходимость этого авторы [3] мотивируют
так: «При необходимости проведения большого объема исследований наличие
50 Глава 1. Аналитический и имитационный подходы к моделированию
соответствующей специализированной системы или языка моделирования мо-
жет многократно, а иногда и на несколько порядков ускорить процесс исследо-
вания... Последнее обстоятельство привело к появлению чрезвычайно большого
разнообразия различных моделирующих средств и языков, ориентированных на
конкретные области применения. В ежегодном обзоре, приводимом в журнале
«Simulation», уже десять лет назад перечислялось около ста языков и систем,
предлагаемых различными фирмами и организациями. К настоящему времени
размер списка увеличился в несколько раз и имеет устойчивую тенденцию к
дальнейшему росту». Сложившаяся ситуация названа «вавилонским столпо-
творением в моделировании». Авторы формулируют следующие принципы по-
строения универсальной системы моделирования (УСМ):
О четкая модульная структура;
О масштабируемость;
О открытая архитектура;
О иерархия моделей;
О развитый графический интерфейс.
Там же авторы отмечают: «Применительно к моделированию можно утверждать,
что на одном полюсе универсальности находятся языки программирования типа
Fortran, позволяющие при достаточной квалификации в программировании соз-
давать модели практически любого назначения и любой сложности, но ценой,
как правило, значительных затрат времени и труда. А на другом — современные
системы широкого назначения». Заметим, что данная книга находится на первом
из полюсов, только вместо Фортрана следует подставить C++. Еще одна концеп-
ция универсальной системы имитационного моделирования (УСИМ) предложе-
на в [38]. Эту концепцию автор основывает на трех принципах:
О простота, то есть минимум необходимых знаний исследователя о системе
и, как следствие, минимум трудозатрат на разработку модели;
О модульность, то есть единообразное описание всех элементов системы в виде
некоторой стандартной математической схемы;
О универсальность, то есть способность охватить многоуровневые сложные си-
стемы произвольной структуры, элементы которых являются динамическими
системами в широком смысле слова.
Отметим, что информационные порталы, посвященные имитационному модели-
рованию, созданы и развиваются в Украине, в Киеве, под руководством профес-
сора В. Н. Томашевского [105] и в России, в Иваново [92], где описана интерак-
тивная система моделирования деловых процессов Process Model. Интересная
система имитационного моделирования сетей ЭВМ NetStimulator разработана
в России в ЗАО «Самара-Диалог» [94]. NetStimulator предназначен для модели-
рования сетей с пакетной коммутацией и различными методами маршрутизации
пакетов. NetStimulator позволяет задавать топологию сети, способ маршрутиза-
ции пакетов, длины пакетов и распределение их числа в одном сообщении, раз-
мер буфера на узле коммутации, приоритеты сообщений, временные ограниче-
ния на пребывание в сети. Система «понимает» такие аббревиатуры, как RIP,
BGP, OSPF и др. В результате «прогона» модели можно получить:
1.4. Имитационное моделирование систем 51
О средние задержки (время доставки) сообщений различных типов;
О гистограммы и функции распределения задержки сообщений;
О гистограммы плотностей и функций распределения занятой памяти по узлам
коммутации;
О количество сообщений различных типов, дошедших до адресата;
О количество отказов в доставке сообщений по различным причинам (нехватка
памяти, превышение допустимых лимитов времени).
Превосходный обзор технологий современного визуального моделирования дан
в работе [22]. Там же предложена классификация наиболее известных пакетов
на пакеты блочного моделирования, пакеты физического моделирования и паке-
ты, ориентированные на схему гибридного автомата. Авторы этой работы сами
являются разработчиками известной в России моделирующей системы MVS (Model
Vision Studium), созданной на кафедре распределенных вычислений и компью-
терных сетей Санкт-Петербургского государственного технического университе-
та. Эта система описана ими в вышедших за последние годы книгах [5] и [21].
Наконец, мощная профессиональная система моделирования AnyLogic разрабо-
тана российской фирмой «Экспериментальные объектные технологии» [103].
В ее состав входит библиотека Enterprise Library, предназначенная для построе-
ния дискретно-событийных моделей. К ее достоинствам относятся большой
набор моделирующих шаблонов, развитые средства анимации модели, возмож-
ность дополнять библиотеку и настраивать логику моделирования с помощью
собственного программного кода на языке Java. Обращает на себя внимание на-
личие в библиотеке подмножества шаблонов для моделирования сетей и работы
с ресурсами, а также классификация ресурсов на персонал (staff), перемещаемые
(portative) и статические (static).
Закончим на этом краткий обзор известных систем. Среди десятков языков моде-
лирования нет наилучшего, который превосходил бы остальные по всем показа-
телям, — каждый имеет свои достоинства и недостатки, а также четко очерченную
область применимости. Почему же все-таки C++? Автор отдает ему предпочте-
ние по следующим причинам:
1. Выбирая определенный язык и среду моделирования, мы неизбежно оказыва-
емся в рамках тех условностей и ограничений, которые накладываются опи-
санием этого языка. Моделируемая система может иметь такие тонкие осо-
бенности, например дисциплины обслуживания, которые невозможно или
очень трудно отразить предлагаемыми языком синтаксическими средствами.
Любое усложнение системы сразу же порождает опасение — а можно ли бу-
дет это описать? Реализация модели на C++ свободна от этих ограничений,
поскольку мы не связаны никакими условностями и заранее уверены в том,
что сумеем описать все что угодно. Снова обратимся к [90]. На стр. 101 упо-
мянуты пакеты ©RISK (Palisade Corp., 1997), работающий поверх Microsoft
Excel, и Monte Carlo (Primavera, 1995). Эти пакеты имеют встроенные воз-
можности вероятностного анализа. Тем не менее, автор констатирует, что
«системному аналитику может потребоваться и собственное программное
обеспечение или, по крайней мере, он может пожелать дополнить имеющийся
52 Глава 1. Аналитический и имитационный подходы к моделированию
продукт фрагментами собственного кода, чтобы получить от модели желаемую
информацию в полном объеме». Далее, на стр. 102, описав проект нефтяной
установки в Северном море, автор признает, что для возможности представ-
ления его средствами ©RISK пошел на большие упрощения, и делает вывод:
«Для моделирования в условиях более реалистичных предположений о си-
стеме потребуется универсальный язык программирования» (курсив мой —
И. Т.). Анализ этого проекта с помощью C++ будет рассмотрен в главе 14.
2. Высокоуровневая среда моделирования является «черным ящиком» для поль-
зователя — он не знает, по каким алгоритмам происходит моделирование.
Всегда остается опасность того, что исследователь и моделируемая система
не до конца «поняли» друг друга, и результаты — это не совсем то, что хоте-
лось получить изначально. Программирование на C++ вселяет в нас полную
уверенность в том, что мы моделируем в точности то, что нам нужно, и отсле-
живаем именно те характеристики, которые нас интересуют. Имея отлажен-
ную программу, разработчик может чувствовать себя полновластным хозяи-
ном процесса моделирования и «выжать» из нее все, что только возможно.
3. Объектно-ориентированная технология проектирования и реализации про-
граммных продуктов является мощным и эффективным инструментом по-
строения программ моделирования дискретных систем. Удачная объектно-
событийная модель делает процесс программирования довольно легким и
приятным занятием, свойства объектов и законы, по которым они существу-
ют, наглядным и естественным образом переносятся в программный код.
В главе 5, а также во второй части книги автор постарался убедительно про-
иллюстрировать справедливость этого тезиса.
4. Важную роль играет также вопрос доступности средств моделирования. Спе-
циализированные системы часто являются коммерческими, причем стои-
мость их довольно высока и может окупиться только при широкомасштабном
промышленном использовании. Так, автор [90] признает, что у читателей его
книги могут возникнуть проблемы с практической реализацией рассмотрен-
ных примеров, так как, скорее всего, не у всех из них установлен пакет ©RISK.
А ведь при этом он имел в виду не российских читателей, а в основном своих
соотечественников! Компилятор языка C++ и здесь имеет очевидные и неос-
поримые преимущества.
Безусловно, изложенные доводы являются в определенной степени субъектив-
ными и несут на себе отпечаток личного вкуса и опыта практической работы
автора. Ни в коем случае не хочу утверждать, что C++ — это всегда наилучший
выбор. Его использование требует от исследователя высокой квалификации как
программиста-разработчика, а специалист в своей предметной области таковой
в большинстве случаев не обладает. Если некоторая среда моделирования хоро-
шо адаптирована под определенный класс задач и удобна в использовании, нет
смысла «изобретать велосипед», начиная все с нуля. Применение C++ оправ-
дано, когда речь идет о сложных нестандартных системах, только начинающих
рассматриваться специалистами в связи с появлением новых подходов, методов
в науке, а возможно, и новой науки.
Выводы 53
Выводы
В этой главе мы ознакомились с основными понятиями теории массового обслу-
живания и дали краткую сравнительную характеристику двух подходов к моде-
лированию систем — аналитического и имитационного. Можно сделать следу-
ющие выводы:
1. Две методологии моделирования во многом противоположны друг другу —
сильные стороны одной являются слабостями другой, и наоборот.
2. Успех моделирования во многом зависит от опыта и интуиции разработчика,
глубины его проникновения в сущность моделируемой системы и понимания
того, как она интерпретируется в рамках выбранного средства моделирова-
ния.
3. Существуют два метода продвижения модельного времени в имитационной
модели — календарно-событийный и пошаговое тактирование. В первом слу-
чае время изменяется скачками, во втором — непрерывно.
4. Важное значение имеет корректная интерпретация результатов моделиро-
вания.
5. Выбор наиболее подходящего программного обеспечения среди большого
множества альтернатив зависит от существа проблемы, которую требуется
исследовать, и вопросов, на которые исследователь желает получить ответ.
О Одной из таких альтернатив, подходящей в большинстве случаев, но в то же
время требующей достаточной квалификации от разработчика, является уни-
версальный язык программирования высокого уровня C++.
Глава 2
Краткий обзор объектных
возможностей языка C++
Основные вопросы, рассматриваемые
в данной главе:
□ Что такое класс?
□ Описание классов в C++. Объявление
полей данных и методов
□ Дружественные функции и классы:
прямой доступ к закрытым полям
и методам
□ Иерархические системы классов:
наследование
□ Что такое виртуальные функции
и для чего они нужны: связь
с наследованием
□ Абстрактные классы — основа
иерархических систем классов
□ Управление памятью: выделение
и освобождение
□ Перегрузка операций: для разных
классов операции могут работать
по-разному
□ Обработка исключений: исключения
как объекты
□ Шаблоны: тип как параметр
2.1. Классы расширяют C++ новыми типами данных 55
В этой главе мы коротко остановимся (применительно к языку C++) на тех
приемах объектно-ориентированного программирования (ООП), которые пона-
добятся нам при написании программ моделирования систем с помощью объек-
тов. Основная цель главы — освежить в памяти читателя методы программиро-
вания на C++ и дать необходимые разъяснения тем читателям, которые не
слишком уверенно ими владеют. Опытные разработчики, владеющие языком на
уровне, предусматривающем знакомство с [47], могут, разумеется, полностью
пропустить эту главу.
2.1. Классы расширяют C++ новыми
типами данных
Конструкция класса позволяет расширить стандартный набор типов данных
C++ (char, int, float и т. д.) собственными типами. Новый тип данных в C++
можно задать также с помощью перечисления (enum), объединения (union) и
структуры (struct), но главной синтаксической конструкцией, обеспечивающей
наибольшую гибкость представления данных и функциональность работы с
ними, является все-таки класс. По существу, класс — это описание определяемо-
го типа. Если мы описали класс Complex (комплексное число) или Matrix (матри-
ца) в некотором заголовочном файле и включили этот файл в свою программу
директивой #include, то в дальнейшем можем объявлять переменные типа
Complex или Matrix и пользоваться ими в программе наравне с переменными лю-
бых встроенных типов. Переменная, относящаяся к этому новому типу, называ-
ется объектом класса. Так же как и для встроенных типов, для объектов можно
объявлять скалярные переменные, массивы, указатели, массивы указателей,
выделять и освобождать память. Переменные этого типа могут быть, в свою
очередь, полями других классов, передаваться в качестве параметров функций
и возвращаться ими как результат.
Синтаксически определение класса выглядит точно так же, как и определение
структуры, только вместо слова struct используется слово cl ass:
class иня_класса
{ /*: */ }:
Основное же отличие класса от структуры заключается в том, что все поля
структуры — открытые (по умолчанию), а поля класса нужно объявлять как от-
крытые (public), защищенные (protected) и закрытые (private), что позволяет го-
ворить о реализации механизма инкапсуляции данных. Необходимость такого
механизма показывает уже следующий крайне простой пример. Рассмотрим та-
кую простую структуру данных, как замкнутый интервал на координатной пря-
мой. Его описание выглядит так:
struct Interval
{
float left; //левая точка
float right: //правая точка
}:
56 Глава 2. Краткий обзор объектных возможностей языка С++
Естественно, должно выполняться неравенство right > left, иначе интервал не
имеет смысла. Однако пользователю ничто не мешает включить в свою програм-
му следующий код:
Interval А:
A.left-2; A.rlght-1:
В результате будет создана заведомо противоречивая структура данных, и все
операции по ее обработке заведомо бессмысленны. Нетрудно придумать и такой
пример, когда неконтролируемый доступ к полям данных может привести к ава-
рийному завершению программы. Это может произойти хотя бы при назначении
полю «Знаменатель» структуры «Рациональная дробь» значения ноль. Поэтому
доступ к данным надо как-то регулировать, что и позволяет делать конструкция
класса.
2.2. Объявление классов в C++
Итак, класс в C++ служит для абстрактного представления данных разных ти-
пов. Он включает поля данных и методы, которые могут классифицироваться
как закрытые, открытые и защищенные. Закрытые данные и методы доступны
только через методы этого же класса, тогда как открытые доступны для любых
объектов. Открытые методы образуют для объектов класса интерфейс, с помо-
щью которого к ним можно обращаться извне. Защищенные данные и методы
похожи на закрытые, но доступны также для методов производных классов.
Если определение метода класса дается за пределами объявления класса, необ-
ходимо уточнить имя метода с помощью операции расширения области видимо-
сти «::». Имя, стоящее слева от знака операции «::», — это имя класса, а имя,
стоящее справа от него, — метод, определенный в классе. Такая же операция
применяется для ссылки на статические члены класса. Если слева от знака опе-
рации расширения области видимости имени нет, то имя, указанное справа,
представляет собой глобальную переменную или функцию. Например, если есть
глобальная переменная х, а в объявлении класса есть поле х, то методы класса
могут обращаться к глобальной переменной х посредством записи : :х, а к полю
х — с использованием записи х или <иня класса»: :х.
В следующем фрагменте кода (листинг 2.1) приведено описание класса для весь-
ма простого объекта — клетки шашечной доски размером 8x8.
Листинг 2.1. Класс, описывающий клетку шашечной доски
#include<cmath>
#include<cstring>
#include<iostream>
using namespace std;
enum Color {White. Black}:
class Field
{
private:
char letter; //буквенная часть, a-h
int digit: //цифровая часть. 1-8
Листинг 2.1. Класс, описывающий клетку шашечной доски 57
int get_letter_order(): protected: //метод возвращает порядковый номер буквы
static int num_fields; //статический член - счетчик объектов //класса
public:
friend class Checker; friend class Checker_Simple:
friend class Checker_Great;
FieldO //конструктор без параметров.
//создает клетку al
{
letter-’a’:
digit-1:
num_fields++:
}
FieldCcharl. int d); //конструктор с параметрами
-FieldO //деструктор
{
num_fields--:
}
Color get_Color(): //возвращает цвет клетки
void printO: //печать содержимого объекта
static int fieldsO { return num_fields; } //возвращает количество
//объектов
}:
int Field::num_fields-O: //инициализация статического поля
//вне класса
Field::Field(char 1, int d) //конструктор с параметрами, контроль
//данных опущен преднамеренно, см. 2.9
{
letter-1:
digit-d:
num_fields++:
}
int Field::get_letter_order()
{
switch(letter)
{
case ’a': return(l):
case 'b': return(2):
case c': return(3):
case 'd': return(4);
case ’e': return(5):
case T: return(6);
case 'д': return(7):
case ’h’: return(8):
default: break:
}
}
Color Field::get_Color()
{
int kl=get_letter_order():
if ((kl+digit)X2--0) return(Black):
58 Глава 2. Краткий обзор объектных возможностей языка C++
return(White):
}
void Field::print()
{
if (get_Color()“Black) cout « letter « digit « " - black field\n" « flush:
else cout « letter « digit « " - white field\n" « flush;
}
В данном примере поле данных numfields и метод fields О объявлены как стати-
ческие. При таком объявлении объекты получают доступ к единственному эк-
земпляру каждого статического поля данных. Статические поля данных исполь-
зуются как глобальные переменные всеми объектами этого же типа.
Статическому полю Fields: :num_fields по умолчанию присваивается значение О
либо его можно инициализировать другим значением в области видимости про-
граммы. Однако дальнейшее изменение этой переменной в пользовательских
программах должно производиться только через методы класса Field, методы
производных классов или дружественные функции. Статическую переменную
можно использовать и в случае, когда не существует ни одного объекта данного
класса. Статические методы могут обращаться только к статическим полям. Так,
метод Field::fields() не может обращаться к полю Field::letter. В то время как
нестатические методы должны вызываться через объекты класса, статические
методы такого ограничения не имеют. Статические поля данных и методы обычно
используются для отслеживания количества созданных объектов класса, а также
для сбора общих статистических данных.
Методы Field::Field — это конструкторы. Метод-конструктор вызывается при
создании объекта класса и инициализирует поля данных вновь созданного объ-
екта. Конструктор можно перегружать, то есть в одном классе может быть опре-
делено несколько конструкторов с различными прототипами, как в приведенном
примере. Существуют три типа конструкторов. Конструктор по умолчанию не
имеет параметров или все его параметры получают значения по умолчанию, но
вызывается он всегда без параметров. Если в определении класса конструктор не
определен, то автоматически создается конструктор по умолчанию с пустым те-
лом, он не выполняет инициализации объекта. В результате обработки компиля-
тором объявления объекта
Field f:
вызывается конструктор по умолчанию — либо, как в нашем случае, определен-
ный в классе Field, либо созданный компилятором. Второй из конструкторов,
определенных в классе Field, является конструктором инициализации. В соот-
ветствии со своими параметрами он инициализирует состояние объекта, то есть
присваивает значения полям данных. Конструктор копирования имеет один па-
раметр, являющийся константной или обычной ссылкой на объект данного клас-
са, и позволяет при создании объекта инициализировать его значением другого
объекта того же класса. Конструктор копирования, если явно не определен, соз-
дается автоматически и выполняет поэлементное копирование полей одного
объекта в другой. Основной признак необходимости явного написания такого
конструктора — наличие среди полей данных класса указателей. В этом случае
поэлементное копирование неприемлемо, так как поля-указатели двух разных
2.3. Дружественные функции и классы 59
объектов будут указывать на одну и ту же область памяти. В конструкторе же
копирования программист обеспечивает выделение новой области памяти для
данных объекта-копии и программирует перенос в нее информации из объекта-
оригинала.
Метод Field: :-F1eld() — это деструктор. Он вызывается тогда, когда объект
класса должен быть уничтожен, и предназначен для завершения обработки дан-
ных надлежащим образом, например для освобождения динамической памяти
или, как в нашем случае, декремента счетчика объектов. Аргументов деструктор
иметь не может и должен быть единственным в классе. Освобождая память, за-
нимаемую объектом, деструктор уничтожает все результаты работы конструкто-
ра. Обычно тело деструктора пусто, так как система сама освобождает память и
выполняет другие необходимые действия. Но если конструктор явно распреде-
лял память для полей данных объекта с помощью оператора new, в теле деструк-
тора должен быть оператор del ete, освобождающий эту память. Имя деструкто-
ра — это имя соответствующего класса с префиксом «~».
В примере с классом Field реализации одних методов размещены внутри класса,
а реализации других — вне его. В первом случае методы будут использоваться
как встроенные ( или inline-подставляемые) функции, аналогом которых в язы-
ке С являются макроопределения. Преимущество использования inline-функ-
ций состоит в повышении производительности программ благодаря отсутствию
дополнительных затрат на вызовы функции. Подставляемыми могут быть толь-
ко короткие функции, не содержащие управляющих операторов.
2.3. Дружественные функции и классы
Используемая в C++ концепция дружественности позволяет предоставлять пря-
мой доступ к закрытым и защищенным элементам класса из внешних функций и
методов других классов. Этот принцип рассчитан на особые случаи, когда пря-
мой доступ к закрытым данным объекта является для функции более эффектив-
ным, чем доступ через интерфейс его класса. Например, функции, перегружа-
ющие бинарные операции (+, * и т. д.) для классов, описывающих различные
математические структуры (комплексные числа, рациональные дроби, многочле-
ны, матрицы), принято объявлять друзьями этих классов. Прототип такой функ-
ции в протоколе класса выглядит примерно так:
friend Complex operator +(const Complex &a. const Complex &b)
Так как хорошо спроектированные классы должны отражать взаимоотношения
объектов реального мира, которые они моделируют, то и свойство дружествен-
ности следует вводить в программу в том случае, если оно имеет реальный ана-
лог. Если у объекта типа А в жизни «нет тайн» от объекта типа В, то есть объект
типа В по роду своей деятельности знает все об А и может делать с ним все, что
сочтет нужным, то класс В должен быть объявлен дружественным классу А. Но,
в отличие от жизни, это отношение не является симметричным — человек управ-
ляет автомобилем и знает о нем все, но не наоборот.
60 Глава 2. Краткий обзор объектных возможностей языка C++
В примере с классом Field дружественным этому классу следует объявить класс,
описывающий объекты, которые мы ставим на игровые клетки, например, шаш-
ки. Для того чтобы шашка сделала ход, она должна проверить его допустимость,
для чего необходим полный доступ к полям данных каждой клетки. Чтобы объя-
вить класс Checker (шашка) другом класса Field, в протоколе класса Field сразу
после ключевого слова public следует вставить строку friend class Checker;.
2.4. Наследование классов в C++
Наследование позволяет порождать класс от одного или нескольких существу-
ющих классов. Новый класс называется подклассом (или производным классом),
а класс, от которого он произведен, — базовым, или родительским, классом. Под-
класс наследует всем защищенным и открытым полям данных и методам базово-
го класса и может обращаться к ним. Кроме того, в подклассе могут определять-
ся другие поля данных и методы, уникальные для него самого.
В объявлении подкласса имя базового класса может предваряться модификато-
ром доступа производного класса к элементам базового класса, имеющим воз-
можные значения public, private или protected:
class «имя производного класса*: «модификатор доступа*] <имя базового класса*
Это означает, что открытые поля и методы базового класса следует считать в под-
классе, соответственно, открытыми, закрытыми или защищенными. Если моди-
фикатора доступа нет, по умолчанию они являются закрытыми. Когда определят-
ется объект подкласса, то функции-конструкторы этого подкласса и его базового
класса вызываются в следующем порядке:
1. Конструкторы базового класса в последовательности, указанной в объявле-
нии подкласса.
2. Конструкторы полей-объектов в последовательности, объявленной в под-
классе.
3. Конструкторы подкласса.
При вызове конструктора подкласса данные, подлежащие передаче в конструк-
тор его базового класса, и конструкторы его полей данных указываются в списке
инициализации конструктора подкласса. Этот список указывается после списка
аргументов метода-конструктора подкласса, но до определения тела метода:
«подкласс*::<подкласс*(<список аргументов*):«список инициализации* { /*тело*/ }
«список инициализации* можно представить так:
«имя класса*(«аргумент*) [.«имя класса*(аргумент>)]+
Конструктор подкласса должен передать часть аргументов конструктору базово-
го класса, чтобы тот произвел необходимую инициализацию. Ее нельзя осущест-
вить в теле конструктора подкласса, так как конструктор базового класса зарабо-
тает раньше, чем начнет выполняться тело конструктора подкласса. Вызов
конструктора базового класса производится в списке инициализации (лис-
тинг 2.2). Кроме того, инициализацию собственных переменных подкласса раз-
решается проводить не в теле конструктора, а непосредственно в списке.
2.5. Виртуальные функции 61
Определим класс Шашка и производный от него класс Данка. Поля данных шаш-
ки — клетка, на которой она стоит, и ее цвет. У дамки сверх того — клетка, на ко-
торой она превратилась в дамку. Оба класса по-разному реализуют метод Ход,
в результате выполнения которого клетка дислокации шашки изменяется. Реа-
лизация этого метода будет приведена позднее — в окончательном варианте.
Листинг 2.2. Класс «Простая шашка»
class Checker_Simple
{
Field field: //клетка доски, на которой стоит шашка
Color color: //цвет шашки
public:
Checker_Simple(Fields f. Color c):
int moveCFieldS f):
void printO:
}:
// Производный класс "Дамка"
class Checker_Great: public Checker_Simple
{
Field transform; //клетка превращения простой шашки в дамку
public:
Checker_Great(Fields f. Color c. char a. int b): Checker_Simple(f. c).
transform(Field(a.b)) {} //конструктор co списком инициализации
int move(FieldS f):
void printO;
}:
2.5. Виртуальные функции
Представим следующую ситуацию. С помощью графического интерфейса поль-
зователь поставил на некоторую клетку доски шашку, после чего хочет сделать
ею ход, указав мышью клетку, на которую он хочет эту шашку переставить. Про-
граммируя эту ситуацию, мы не можем заранее знать, какую шашку выставит
пользователь — простую или дамку. Поэтому возникает вопрос — какую из реа-
лизаций метода move О вызывать — для базового или для производного класса?
Дело в том, что при открытом наследовании указатель (ссылка) на объект произ-
водного класса является указателем (ссылкой) также и на объект базового класса,
но не наоборот. Поэтому, возвращаясь к примеру с шашками, какую бы шашку
ни поставил пользователь, на нее можно ссылаться через переменную (напри-
мер, с именем Sh) типа CheckerSimple*. Но если для того чтобы сделать ход, мы
напишем
Sh->move(f),
где f — ссылка на Field, этот вызов будет связан компилятором именно с мето-
дом moved класса Checker Simple, ведь компилятор не может зйать, на объект ка-
кого класса будет при выполнении программы реально указывать Sh.
Одно из решений — хранить в протоколе класса Checker Simple поле данных
с именем класса. Тогда мы сможем записать что-то наподобие следующего:
62 Глава 2. Краткий обзор объектных возможностей языка C++
switch Sh->class:
case Simple: Sh->Checker_S1mple::move(f):
case Great: Sh->Checker_Great::move(f):
Но такой подход явно «не в духе» ООП. В самом деле, одно из основных пре-
имуществ ООП — простота сопровождения программ и минимальная переделка
ранее написанного кода. Предположим теперь, что появился еще один вид шаш-
ки, описываемый своим производным классом со своим методом move(). В этом
случае весь ранее написанный код придется просмотреть на предмет нахождения
в нем таких switch-case- или if-конструкций и дописать всюду еще одну строчку
case или 1 f, а затем перекомпилировать.
Разумеется, в C++ существует более изящное решение. Все, что требуется для
решения проблемы, — дописать перед объявлением метода move() в базовом классе
ключевое слово virtual:
virtual lot move(Field& f):
Виртуальная функция позволяет реализовать на C++ известный в теории объ-
ектно-ориентированного программирования принцип подстановки. Существуют
различные варианты его формулировок, один из них звучит так: объект произ-
водного класса является объектом базового класса (но не наоборот). Рассмотрим
принцип подстановки применительно к следующей ситуации. Пусть имеется ба-
зовый класс А и производный от него класс В. Можно сказать, что каждому объ-
екту b типа В может быть сопоставлен некоторый объект а типа А, полученный
выделением у объекта b только тех полей данных и методов, которые унасле-
дованы от класса А. Тогда поведение программы, работающей с объектом Ь, не
должно измениться, если в текст программы вместо b подставить а. Следует так-
же отметить, что в качестве а и b могут выступать сами объекты, указатели или
ссылки. Так, например, если классы А и В имеют по-разному реализованный
метод functionO, то фрагмент кода
В Ь:
А 8а-Ь:
a. functionO:
b. functionO;
приведет к вызову в обоих случаях метода В::functionO, но при двух существен-
ных оговорках:
1. Класс А является открытым базовым классом для класса В (иначе компилятор
«не пропустит» код А &а=Ь;).
2. Метод А::functionO объявлен виртуальным (иначе a.functionO приведет к вы-
зову метода А:: functionO для объекта Ь).
Ключевое слово vi rtual инструктирует компилятор о том, что на этапе компиля-
ции связывание вызова метода с конкретным методом (так называемое раннее
связывание) делать не надо. Вместо этого на этапе выполнения будет произведе-
но так называемое позднее связывание, когда конкретный класс созданного объ-
екта уже будет известен (листинг 2.3). Использование виртуальных функций
позволяет не модифицировать ранее написанный код при появлении новых про-
изводных классов — их протоколы просто компилируются и включаются в биб-
лиотеку классов. Автору нового класса нет необходимости изучать, что и как на-
писали его предшественники.
2.5. Виртуальные функции 63
Листинг 2.3. Пример фрагмента кода, использующего вызовы виртуальной функции
int main()
{
Field fl('d'. 4). f2('h'.8). f3('e'. 5):
Checker_Simple a(fl. White):
Checker_Great b(f2. Black):
Checker_S1mple *p-&b:
b->move(f3):
p-&a:
a->move(f3):
return 0:
}
Таким образом, понятие виртуальной функции тесно связано с наследованием
классов, а ее типичным использованием является вызов через указатель на объ-
ект базового класса. Например, в языке Java в силу соображений безопасности
нет указателей, поэтому там нет смысла разделять функции на обычные и вирту-
альные — виртуальными по умолчания являются все методы классов.
Можно, однако, привести пример и такой ситуации, когда классы всех объектов
в программе задаются явно и известны на этапе компиляции, но, тем не менее,
необходимость объявить функцию виртуальной остается (листинг 2.4) (см., на-
пример, [42]).
Листинг 2.4. «Нетипичный» пример использования виртуальной функции
class Base
{
public:
virtual int fCconst int &d)
{
return d/2:
}
int CallFunction(const int &d)
{
return f(d)*f(d):
}
}:
class Derived: public Base
{
public:
int ficonst int &d)
{
return dX2:
}
}:
int main(void)
{
Base A:
Derived B:
printf("Al=fcd. BMd\n", A.CallFunction(5). B.CallFunction(5)):
}
64 Глава 2. Краткий обзор объектных возможностей языка C++
При запуске эта программа выдаст: А1=4, В1=1. Если же из прототипа функции f
убрать слово virtual, результат будет таким: А1=4, В1=4.
Заметим, что при определении виртуальной функции в производном классе сло-
во virtual писать не обязательно, но желательно — для повышения читаемости
программного кода.
2.6. Абстрактные классы
Абстрактным называется класс, служащий базовым для других классов. Он со-
держит неполную спецификацию своих методов, поэтому для абстрактного клас-
са никаких объектов создать нельзя. Компиляторы отслеживают это ограниче-
ние. В абстрактном классе объявляется одна или несколько чисто виртуальных
функций; эти функции не имеют реализаций, и все подклассы этого абстрактно-
го класса должны их переопределять. Для объявления чисто виртуальной функ-
ции используется следующий синтаксис:
virtual void move()-0:
Подкласс, не реализовавший хотя бы одну чисто виртуальную функцию, тоже
будет абстрактным. Абстрактный класс должен содержать как минимум одно
объявление чисто виртуальной функции.
Отметим, что особое место при описании абстрактных классов занимает метод-
деструктор, который, в отличие от конструктора, может быть виртуальным и
даже чисто виртуальным. Чисто виртуальный деструктор должен обязательно
иметь реализацию (так как внутри деструктора производного класса всегда вы-
зывается деструктор базового класса), но класс, тем не менее, остается абстракт-
ным.
Для чего же нужны абстрактные классы? Вернемся к нашему примеру с шашка-
ми. Построенная нами иерархия наследования, с точки зрения формальных пра-
вил C++, конечно же, правильна, она будет компилироваться и даже кое-как ра-
ботать. Но более или менее опытному проектировщику, знакомому с шашечной
игрой, она будет резать глаз. Почему? Дело в том, что наследование классов
должно отражать отношение «является» между реальными объектами этих клас-
сов. Увы, дамка простой шашкой не является, поэтому отношение наследования
здесь неправомерно. С логической точки зрения, между простой шашкой и дам-
кой должна существовать не вертикальная (родитель — потомок), а горизонталь-
ная связь (потомки одного родителя), так как они обе — шашки, и ни простая
шашка не есть обобщение понятия дамки, ни наоборот. Неправомерность насле-
дования становится очевидной и с точки зрения реализации — у простой шашки
должен существовать метод «Превращение в дамку», наследование которого
классом «Дамка» просто абсурдно. Требуется иное решение, которое учло бы
и тот факт, что между простой шашкой и дамкой все-таки есть много общего,
а именно: наличие полей данных «Клетка» и «Цвет». И здесь на помощь прихо-
дит идеальная развязка такой ситуации — абстрактный класс.
Приведенный далее код описывает совокупность трех классов. Это абстрактный
класс «Шашка» с полями данных «Клетка», «Цвет» и чисто виртуальной функ-
Листинг 2.5. Абстрактный класс «Дамка», не имеющий экземпляров объектов 65
цией «Ход». От него наследуются два подкласса — «Простая» и «Дамка», кото-
рые каждый по-своему реализуют эту функцию. Кроме того, подклассы могут
определять какие угодно собственные методы, например «Превращение в дам-
ку» для простой шашки. Теперь никаких противоречий нет. Такой же подход, за-
метим, годится и для шахмат, только там подклассов будет не два, а шесть —
пешка, ладья, конь, слон, король, ферзь (листинг 2.5).
Листинг 2.5. Абстрактный класс «Дамка», не имеющий экземпляров объектов
class Checker
{
Field field: //клетка доски, на которой стоит
//шашка
Color color: //цвет шашки
public:
CheckerCconst Field& f. Color с): //метод-конструктор
Fields getfieldO { return field: } //получить клетку (inline-функция)
Color get_color() { return color: } //получить цвет (Inline-функция)
virtual bool move(const Fields f)-0: //чисто виртуальная функция Ход.
//не имеющая реализации в этом
//классе
virtual void print()-0: //чисто виртуальная функция
//для вывода содержимого объекта
};
Checker::Checker(const Fields f. Color c): color(c). field(Fleld(f.letter, f.digit))
{
if (f.get_color()“White) //шашка не может стоять на белой
//клетке
{
cout « "Checker can not be set on the white field" « endl:
exit(l): //не лучшее решение!
} else:
if ( (f.digit“8) SS (color—White) ) //белая шашка не может стоять
//на восьмой горизонтали
{
cout « "White checker can not be set on the eighth line" « endl:
exit(l):
} else:
if ( (f.digit—1) SS (color—Black) ) //черная шашка не может стоять
//на первой горизонтали
{
cout « "Black checker can not be set on the first line" « endl;
exit(l);
} else:
}
//производный класс «Простая шашка»
class Checker_Simple: public Checker
{
public:
Checker_Simple(Fields f. Color c): Checker(f. c) {}
int move(FieldS f):
void printO:
}:
66 Глава 2. Краткий обзор объектных возможностей языка C++
//Реализация метода move. Возвращает true, если ход допустимый, иначе -
//false
bool Checker_Simple::move(Field& f)
{
1nt kl=get_f1eld().get_letter_order():
Int k2-f.get_letter_order():
if (get_color()—White)
if ((abs(k2-kl)-=0) || ((f.digit-get_field().digit)-=O)) return(false):
else:
else
if ( (abs(k2-kl)—0) || ((get_fieldO.digit-f,digit)=-0) ) return(O):
get_fi eld().1etter=f.1etter:
get_fieldO.di git-f.digit:
return(true):
}
//Распечатка содержимого объекта
void Checker_Simple::printO
{
cout « "I am the Simple Checker" « endl:
if (get_color()=-White) cout «"My color is White"«endl:
else cout « "My color is Black" « endl:
cout «"My current field is:” «endl:
get_fi el dO. printO:
}
//производный класс «Дамка»
class Checker_Great: public Checker
{
Field transform: //клетка превращения простой шашки в дамку
public:
Checker_Great(Field& f. Color c. char a. int b): Checker(f. c). transform(Field(a.b))
{
}
bool move(Field& f):
void printO:
}:
//реализация метода «Ход» для «Дамки»
bool Checker_Great::move(Field& f)
{
int kl=get_field().get_letter_order():
i nt k2-f.get_letter_order():
if ( abs(k2-kl)==abs(f.digit-get_fieldO.digit) ) : else return(O):
get_f1eld 0.1etter-f.1etter:
get_fi eld().digi t-f.di gi t:
return(l):
}
//Вывод содержимого объекта «Дамка»
void Checker_Great: :printO
{
cout « "I am the Great Checker" « endl:
if (get_color()“White) cout «"My color is White"«endl:
else cout « "My color is Black" « endl:
cout «"My current field is:” «endl:
get_field().printO:
}
Листинг 2.5. Абстрактный класс «Дамка», не имеющий экземпляров объектов 67
int Field::num_fields-O:
//Пример использования описанных классов
int mainO
{
Field fl('d'. 5):
fl.print!):
//Обращение к статическому методу помимо объекта
cout « "Total: “«Field:: f 1 el ds () «endl:
Field f2('h’. 2):
f2.print():
//Обращение к статическому методу через объект
cout « "Total: "«f2.fi el ds О «endl:
Checker_Simple chl(f2. White):
Checker *ptr=&chl: //инициализация указателя на объект абстрактного
//класса
Field f3Cg’. 3):
cout « endl « "Before the move:" « endl:
ptr->print(): //этот вызов связывается с функцией только на этапе
//выполнения. Местоположение шашки до хода
int k-ptr->move(f3): //этот вызов также связывается только на этапе
//выполнения
if (к)
{
cout « endl « "After the move:" « endl:
ptr->pnnt(): //местоположение шашки после хода
}
else {
cout « "Illegal move:":
(ptr->get_field()).print!):
cout «
f3.print!):
cout « endl:
}
Checker_Great ch2(f2. White, 'f'. 8):
ptr-&ch2:
Field f4('b‘. 8):
cout « endl « "Before the move:" « endl:
ptr->print():
k-ptr->move(f4):
if (k)
{
cout « endl « "After the move:" « endl:
ptr->print():
}
else {
cout « "Illegal move:";
(ptr->get_fi eld()).pri nt():
cout «
f4.print!):
cout « endl:
}
Field f5('j'. 5):
Field fGCa'. 9):
}
68 Глава 2. Краткий обзор объектных возможностей языка C++
Использованные в этом примере вызовы функции exit О — не лучшее решение
проблемы аварийного завершения программы в случае некорректных данных.
Подробнее этот вопрос будет рассматриваться в 2.9.
2.7. Операции new и delete
В C++ можно создавать объекты динамически — это делается посредством опе-
рации new. Эта операция выполняет две функции: выделяет память для объекта
и вызывает конструктор для инициализации полей объекта. Если данные ини-
циализации не указаны, то вызывается конструктор без аргументов. Операция
возвращает адрес созданного объекта. Если память выделить не удалось, то кон-
структор не вызывается, а генерируется исключение bad alloc. Возвратить па-
мять и уничтожить объект можно операцией delete.
Например, можно объявить указатель на объект класса Field и инициализиро-
вать его следующим образом:
Field *field=new FieldC'e’. 5):
С помощью операции new можно создать массив объектов. Для этого нужно ука-
зать имя класса и в квадратных скобках — число объектов в массиве:
Checker *Posltlon-new Checker[10J:
Чтобы инициализировать объекты массива, выделенные операцией new, класс
этого объекта должен иметь конструктор без аргументов. Этот конструктор вы-
зывается по умолчанию для каждого элемента массива. Массив, созданный та-
ким образом, называется динамическим. Замечательной особенностью такого
способа создания массива является то, что количество элементов можно зада-
вать выражением, вычисляемым во время выполнения программы. Обращаться
к элементам динамического массива можно точно так же, как и к элементам
обычного, например Position[5] или Position[i+2].
Память, выделенная для объекта операцией new, возвращается операцией delete.
Эта операция фактически уничтожает динамический объект. Например, чтобы
удалить объект класса Checker Simple, на который указывает указатель р,
checker *p-new Checker_Simple(fl. White):
нужно сделать следующее
delete р:
Для уничтожения динамического массива применяется другая форма операции
delete — с квадратными скобками. Например, для уничтожения созданного ди-
намического массива Position необходимо написать оператор
delete[] Position:
2.8. Перегрузка операций
C++ позволяет определять стандартные встроенные операции для работы с клас-
сами. Это дает возможность использовать объекты классов практически так же,
2.8. Перегрузка операций 69
как и объекты встроенных типов. Перегрузка операций наглядно демонстриру-
ется в следующем примере (листинг 2.6).
Листинг 2.6. Класс, описывающий календарную дату
class Data //класс описывает обычную
//календарную дату
{
private:
int day: //число
int month; //месяц, целое число в диапазоне
//0-11
int year: //год
int dni[12]: //количество дней в месяцах
public:
DataCint a. int b. int с) //конструктор инициализации
{
year=a: month=b: day-c:
dni[01=31:
if (((year % 100) !- 0) && ((year % 4) — 0) || ((year % 400) — 0))
dni[l]=29: else dni[l]=28:
dni[2]=31: dni[3]=30: dni[4]=31: dni[5]-30:
dni[6]=31: dni[7]=31: dni[81=30: dni[9]-31:
dni[10]=30: dni[ll]-31:
}
DataO //конструктор по умолчанию,
//инициализирует объект датой
//1 января 2000 года
{
year=2000: month=0: day-1:
dni[01-31:
dni[1]=29:
dni[21=31: dni[3]=30: dni[4]=31: dni[5]=30:
dni[6]=31: dni[7]-31: dni[8]=30: dni[9]-31:
dni[10]=30: dni[ll]-31:
}
Data& operator++() //увеличение на единицу. Отвечает
//на вопрос - какая дата будет
//завтра?
{
if (day < dni[month]) day++: //завтра месяц еще не закончится
else if (month < 11) { day=l: month++: } //завтра будет уже другой
//месяц
else { day-1: month=0: year++: } //завтра будет уже другой год
return(*this):
}
Data& operator--() //а какая дата была вчера?
{
if (day > 1) day--; //вчера был тот же месяц
else if (month > 0) { month--: day=dni[month]; } //сегодня - первое
//число
else { day=31; month=ll; year--; } //сегодня - 1 января
}
70 Глава 2. Краткий обзор объектных возможностей языка C++
//Другие методы, не являющиеся операторами. - селекторы членов класса.
//вычисление дня недели и т.д.
friend bool operator < (const Data& dl. const Data& d2) //предшествует ли
//первая дата второй?
{
if (dl.year < d2.year) return(true): //даты - в разных годах
else if (dl.year — d2.year)
if (dl.month < d2.month) return(true): //даты - в разных месяцах
//одного года
else if (dl.month — d2.month)
if (dl.day < d2.day) return(true): //даты - в одном месяце
//одного года
return(false):
}
friend bool operator -- (Data dl. Data d2) //одинаковы ли даты?
{
if ((dl.year — d2.year) && (dl.month — d2.month) &&
(dl.day — d2.day)) return(true):
else return(false);
}
friend int operator - (Data d2. Data dl) //сколько дней прошло
//от одной даты до другой?
{
int i. sum:
if (d2<dl) return(-l): //ошибка, первая дата предшествует второй.
//вычитать нельзя, это неявный вызов метода
//operator <
if (d2.year — dl.year)
if (d2.month — dl.month)
return(d2.day-dl.day): //один год и один месяц
else //один год. разные месяцы
{
sum-dl.dni[dl. month]-dl.day+d2.day:
for (i-dl.month+1: i<-d2.month-1: i++)
sum+=dl.dni[i];
return(sum):
}
else //разные годы
{
sum=dl.dni[dl.month]-dl.day:
for (i-dl.month+1: i<12: i++)
sum+-dl.dni[i]:
for (i-dl.year+1: i<-d2.year-l: 1++)
{
if ( Ш -- 0 ) sum+-366: else sum+=365:
}
for (i-0: i<d2.month: i++)
sum+-d2.dni[i]:
sum+-d2.day:
return(sum):
}
}
}:
2.8. Перегрузка операций 71
В этом примере определяется класс Data, который соответствует календарному
понятию даты, — число, месяц, год. Объектами этого класса можно оперировать
с помощью операций, которые объявлены как перегруженные функции. Они де-
лают объекты класса Date более доступными для понимания и использования в
том смысле, в котором они понимаются в повседневной жизни (фразы «через
столько-то дней», «столько-то дней назад», «с тех пор прошло столько-то време-
ни» и др.). Подобные вопросы можно поставить и для понятия «Время» и пере-
грузить для этого класса те же операторы, но логика их работы будет уже другой
(листинг 2.7).
Листинг 2.7. Класс, описывающий время
class Time
{
private:
int hour: //часы
int minute: //кинуты. Секунды не описаны - не всегда они и нужны,
//например, если данный класс нужен нам для работы
//с железнодорожным или авиарасписанием
public:
. .................. здесь идут конструкторы
friend bool operator < (const Time& vl. const Time& v2) //предшествует
// ли один момент времени другому в предположении, что оба принадлежат одним // суткам
{
if (vl.hour < v2.hour) return(true); //разные часы
else if (vl.hour-v2.hour)
if (vl.minute < v2.minute) return(true): //один и тот же час
return(false):
}
friend Time operator - (const T1me& v2. const Time& vl) //сколько прошло
//минут?
//Предполагается, что vl физически предшествует v2. но не более чем
//на одни сутки
{
Time v3(1.2):
if (v2>vl) //моменты времени относятся к одной календарной дате
{
if (v2.hour > vl.hour)
{
if (v2.minute > vl.minute)
{
v3.hour-v2.hour-vl.hour;
v3.mi nute-v2.mi nute-v1.minute:
}
el se
{
v3.hour-v2.hour-vl.hour-1:
v3.mi nute-60-vl.mi nute+v2.mi nute:
}
}
else
72 Глава 2. Краткий обзор объектных возможностей языка C++
{
v3.hour=0:
v3.mi nute-v2.mi nute-vl.mi nute:
}
}
else //момент v2 относится к "завтрашней" календарной дате
//по отношению к vl
{
if (v2.minute > vl.minute)
{
v3.hour-24-vl.hour+v2.hour;
v3.mi nute-v2.mi nute-vl.mi nute:
}
el se
{
v3.hour-24-vl.hour+v2.hour-1;
v3.mi nute-60-vl.mi nute+v2.mi nute:
}
}
}
return(v3):
}:
2.9. Обработка исключений
В реализации конструктора для класса Checker нами была предусмотрена провер-
ка на легальность создаваемого объекта — недопустимые сочетания значений по-
лей данных отбрасываются. Такую же проверку следовало бы производить и
в конструкторе класса Field, однако мы отложили этот вопрос до нынешнего мо-
мента, чтобы проиллюстрировать альтернативную возможность контроля дан-
ных — обработку исключительных ситуаций.
В C++ есть стандартный метод обработки исключительных ситуаций, с помо-
щью которого программы реагируют на нарушения нормального хода выполне-
ния. Несмотря на то что мы привыкли говорить о ситуациях, в C++ принято го-
ворить об исключениях. Исключение — это объект, сам факт генерации которого
говорит о возникновении исключительной ситуации. Такой подход очень удо-
бен, так как наличие развитых средств работы с объектами позволяет использо-
вать их и при обработке исключений. Объект-исключение, так же как и обычные
объекты, можно объявлять как переменную, передавать в функцию как аргумент
и возвращать в качестве результата, назначать элементом массива и полем дан-
ных другого объекта.
Общая схема обработки исключений такова: в одной части программы, где обна-
ружена невозможность выполнения (некорректные данные, недостаток или от-
сутствие ресурса и т. п.), исключение порождается; другая часть программы
контролирует возникновение исключения, фиксирует факт его возникновения
и обрабатывает его. В C++ есть три зарезервированных слова: try, catch, throw, —
которые и используются для организации процесса обработки исключений.
2.9. Обработка исключений 73
Исключение генерируется при помощи оператора throw, а перехватывается и об-
рабатывается в секции-ловушке catch. Если блок catch не завершает работу про-
граммы, то управление передается на первый оператор после всех блоков catch.
Контролироваться генерация исключения может только в том случае, если стро-
ка кода — «виновница» исключения — находится внутри блока try. Рассмотрим
пример (листинг 2.8).
Листинг 2.8. Обработка исключительной ситуации
Field::Field(char L. int d) //конструктор с параметрами
{
try
{
if (strchrCabcdefgh". D--NULL) throw "Illegal letter”: //недопустимая
//буква
letter=L:
try
{
if ((d<l) || (d>8)) throw "Illegal digit": //недопустимая цифра
digit-d:
}
catch(const char *msgl) //обработчик
//для недопустимой цифры
{
cerr « “Exception: " « msgl « endl;
}
}
catchlconst char *msg2) //обработчик
//для недопустимой буквы
{
cerr « "Exception: " « msg2 « endl;
}
num_fields++:
}
Оператор throw имеет следующий синтаксис:
throw «выражение»:
где «выражение» — это любое выражение C++, которое при вычислении дает зна-
чение одного из базовых типов данных C++ или объект класса. Тип объекта-ис-
ключения используется для выбора блока catch, тип параметра которого совмес-
тим с типом выражения. Если тип параметра секции-ловушки не является
встроенным типом, то можно передать в блок обработки дополнительную ин-
формацию в полях данных класса, которая может использоваться catch-блоком
при обработке исключения. Если оператор throw выполняется, то остальные опе-
раторы в блоке try пропускаются и управление передается в выбранный блок
catch. Заметим, что catch не является функцией — это просто набор операторов
C++, которые объединены в группу для каждого типа исключений.
После прерывания try-блока выполняются следующие действия:
О создается временный объект типа Т, инициализируемый значением выра-
жения в операторе throw. Для создания объекта вызывается конструктор
74 Глава 2. Краткий обзор объектных возможностей языка Син-
копирования: явно — при его наличии в определении типа Т и автоматиче-
ски — при отсутствии;
О запускается проход по стеку вызовов. При этом могут происходить выходы
из блоков и вызванных функций, следовательно локальные объекты должны
уничтожаться, для чего при необходимости по ходу поиска соответствующего
блока catch вызываются деструкторы;
О находится первый из catch-блоков, в котором объявлен тип исключения, со-
вместимый с типом сгенерированного исключения. Ему и передается управ-
ление, а в качестве параметра выступает созданный объект типа Т. Если соот-
ветствующий catch-блок не найден, вызывается специальная функция termi nateO,
завершающая обработку.
2.10. Шаблоны
Алгоритмы обработки многих известных в информатике структур данных опре-
деляются только логикой организации самой структуры и не зависят, либо за-
висят очень слабо, от конкретного типа данных, которыми они заполнены. Это
замечание относится к спискам, массивам, множествам, стекам, очередям, де-
ревьям. Разумеется, все эти структуры весьма заманчиво было бы оформить в
виде библиотечных классов C++. Но как описать тип их элементов? Конкретно
поведем речь об одной из самых простых и в то же время наиболее распростра-
ненной структуре данных — односвязном списке. Известные средства допускают
две возможности:
О создать отдельный класс для каждого типа — «Список целых», «Список дей-
ствительных», «Список строк» и т. д. Недостатки: дублирование кода, необ-
ходимость размножения вносимых в класс корректировок для всех вариантов
реализаций, необходимость создания все новых и новых классов при появле-
нии новых типов;
О использовать в качестве элемента данных бестиповый указатель void*. Недо-
статки: необходимость явного приведения указателя к нужному типу и, как
следствие, невозможность гарантировать надежную работу кода в пользова-
тельских программах. Программист, которому требуется в рамках некоторого
проекта написать функцию по обработке массива объектов, может справедли-
во полагать, что все объекты массива должны иметь тип А и явно приводить
void* к указателю на этот тип, а на самом деле в массив кто-то по ошибке за-
нес объект типа В, и выполнение программы станет непредсказуемым.
Как видим, ни один из этих вариантов нельзя признать элегантным с точки зре-
ния объектно-ориентированной технологии программирования. Требуется более
изящное решение, в котором тип выступал бы в качестве параметра класса, по-
добно тому, как различные данные выступают в качестве параметров функций.
Такое решение, конечно же, было найдено, и имя ему — шаблоны. Шаблоны пред-
ставляют собой специальные классы или функции с подставляемым типом дан-
ных. Эта подстановка происходит при использовании шаблона в пользователь-
ском коде. Рассмотрим пример реализации шаблонов для списка (листинг 2.9).
2.10. Шаблоны 75
Этой структуре данных мы уделяем особое внимание, так как активно будем поль-
зоваться ею для хранения объектов при разработке моделей для задач, рассмат-
риваемых во второй части книги, header-файл, который мы сейчас напишем, мож-
но будет использовать всюду, где есть необходимость работать со списками —
неважно чего именно — целых чисел, кроликов, собачек или океанских лайнеров.
Листинг 2.9. Файл List.h. Шаблон связного списка и алгоритмы его обработки
• template <class Type> //это постоянная «заставка» //к классам и функциям //с параметризованным типом
class ListNode {
private:
ListNode<Type> *next: //указатель на следующий //элемент списка
Type *data: //указатель на данные. //хранящиеся в элементе списка
public:
ListNode(Type *d. ListNode<Type> *n): //конструктор
-ListNodeO: //деструктор
Type *Data(): //метод для чтения данных
Li stNode<Type> *Next (); //метод для чтения указателя //на следующий элемент
void PutNext(ListNode<Type> *n): //метод для записи указателя //на следующий элемент, //альтернатива методу - //объявление функций ListAdd //и ListDelete дружественными //классу ListNode
void PrintO: //печать содержимого элемента //списка
template <class Type>
ListNode<Type>::ListNode(Type *d. ListNode<Type> *n) : next(n). data(d)
{
}
template <class Type>
ListNode<Type>::-ListNode()
{
delete data:
}
template <class Type>
Type *ListNode<Type>::Data()
{
return data:
}
template <class Type>
Li stNode<Type> *Li stNode<Type>::Next()
{
return next:
}
template <class Type>
void L1stNode<Type>::PutNext(ListNode<Type> *n)
{
76 Глава 2. Краткий обзор объектных возможностей языка C++
next=n;
}
template <class Туре>
void ListNode<Type>:: PrintO
{
data->Print(): //предполагается наличие метода PrintO для класса.
//имя которого будет подставлено в пользовательском коде
}
//Описание класса-шаблона завершено, далее идут функции-шаблоны, работающие
//не с отдельным элементом, а со всем списком
template <class Туре>
void ListAdddistNode<Type> *head. ListNode<Type> *11)
//Добавление нового элемента 11 в хвост списка с головой head
{
ListNode<Type> *old_tail. *v:
// ищем нынешний хвост списка
for (v-head: v!-NULL: v=v->Next())
old_tail-v:
old_tall->PutNext(li); //добавляем вслед за найденным хвостом новый
// элемент
}
template <class Туре>
L1stNode<Type> *ListDelete(ListNode<Type> *head. ListNode<Type> *11)
//Удаление элемента 11 из списка с головой head
//Функция возвращает указатель на голову нового списка
{
int j:
ListNode<Type> *old. *ol;
if (li--head) //удаляемый элемент может быть головой списка.
//в этом случае голова у списка меняется
{
ol=head->Next();
delete 11:
return ol:
}
//Удаляемый элемент не является головой списка. Голова остается прежняя
for (ListNode<Type>* v=head: v!-li: v-v->Next()) //поиск элемента.
//предшествующего
//удаляемому
old=v:
ol-li->Next():
old->PutNext(ol): //предшествующий элемент теперь «видит» элемент
//стоявший в списке вслед за удаляемым
delete 11:
return head;
}
//Печать всех элементов списка с головой head
template <class Туре>
void ListPrint(ListNode<Type> **head)
{
for (LlstNode<Type>* v=head; v!-NULL: v-v->Next())
v->Print():
}
//Подсчет количества элементов в списке с головой head
Выводы 77
template <class Type>
int L1stCount(ListNode<Type> *head)
{
int i: i=0:
for (ListNode<Type>* v=head: v!=NULL: v=v->Next())
i++:
return i;
}
Например, элемент списка объектов знакомого нам класса Field можно создать
с помощью описанного шаблона так:
Field *р - new FieldCa’.1):
L1stNode<Field> *st=new ListNode<Field>(p. NULL):
Широкий набор шаблонов для самых разнообразных структур данных опреде-
лен в библиотеке STL. Примеры использования этой библиотеки приведены в
приложениях 1 и 3. За последние несколько лет в русском переводе вышли до-
статочно подробные руководства и учебники по STL [2], [13], [23], [27], [30],
[41], есть и книга отечественного автора [29]. Отличаясь по стилю изложения,
глубине детализации, широте охвата и форме подачи материала, эти книги могут
удовлетворить практические потребности программистов с разным опытом и
квалификацией.
Выводы
Мы кратко изучили основные возможности языка C++, знание которых необхо-
димо для написания программ, использующих классы. Для понимания текстов
программ, приведенных в последующих главах, этих сведений будет достаточно,
однако богатство языка C++ далеко не исчерпывается тем, что мы успели рас-
смотреть. Желающим глубже познать секреты C++ и те воистину безграничные
возможности, которые открывает перед разработчиком этот язык программиро-
вания, следует обратиться к более полным руководствам недостатка в которых
сейчас нет. Мы же сосредоточим внимание на применении C++ к имитационно-
му моделированию. Но прежде нам нужно рассмотреть очень важную сопутст-
вующую задачу, без решения которой имитационное моделирование реализовать
нельзя. Этой задачей является генерация последовательностей случайных чисел,
распределенных по заданному закону.
Глава 3
Генерация вероятностных
распределений
Основные вопросы, рассматриваемые
в данной главе
□ Теория: метод обратной функции
□ В основе любого генератора —
равномерное распределение
□ Три случая применения метода
обратной функции: особенности
реализации
□ Распределения фазового типа
3.1. Метод обратной функции 79
Вопрос генерации случайной последовательности, распределенной по заданному
закону, хоть и не связан явно с технологией объектного моделирования, имеет
исключительно важное значение. При отсутствии генератора случайных чисел
(ГСЧ) программа просто не сможет имитировать ни одного случайного события
в системе, как-то прибытие новой заявки или завершение ее обслуживания. ГСЧ
играет, если можно так выразиться, роль кочегара, регулярно подбрасывающего
в топку «уголь» — одно случайное число за другим. Без этого даже самый конст-
руктивно совершенный паровоз двигаться не сможет. В этой главе мы кратко
рассмотрим математические основы ГСЧ, примеры их реализации на языке С
для различных законов распределения, а также ряд проблемных ситуаций и «ло-
вушек», возникающих при этом.
Рассматриваемые в этой части книги примеры ГСЧ ни в коем случае не следует
рассматривать как эталон программной реализации, на что претендуют коммер-
ческие библиотеки, хотя результаты работы этих программ и прошли успешно
все необходимые статистические проверки. Автор всего лишь ставил целью от-
разить свое отношение к теме и свой личный опыт, приобретенный при работе
с ГСЧ. У квалифицированного в данном вопросе читателя вполне могут быть
иная точка зрения и иной стиль реализации.
3.1. Метод обратной функции
Предположим, что нам нужно генерировать независимые реализации случайной
величины X с заданной функцией распределения F(x). Такая задача всегда воз-
никает при моделировании систем с очередями, если входные потоки и длины
заявок описываются случайными величинами. Чтобы отследить прибытие новой
заявки или завершение обслуживания имеющейся, такую случайную величину
предварительно нужно разыграть, а для этого необходим определенный матема-
тический метод, реализованный программно. Как мы далее убедимся, достаточно
иметь в своем распоряжении генератор чисел, равномерно распределенных на
интервале [0; 1]. Предположим, что такой генератор у нас уже имеется, и рас-
смотрим метод, известный как метод обратной функции [И], [36]. Прежде всего
заметим, что F(x) по смыслу является неубывающей функцией на конечном или
бесконечном интервале. Для значения 0 < и < 1 определим обратную функцию
F~\u) следующим образом: в точке разрыва F(x) х = х0 зададим некоторое доста-
точно малое е > 0 и, если F(x0 - е) <и< F(x0 + е), определим F~’(u) = х0; иначе,
назначим для Гм(и) такое значение х, для которого F(x) = и. Далее зададим слу-
чайную величину X с помощью преобразования
x = f;'(U), (3.1)
где U — случайная величина, равномерно распределенная на интервале [0; 1].
Идея метода заключается в следующем. Генерируем значение U, затем решаем
уравнение (3.1) относительно X. Тогда справедливо
P{X<x} = F(x), (3.2)
80 Глава 3. Генерация вероятностных распределений
то есть случайная величина X имеет функцию распределения F(x). Доказать (3.2)
несложно.
Заметим, что Р{Х < х} = P{F~\U) < х} = P{U< F(x)}. Теперь — ключевое рассуждение:
если величина Uравномерно распределена на интервале [0; 1], то P{U< и} = F(u) = и.
Значит, P{U < F(x)} = F(x), и (3.2) доказано. Рассмотрим простейший пример.
Предположим, что X имеет экспоненциальное распределение со средним зна-
чением т. Тогда F(x) = 1-е~х/т (0 < х < <ю), и уравнение (3.1) принимает вид
U = 1 -е~х/'. Решив его, получим х = -т ln(l - U). Нетрудно показать, что 1 - U
также равномерно распределена на [0; 1], как и U. Тогда вычисление можно упро-
стить: х = —т In U. Таким образом, для реализации случайной величины X полу-
чаем реализацию случайной величины U и вычисляем X по формуле х = -т In U.
Отдельно рассмотрим случайную величину X, заданную дискретным распреде-
лением Р{Х = х,} -ph 1= 1, 2,..., п. Для получения выборки из такого распределе-
ния вновь сгенерируем значение и и примем X = xf, если
1-1 I
Ер, <“ <Ер> (j =1- 2..... n>>
7=1 7=1
где сумма, не имеющая слагаемых, полагается равной нулю. Предположим, на-
пример, что нужно разыграть случайную величину, принимающую два значения,
которые соответствуют таким событиям: следующее поступление заявки из
входного пуассоновского потока произойдет раньше или позже завершения об-
служивания находящейся в данный момент на сервере заявки. Длительность об-
служивания распределена экспоненциально со средним 1/ц. Вероятность перво-
го из этих событий есть на самом деле вероятность того, что экспоненциальная
случайная величина с параметром X меньше экспоненциальной случайной вели-
чины с параметром р (учли свойство отсутствия последействия). Эта вероят-
мо -мо
ность вычисляется как J к (0(1 (t))dt = J
Тогда можно утверждать, что следующая заявка поступит раньше, чем будет за-
вершено обслуживание предыдущей, если реализация и случайной величины U,
равномерно распределенной на интервале [0; 1], удовлетворяет неравенству
0 < и < Х/(ц + если же выполняется Х/(ц + X) < и < 1, то раньше завершится
обслуживание заявки.
Несмотря на то что описанный метод обратной функции обладает достаточной
общностью, его применение существенно зависит от вида функции распределе-
ния F(x) и зачастую требует определенного опыта в программировании числен-
ных методов. Иначе можно получить совсем не такую случайную последователь-
ность, как хотелось бы, что, разумеется, исказит результаты моделирования.
Проведем классификацию функций F(x) и рассмотрим особенности реализации
метода в каждом из случаев.
3.2. Равномерное распределение 81
3.2. Равномерное распределение
Рассмотрим ГСЧ, лежащий в основе метода обратной функции при любой F(x).
Нашей задачей является получение последовательности заданной длины, со-
стоящей из случайных чисел, равномерно распределенных на отрезке [0; 1]. Сде-
лать это можно так (листинг 3.1).
Листинг 3.1. Реализация равномерного распределения
#include<cstdlib>
#include<ctime>
using namespace std:
const int N=1000; //задаем длину случайной последовательности
srand((unsigned)time(O)):
for (int i=0: i<N: i++)
r=get_uni form():
float get_uniform()
{
int number-randO:
float result=(float)number/(RAND_MAX+l):
return(result);
}
Функция srandO используется для инициализации ГСЧ, для чего нужно задать
в качестве ее аргумента некоторое целое число. Удобно использовать результат
функции timeO, которая возвращает количество секунд, прошедших с начала су-
ток 1 января 1970 г. Этот прием гарантирует, что случайная последовательность
при каждом вызове будет разная. Функция rand() возвращает целое число, рав-
номерно распределенное от 0 до RANDMAX. Константа RANDMAX определена в заго-
ловочном файле cstdlib, ее типичное значение равно 32 767. Приведение к типу
float необходимо для того, чтобы результат деления не оказался равен нулю. Для
получения числа в интервале [0; 1] делить лучше на RANDMAX+1, а не на RAND MAX,
так как, если rand О возвратит максимальное значение, что не исключено,
get_uniform() возвратит единицу, а уравнение F(u) = 1 решения не имеет.
Интересно отметить, что большинство генераторов равномерного распределения
используют алгоритмы, выдающие строку из цифр, которая на каждом такте
цикла подвергается преобразованиям, так что следующая строка кажется слу-
чайной. Это относится, например, к методу срединных квадратов или к методу
остатков [12], который в [8] назван методом конгруэнций. Алгоритм перехода от
одной строки цифр к другой — детерминированный (возведение в квадрат, деле-
ние и др.), поэтому и случайные числа на самом деле являются детерминирован-
ной последовательностью, хотя и прошедшей, разумеется, тесты на случайность
и некоррелированность. Из-за этого часто говорят также о псевдослучайных
числах.
82 Глава 3. Генерация вероятностных распределений
3.3. Уравнение F'1 (и) = х имеет
решение, являющееся
элементарной функцией
Рассмотрим случай, когда и явно выражается через х. Такой ГСЧ реализовать
наиболее просто — достаточно написать одну функцию с прототипом
float де1_<имя распределения>(<параметры распределения»)
Вызов функции — генератора случайных чисел обычно осуществляется в цикле,
потому что одно случайное число само по себе не имеет смысла. Говорить о реали-
зации случайной величины, распределенной по заданному закону, можно только
при наличии последовательности, длина которой достаточна для проведения
проверочных статистических тестов на соответствие этому закону. Количество и
типы параметров распределения, а также ограничения, накладываемые на них,
зависят от вида распределения (см. примеры в табл. 3.1).
Приведем пример ГСЧ для распределения Парето. Параметры распределения вво-
дятся с клавиатуры, результаты записываются в выходной файл (листинг 3.2).
Листинг 3.2. Реализация распределения Парето
#include<stdio.h>
#include<cmath>
#1nclude<cstdl1b>
using namespace std:
const int VOLUME-1000: /‘количество членов случайной
последовательности*/
/*Прототип ГСЧ. А и В - параметры распределения Парето. А>0. В>0*/
float get_pareto(float A. float В):
int main()
{ ' Л
FILE *out: /‘Указатель выходного файла*/
float А. В. result:
out-fopen(”out_pareto". "wt"):
/‘ввод параметров*/
printf(”\nA-'’): scanfC'Xf". &A);
printf("\nB-"): scanfCSf". &B):
srand((unsigned)time(O)): /‘инициализация датчика равномерного
распределения*/
for (int i-0: i<VOLUME: i++)
{
/‘вызов этой функции возвращает случайное число, полученное с помощью распределения
Парето*/
result=get_pareto(A. В):
fprintfCout. "Шп". result):
}
fclose(out):
return 0:
}
float get_oareto(float A. float B)
3.3. Уравнение F’1 (и) =х имеет решение, являющееся элементарной функцией 83
int r_num: float root, right;
r_num=rand(): /Получение случайного целого числа*/
right-(float)r_num/RAND_MAX+l; /Проекция на интервал (0:1)*/
root-A/(pow(l-right. 1.0/В)); /*вычисление значения обратной функции*/
return(root);
}
Заметим, что в методе getparetoO мы не проверяем условие В * 0, так как, со-
гласно табл. 3.1, В > 0. Предполагается, что корректность значений параметров
распределения проверяется до вызова ГСЧ.
В табл. 3.1 перечислены несколько вероятностных распределений, для которых
функция Р*(и) является элементарной.
Таблица 3.1. Распределения вероятностей с элементарной обратной функцией
Название F(x) F~'(u)
Bradford lnfl + fc^l I В-A J A <x< В, C>0 (В-Л)((С+1)“ -1)
ln(C + 1) С
Burr + to 1 a D-i t i ( !_ А с A + B и D+1 -1
у > А, В > 0, C: > 0, D < 100
Cauchy 1 1 1 -,B>0 ( 2 'I
2 л f x - tgl в J “”°^(2и “1)J
Exponential A-x l-e B ,x>A,B >0 Л-В1п(1 -и)
ExtremeLB ( (x-AYc ехрП В J A+ В(-1пи)ё
x > A, В > 0, 0 < 1 :C< 100 л + в[--11c J
Fisk + to 1 ч tb
x > A, В > 0, 0 < C< 100
Gumbel ( (А-л expl- expl—— в > 0 A - B-In(-lnw)
Laplace 1 (x - 2 4 В J . 1 f A-x IA * V tb. to V о (A+ B- ln(2u), и <0,5; ]Л - В-ln(2(l-и)), и >0,5
84 Глава 3. Генерация вероятностных распределений
Название F(x) F'(m)
Logistic . (А-хуВ 0 Л-В-Inf— -11 1+exp J \ D )
Pareto 1 К 1 VЧ В '° < А < х, В > 0 А(1-и)в
Reciprocal 1 ('И 1п — (х J /1п to 1 :ь о Л 1Л Н 1Л Ьо В-А'-“
Weibull 1 - ехр —- ,х>Л,В>0,С>0 в J J Л + В(-1п(1-Ы))с
3.4. Функция F^fu) не является
элементарной
Если функция F~'(u) не является элементарной, уравнение F(x) = и для каждого
и нужно решать численно. Эта процедура существенно облегчается тем фактом,
что F(x) по своему смыслу — монотонно возрастающая, а следовательно, при лю-
бом х уравнение имеет единственное решение на [0; 1]. Можно применить любой
численный метод решения уравнений, например метод половинного деления,
сходящийся, как известно, со скоростью геометрической прогрессии [9]. Для
ГСЧ потребуется написать две дополнительные функции — реализации метода
половинного деления и для вычисления F(x). Здесь от пользователя наряду с за-
данием параметров распределения требуется ввести еще один — точность вычис-
лений. Этот параметр регулирует количество итераций при решении уравнения.
В качестве примера приведем ГСЧ для распределения Эрланга
7 =0 J •
часто используемого при моделировании систем с очередями (листинг 3.3).
Листинг 3.3. Реализация распределения Эрланга
#include<cstdio>
#include<cmath>
#include<cstdlib>
using namespace std:
const int VOLUME-IOOO:
/*Прототип ГСЧ.
k - порядок распределения Эрланга.
mu - параметр.
eps - требуемая точность
*/
float get_erlang(float mu. int k. float eps):
/*Численное решение уравнения методом половинного деления - вычисление обратной функции
3.4. Функция F-'(u) не является элементарной 85
right - правая часть уравнения
*/
float equ(float mu. float right, int k. float eps);
/*Вычисление функции распределения - левой части уравнения - в заданной точке х*/
float functiontfloat mu. int k. float x):
int main!)
{
FILE *out:
float result, mu. eps: int i. k:
out=fopen("out_erlang". "wt"):
printf(”\nMu="): scanfCSf”. &mu):
printf(”\nk="): scanf("$d". &k):
printf("\nPrecision=”): scanf(”fcf". &eps):
srand!(unsigned)time(O)):
for (i=0: i<VOLUME: i++)
{
result=get_erlang(mu. k. eps):
fprintflout. "W. result):
}
fclose(out):
return 0:
}
float get_erlang(float mu. int k. float eps)
{
int r_num: float root, right:
r_num=rand():
right-! fl oat)r_num/RAND_MAX+l;
root=equ(mu. right, k. eps);
return!root):
}
/*вычисление функции FCt) - левой части уравнения - в заданной точке t*/
float function!float mu. int k. float t)
{
float prod, s: int i:
prod-1:
s-1: //сумма инициализируется единицей, так как нулевая степень и 0!
//равны 1
for(i-l;i<k: i++)
{
prod=prod*mu*t/i:
s+-prod:
}
s=s*exp(-mu*t):
return(l-s):
}
float equ(float mu. float right, int k. float eps)
{
float edgel. edgeZ. middle, value:
/♦инициализация интервала, на котором ищется корень.
Правая граница - среднее плюс десятикратное среднеквадратичное отклонение. Будем
считать, что вероятностью попадания случайной величины в область правее этого
значения можно пренебречь*/
edgel-0.0: edge2-(f1 oat)k/mu+10*sqrt((fl oat)k)/mu:
/*если длина начального интервала все-таки мала - удваиваем его. пока корень уравнения
не окажется внутри интервала. В правой границе edgeZ интервала приближения корня
86 Глава 3. Генерация вероятностных распределений
значение левой части уравнения должно быть больше значения правой - это означает, что
корень уравнения лежит внутри отрезка [edgel:edge2]*/
while(function(mu. k. edge2)<right) edge2*-2:
/♦итерируем, пока не достигнем заданной точности*/
wh i1е((edge2-edge1)>eps)
{
mi ddle-(edgel+edge2)/2:
value=function(mu. k. middle): /*вычисляем значение левой части
в середине текущего интервала
локализации корня*/
/♦корень лежит в правой половине текущего интервала*/
if ( (value-right)<0 ) edgel-middle:
/♦корень лежит в левой половине текущего интервала*/
else edge2=middle:
}
returnC(edgel+edge2)/2);
}
Другим примером является гиперэкспоненциальное распределение (листинг 3.4)
f*(x) = Epy(1-exp(-M^)); Ер, =!•
У-1 j=l
Листинг 3.4. Реализация гиперэкспоненциального распределения
#include<stdio.h>
#include<math.h>
#include<cstdlib>
#define VOLUME 32000
/*ГСЧ. mu и p - массивы параметров, k - порядок (размер массивов), eps - заданная
точность вычислений*/
float get_hyper(float *mu. float *p. int k. float eps):
float equ(float *mu. float *p. float right, int k. float eps):
float function(float *mu. float *p. int k. float x):
main()
{
FILE *out:
float s. result. *mu. *p. eps;
int i. k:
out=fopen("out_hyper", "wt”);
printf("\nk-"): scanfCXd". &k):
printf("\nPrecision-"): scanfC'Xf", &eps): s-0:
mu-new floatfk]:
p=new floatCk]:
/*ввод параметров распределения p< и ju. Проверка на равенство суммы вероятностей р<
единице */
for(i-0: i<k: i++)
{
printf("\np[fcd]-". i+1); scanf(”fcf". p+1):
s+-p[ij:
printf(”\nmu[2d]-”. 1+1): scanfCfcf". mu+i);
}
/♦Проверка. Сумма вероятностей P(i) должна быть равна единице*/
if (fabs(s-l)>0.0001) {printf("Сумма P(i) не равна 1!!!\п"): exit(l): }
srand((unsigned)time(O)):
3.5. Функция F(x) не является элементарной 87
for (i-0; KVOLUME; i++)
{
result=get_hyper(mu. р. k. eps):
fprintfCout. "W. result):
}
fclose(out):
delete []mu::
delete []p::
}
float get_hyper(float *mu. float *p. int k. float eps)
{
int r_num:
float root, right:
r_num=rand():
right-(float)r_num/RAND_MAX+l;
root=equ(mu. p. right, k. eps): /*см. листинг 3.3*/
return(root):
}
float functional oat *mu, float *p. int k. float t)
{
float s: int i:
s-0:
for(i=0;i<k: i++)
s+=p[i]*(l-exp(-mu[i]*t)):
return(s):
}
Существуют и многие другие распределения, подпадающие под данную катего-
рию, например косинусное
F(X) = _L| л + ^—^ + sin| а- пВ<х<А+ пВ, В> 0.
2л|. В { В ))
3.5. Функция F(x) не является
элементарной
Если функция F(x) не является элементарной, для реализации ГСЧ необходимо
запрограммировать еще один численный метод — вычисления самой функции
F(x). Это может быть метод численного интегрирования, решения дифференци-
ального уравнения или какой-либо другой, в зависимости от функции F(x). При
этом нужно принять решение о выборе метода и шага сетки.
Классический пример распределения такого типа — нормальное,
Г(х) = -Д-
->/2лВ
f l(t-A
Н- 2
dt.
В
Далее приведен пример ГСЧ для этого распределения (листинг 3.5).
88 Глава 3. Генерация вероятностных распределений
Листинг 3.5. Реализация нормального распределения
#include<stdio.h>
#include<math.h>
#include<cstdlib>
#define VOLUME 32000
/*ГСЧ нормального распределения, mean - математическое ожидание, disp - дисперсия, eps -
заданная точность*/
float get_normal(float mean, float disp. float eps):
/♦вычисление интеграла от А до В методом Симпсона*/
float simpson(float A. float В. float mean, float disp):
/♦вычисление обратной функции.
bottom_bound и top_bound - аппроксимация бесконечных пределов интегрирования;
almost_all - значение интеграла, взятого в этих конечных пределах:
mean и disp - параметры нормального распределения:
eps - заданная точность:
right - правая часть уравнения*/
float equ(float bottom_bound. float top_bound. float mean, float disp. float almost_all.
float eps. float right):
/♦вычисление подынтегральной функции в заданной точке х*/
float function(float mean, float disp. float x):
int mainO
{
FILE *out:
float result, hint. mean. disp. eps; int i:
out-fopen("out_norma1". "wt"):
printf("\nMean-"): scanf("!tf". 8mean):
printf("\nDisp-”): scanf("fcf”. &disp):
/♦По смыслу нормального распределения точность не должна превышать значения hint*/
hint=1.0/(disp*sqrt(2*M_PI)):
printf("\nPrecision(<*f)=". hint): scanfCfcf". &eps):
if (eps>hint) { printfCIllegal presicion\n”): exit(l): }
srand((unsigned)time(O)):
for (i=0; i<VOLUME; i++)
{
result=get_normal(mean. disp. eps):
fprintfCout. "if\n". result):
}
fclose(out):
return 0:
}
float get_normal(float mean, float disp. float eps)
{
int r_num;
float root. bottom_bound. top_bound. almost_all. right:
/♦вычисление конечных аппроксимаций пределов интегрирования в соответствии с заданной
точностью*/
bottom_bound“mean-disp*sqrt(-log(2*M_PI*eps*eps*disp*disp)):
top_bound-mean+di sp*sqrt(-1og(2*M_PI*eps*eps*di sp*di sp)):
/♦вычисление интеграла в этих пределах*/
almost_all=simpson(bottom_bound. top_bound. mean, disp):
r_num-rand():
ri ght-(float)r_num/RAND_MAX+l:
root=equ(bottom_bound. top_bound. mean. disp. almost_all. eps. right):
3.5. Функция F(x) не является элементарной 89
return(root):
}
float simpson(float A. float B. float mean, float disp)
{
float kl. k2. k3. s. x. hl. h;
/*шаг интегрирования принимается равным 0.01. В «товарных» реализациях метода
применяется процедура автоматического выбора шага с помощью апостериорных оценок*/
h-0.01:
s=0: hl=h/1.5:
kl-function(mean. disp. A):
for(x-A: (x<B)&&((x+h-B)<-hl); x-x+h)
{
k2-function(mean. disp. x+h/2):
k3-function(mean. disp. x+h);
s-s+kl+4*k2+k3:
kl-k3:
}
s=s*h/6:
return(s):
}
float functional oat mean, float disp. float x)
{
float result:
result-(1.0/(disp*sqrt(2*M_PI)))*exp(-0.5*((x-mean)/disp)*((x-mean)/disp)):
return(result):
}
float equCfloat bottom_bound. float top_bound, float mean, float disp. float almost_all,
float eps. float right)
{
float edgel. edge2. middle, cover, value:
edgel=bottom_bound: edge2-top_bound:
if (right>almost_all) return(top_bound): else:
if (right<(l-almost_all)) return(bottom_bound): else:
cover-0: /*введена для повышения производительности. В новой точке вычисленйе
интеграла производится не от bottom_bound. а от edgel. в то время как значение
интеграла от bottom_bound до edgel уже накоплено в cover*/
wh i1е((edge2-edge1)>eps)
{
middle=(edgel+edge2)/2: value=simpson(edgel. middle, mean, disp):
if ( (cover+value-right)<0 ) { edgel-middle: cover-cover+value: }
else edge2=middle:
}
}
Интеграл вычисляется методом Симпсона, шаг интегрирования взят 0,01. Пере-
менная ht введена для корректного завершения цикла, так как при увеличении
переменной х с шагом h равенство х= В - h практически никогда не выполнится.
Поэтому необходимо поставить дополнительное условие, чтобы значение x + h
не «заскочило» за верхний предел интегрирования. Коэффициент 1,5 был подоб-
ран экспериментальным путем.
90 Глава 3. Генерация вероятностных распределений
Возникает еще одна вычислительная проблема — какими конечными пределами
аппроксимировать несобственный интеграл. В приведенной программе вопрос ре-
шен следующим образом. Выбирается малый параметр е > 0, задающий точность,
и уравнение G(u) = е, где G(u) — подынтегральная функция, решается относитель-
но и. Конкретно для нормального распределения получим ui2 = А ± Вд/-1п(2ле2В2).
Этими значениями аппроксимируются пределы интегрирования. Далее вычис-
“2
ляем определенный интеграл L = jG(u)du. При решении уравнения F(u) = х,
и1
если х> L, возвращаем н2! если х < 1 - L, возвращаем и(. Иначе численно решаем
уравнение F(u) = х.
Помимо теоретических проблем возникают также и проблемы реализации. На-
пример, на каждом шаге цикла нужно перевычислять х, a ut, иг и L остаются по-
стоянными. Поэтому их вычисление в теле функции getnormal () никак нельзя
считать оправданным. Здесь возможны два решения:
1. Вычислить wi, иг и L вне тела цикла в вызывающей функции и передавать
их в getnormalO в качестве параметров. Однако тогда прототип функции
get normal () потеряет наглядность, и ею сможет пользоваться только ее автор.
Ведь от пользователя мы можем требовать лишь задание точности, среднего и
дисперсии, а все остальное — подробности реализации, отягощать которыми
других программист не имеет права;
2. Сделать переменные ш, ui и L внешними. В принципе, это тоже не решает
проблему, а лишь маскирует ее — значения этих переменных все равно нужно
вычислять вне функции get normalO, кроме того, взятая сама по себе она не
будет компилироваться без подключения специального header-файла.
Приведенный в листинге вариант скрывает все подробности реализации ГСЧ, но
при серийном использовании неэффективен. Решением проблемы является ис-
пользование локальных статических переменных внутри функции get normalO.
По умолчанию эти переменные инициализируются нулями. Инициализация ста-
тической переменной производится только однажды — при загрузке программы,
и ее значение после выхода из функции не «забывается». В зависимости от пара-
метров нормального распределения значения ut и и2 могут оказаться равны нулю
не только после инициализации — это могут быть их реальные значения. Того
же нельзя сказать об L, так как это значение определенного интеграла от неотри-
цательной функции. Поэтому проверять, вычислены или нет значения статиче-
ских переменных, надежнее именно по переменной almostall, которая соответ-
ствует L. Модифицируем фрагмент функции get normal ():
/*ш. иг и L соответствуют статическим переменным*/
static float bottom_bound. top_bound. almost_all:
if (almost_ail==0.0)
{
bottom_bound“mean-di sp*sqrt(-1og(2*M_PI*eps*eps*di sp*di sp)):
top_bound“mean+disp*sqrt(-log(2*M_PI*eps*eps*disp*disp)):
almost_all-simpson(bottom_bound. top_bound. mean, disp):
}
З.б. Распределения фазового типа. Композиция ГСЧ 91
В [36] приведен другой метод ГСЧ для нормального распределения, основанный
на центральной предельной теореме. Обзор методов генерации нормального рас-
пределения можно найти также в [8, гл. 21].
Кроме нормального распределения, существует множество других, имеющих не-
элементарную функцию F(x) — логнормальное, гамма-, бета-распределения и раз-
личные их модификации.
3.6. Распределения фазового типа.
Композиция ГСЧ
Существуют такие распределения, что случайную величину, которую они опи-
сывают, можно разложить на составляющие, каждая из которых задается более
простым распределением. К таким относятся распределения Эрланга и гипер-
экспоненциальное.
Рис. 3.1. Схема распределения Эрланга
Рис. 3.2. Схема гиперэкспоненциального распределения
Случайную величину, распределенную по закону Эрланга k-ro порядка, можно
представить в виде суммы k экспоненциально распределенных случайных вели-
чин. Если этой случайной величиной является длительность обслуживания, то
такое обслуживание эквивалентно k последовательным экспоненциальным эта-
пам (см. рис. 3.1). Для гиперэкспоненциального распределения Hk одна из k экс-
поненциальных случайных величин выбирается в соответствии с некоторым
дискретным распределением р, (параллельные этапы, рис. 3.2). Такие схемы
полностью определяют алгоритм ГСЧ, даже если функция распределения Е(%)
не задана явно. Приведем альтернативный ГСЧ для Hk (листинг 3.6). Он по-
строен на основе двух ГСЧ — для экспоненциального распределения и дискрет-
ного, заданного набором вероятностей. Численное решение уравнения здесь не
требуется.
92 Глава 3. Генерация вероятностных распределений
Листинг 3.6. Реализация гиперэкспоненциального распределения
как распределения фазового типа
/♦функция mainO остается без изменений, см. листинг 3.4*/
float get_hyper(float *mu. float *p. int k)
{
int r_num. j; float root, right:
r_num=rand():
ri ght=(float)r_num/RAND_MAX+l:
/*разыгрываем параметр экспоненциального распределения (номер фазы)*/
j=get_discrete(k. р):
/♦вычисляем обратную функцию для экспоненциального распределения с выбранным
параметром*/
root=-log(1-right)/mu[j]:
return(root):
}
/*ГСЧ для дискретной случайной величины 0.1.....к-1.
заданной распределением P(i)*/
int get_discrete(int к. float *р)
{
int i. test: float a. s:
test=rand();
a=(float)test/RAND_MAX+1:
s=0:
for(i=0: i<k; i++)
{
if ( (a>=s) && (a<s+p[i]) ) return(i):
else s=s+p[i]:
}
}
Существуют и более сложные фазовые распределения (рис. 3.3).
Рис. 3.3. Распределение фазового типа
Pi — это параметры экспоненциального распределения длительности пребыва-
ния в состоянии i, Pi — маршрутные вероятности переходов между состояния-
ми. Схемы такого типа часто используются для аппроксимации распределений
«с тяжелым хвостом» [61]. Глядя на эту схему, не так-то просто записать F(x),
тем не менее, ГСЧ пишется без проблем (листинг 3.7).
Листинг 3.7. Реализация распределения фазового типа
float get_phase(float *mu. float *p. int k)
{
int r_num. j. i:
float right, s:
З.б. Распределения фазового типа. Композиция ГСЧ 93
s=0:
for(i=0: i<k; i++)
{
r_num=rand():
right=((f1oat)r_num)/RAND_MAX+1:
/♦наращивание значения случайной величины в соответствии с очередной пройденной
фазой*/
s=s-log(l-right)/mu[i]:
if (i==k-l) return(s): /*все фазы пройдены*/
/♦разыгрываем, перейти к следующей фазе или завершить процесс*/
j-get_side(p[i]):
if (j-=l) return(s): else;
}
}
/♦розыгрыш двухвариантного (0 или 1) события, заданного вероятностью f */
int get_side(float f)
{
int i. test: float a;
test=rand():
a=(float)test/RAND_MAX+l;
if (a<=f) return(O); else return(l);
}
Заметим, что схему, изображенную на рис. 3.3, можно перерисовать в другом
1-1
виде (рис. 3.4), где вероятности р, и г, связаны соотношением г, =(1-р,)]-[р>.
/=1
Вероятность ri — это вероятность того, что состоятся i - 1 этапов обслуживания,
после чего (с вероятностью 1 - р,) процесс обслуживания завершится. Иными
словами, это распределение можно представить совокупностью распределений
Эрланга, каждое своего порядка, одно из которых выбирается в соответствии
с вероятностью г,. Тогда F(x) удается записать как
с/х V1 1 V1 (нх У
i=i V /=° J!
exp(-px)
. , \ V"1 v1 (h x )
1-ехр(-цх)2^2,—У
i=l >=o ] I
и отнести, таким образом, распределение к группе элементарных F(x). Если же
на каждом из этих последовательно-параллельных этапов параметр ц задать раз-
ным, фазовый метод будет, пожалуй, наилучшим вариантом, ибо функция рас-
пределения станет очень сложной.
Таким образом, генератор равномерного распределения в совокупности с мето-
дом обратной функции позволяет создать ГСЧ для произвольного распределе-
ния вероятностей. Функции-датчики равномерного распределения имеются во
всех языках программирования и специализированных моделирующих средах,
в том числе, разумеется, в С и C++. Более того, в некоторых реализациях C++
(например, Borland) имеются готовые классы для генерации наиболее популяр-
ных распределений. Эти функции и классы написаны квалифицированными
специалистами и всесторонне протестированы.
94 Глава 3. Генерация вероятностных распределений
Рис. 3.4. Альтернативная схема распределения фазового типа
Выводы
1. Метод обратной функции является универсальным методом генерации слу-
чайных величин как для непрерывных, так и для дискретных распределений.
2. В основе метода лежит генератор случайной величины, равномерно распреде-
ленной на отрезке [0;1]. Он реализуется в виде функции, использующей биб-
лиотечный целочисленный генератор случайных чисел rand().
3. Сложность реализации метода обратной функции зависит от вида функции
распределения F(x), для которого можно выделить три возможных случая.
В первом случае ГСЧ реализуется непосредственно по аналитической форму-
ле, во втором — требуется численное решение уравнения, в третьем — числен-
ное решение уравнения и численное интегрирование. В двух последних слу-
чаях возникает ряд вопросов вычислительного характера.
4. Для эффективной реализации метода обратной функции в третьем случае по-
лезно использовать локальные статические переменные.
5. Распределения фазового типа являются альтернативной формой представле-
ния для некоторых видов распределений, которая позволяет значительно уп-
ростить генерацию случайных величин.
Глава 4
Простейший пример:
моделирование
микроволновой печи
Основные вопросы, рассматриваемые
в данной главе:
□ Простейший пример проектирования
класса по текстовому описанию
системы
□ Программная реализация
взаимодействия «объект-человек»
□ Программная реализация
взаимодействия «объект-объект»
96 Глава 4. Простейший пример: моделирование микроволновой печи
В этой главе мы от слов впервые перейдем к делу — рассмотрим в упрощенном
варианте функционирование микроволновой печи и проведем объектное проек-
тирование с последующей реализацией. Объектов в этой системе очень мало —
один или два, очередей нет, процессы и алгоритмы обработки событий крайне
примитивны, розыгрыш случайных событий сведен к минимуму, сбор статистики
не требуется. Тем не менее, данный пример довольно наглядно, без «засорения»
множеством сопутствующих факторов иллюстрирует основные идеи объектного
подхода к имитационному моделированию. Далее эти идеи будут сформулирова-
ны, уточнены и использованы для построения моделей более сложных систем.
Текстовое описание системы взято из монографии [45].
4.1. Описание
Перечислим правила, формализующие реакцию печи на внешние воздействия:
1. Имеется единственная кнопка управления, доступная для пользователя печи.
Если дверца печи закрыта и вы нажмете кнопку, печь будет готовить пищу,
при этом силовой элемент будет находиться под напряжением в течение
одной минуты.
2. Если вы нажмете кнопку во время работы печи, вы получаете дополнитель-
ную минуту.
3. Нажатие кнопки при открытой двери не имеет никакого эффекта.
4. Внутри печи имеется электрическая лампочка. Во время работы печи она
должна быть включена, чтобы можно было посмотреть через стекло дверцы
печи и увидеть, например, что блюдо сгорело. Всякий раз, когда дверца от-
крыта, электрическая лампочка должна гореть для того, чтобы вы могли ви-
деть пищу или вымыть печь. Таким образом, лампочка горит либо при откры-
той двери, либо при закрытой двери и включенном силовом элементе.
5. Вы можете приостанавливать процесс приготовления пищи открыванием
дверцы. В этом случае время сбрасывается.
6. Если вы закрываете дверь, электрическая лампочка гаснет. Это нормальная
конфигурация, если пищу поместили в печь, но еще не нажали на кнопку.
7. Если время истекло, то выключаются как силовой элемент, так и лампочка.
Тогда подается звуковой сигнал, сообщающий, что пища готова.
4.2. Проектирование класса Печь
Выделим из описания печи те элементы, совокупность значений состояний кото-
рых характеризует состояние печи в целом. К таким элементам относятся:
О Электрическая лампочка. Возможные значения: включено или выключено;
О Силовой элемент. Возможные значения: включено или выключено;
О Дверца. Возможные значения: открыта или закрыта.
4.2. Проектирование класса Печь 97
Итак, мы имеем три элемента, состояние каждого из которых может принимать
два возможных значения. Однако это не означает, что общее количество состоя-
ний равно восьми, так как эти три элемента не являются независимыми, а зна-
чит, не всякая комбинация их значений разрешена. Более того, внимательно вчи-
тавшись в описание, можно сделать вывод, что между состояниями отдельных
элементов существует достаточно жесткая функциональная зависимость — пара
значений двух элементов однозначно определяет значение третьего. Сведя дан-
ные в таблицу (табл. 4.1), определим, что возможных состояний у печи всего три.
Таблица 4.1. Возможные состояния печи
Лампа Дверца Питание (силовой элемент)
He горит Открыта Невозможная пара значений
He горит Закрыта Выключено
Горит Открыта Выключено
Горит Закрыта Включено
Однако, зная значения состояний перечисленных элементов, мы еще не можем
сказать, что располагаем полной информацией о печи, достаточной для одно-
значного предсказания ее дальнейшего поведения. Не хватает еще одного пара-
метра — времени, которое осталось до конца приготовления пищи на момент на-
блюдения (будем считать, что единицей его измерения в данном случае является
секунда). При включении печи первоначальное значение этого параметра — 60,
при нажатии кнопки оно увеличивается еще на 60. Поэтому состояние печи бу-
дем описывать четырьмя переменными (не будем обращать внимания на то, что
одна из них вычисляется по значениям других).
Теперь проанализируем, какие события приводят к изменению значения хотя бы
одной из переменных. Таких событий в данной системе три:
О нажатие кнопки;
О изменение положения дверцы (мы объединили два события — открытие и за-
крытие дверцы — в одно);
О время истекло.
Теперь, при условии достаточно уверенного знания синтаксиса языка C++, мы
готовы к тому, чтобы все сказанное формализовать в описании протокола класса
Печь (листинг 4.1). Кроме методов, обрабатывающих указанные события, нам по-
надобится еще один открытый метод — run(), — который будет генерировать эти
события и вызывать методы-обработчики. Подробнее мы обсудим его в 4.3.
Листинг 4.1. Файл Stove.h, содержащий класс Печь
#include<iostream>
#include<conio.h>
enim lamp {on. off}: //возможные состояния лампочки
enum door {open, close}: //возможные состояния дверцы
enum power {on. off}; //возможные состояния силового элемента
class Stove
98 Глава 4. Простейший пример: моделирование микроволновой печи
{
private:
lamp current_lamp: //текущее состояние лампочки
door current_door; //текущее состояние дверцы
power current_power; //текущее состояние силового элемента
int time: //сколько времени осталось готовить пищу: //ЗНАЧЕНИЕ -1 соответствует выключенному питанию
void take_door(void): //обработка события, связанного с изменением //положения дверцы
void take_button(void): //обработка события, связанного с нажатием кнопки
void food_ready(void): public: //обработка события «Пища готова»
Stove О: //метод-конструктор
void runO: //метод-диспетчер
}:
//Конструктор. Создает печь с включенной лампой, открытой дверцей и выключенным питанием
Stove::StoveC)
{
cuгrent_lamp-on;
current_door=open:
current_power-off;
time=-l:
}
//изменение положения двери
void Stove::take_door(void)
{
if (current_door=-open) //если дверца открыта - ее закрыли
{
current_door-closed;
current_lamp-off;
cout « “Дверь закрыли" « endl:
return;
}
else //если дверца закрыта - ее открыли
{
current_door=open;
current-!amp=on:
time=-l:
cout « "Дверь открыли" « endl:
if (current_power—on)
cout « "Приготовление пищи прервано" « endl:
current_power”off;
return:
}
}
void Stove::take_button() //нажатие кнопки
{
if (current_door=open) //дверца была открыта
{
cout « "Нажатие кнопки при открытой дверце не имеет эффекта" « endl;
return;
} else;
if (current_power—on) //дверца была закрыта, питание включено
{
time+=60:
4.3. Печью управляет человек 99
cout « "Добавлена одна минута" « endl;
}
else //дверца была закрыта, питание выключено
{
current_power«on;
current_lamp=on:
time=60:
cout « "Начато приготовление пищи" « endl:
}
return:
}
void Stove::food_ready() //время истекло
{
current_power-off:
current_lamp=on:
time=-l:
cout « "Пища готова" « endl: //сообщение о готовности
return;
}
4.3. Печью управляет человек
Логика закрытых методов класса Печь не вызывает вопросов, так как программи-
ровать их очень легко — что читаем в текстовом описании системы, то практиче-
ски с зеркальной точностью отображаем в программном коде. Но это — методы-
исполнители, и возникает вопрос: а кто и когда будет их вызывать, и кто будет
вести отсчет времени? Ясно, что для этого нужен еще один, если можно так вы-
разиться, метод-вершитель, или диспетчер, который обеспечивал бы связь меж-
ду набором методов-исполнителей и внешним миром. Именно таким диспетче-
ром и является открытый метод гип(). В его обязанности входит:
О отсчет модельного времени;
О постоянное «дежурство» и готовность в любой момент принять событие;
О прием событий из внешнего мира или от других объектов и принятие реше-
ния, какой из закрытых методов вызывать для обработки этого события.
Сам метод run() ничего не изменяет в состоянии своего объекта. Работая диспет-
чером, он лишь принимает вызовы и командует своим «подчиненным» — закры-
тым методам, кому именно из них выполнять фактическую работу, связанную
с этим вызовом.
Следующий вопрос — откуда будут поступать вызовы, кто будет генерировать
события в системе. В данной реализации мы не ввели никаких других объектов,
кроме печи, поэтому управляет ею живой человек, имитирующий воздействия
на печь нажатием определенных клавиш на клавиатуре компьютера. Таких воз-
действий, а значит, и назначенных клавиш всего три: клавиша b — нажатие на
кнопку, клавиша d — изменение положения дверцы, клавиша Escape — заверше-
ние моделирования. Прием событий реализован так.
Функция kbhitO проверяет нажатие клавиши на консоли и возвращает ненуле-
вое значение, если клавиша нажата. В противном случае она возвращает ноль.
100 Глава 4. Простейший пример: моделирование микроволновой печи
Следует сделать оговорку, что эта функция не является стандартной, но реализо-
вана во многих популярных компиляторах, поэтому код, содержащий ее вызовы,
в достаточной мере переносим.
Таким образом, при нажатии человеком клавиши происходит выход из внутрен-
него цикла while и управление передается на вызов функции getchO. С помощью
этой функции мы получаем ASCII-код нажатой клавиши, который далее анали-
зируется в операторе множественного выбора switch-case. Если нажатая клави-
ша относится к «горячим», вызывается соответствующий ей закрытый метод,
в противном случае нажатие клавиши не имеет эффекта. Затем метод run О снова
входит во внутренний цикл while и ожидает следующего нажатия клавиши (лис-
тинг 4.2).
Листинг 4.2. Метод-диспетчер класса Stove
void Stove::run()
{
int i:
enum Keys {b=98.d=100.Escape=27}: //коды клавиш
while(true) //этот цикл длится, пока не нажать клавишу Esc
{ whiledkbhitO) //опрос клавиатуры
{ if (tlme>0) time--; if (time==0) food readyO; } i=getch(); //получение кода нажатой клавиши
switch(i) { case b; take_button(): //нажата клавиша Ь. имитирующая кнопку
case d; take_door(); //нажата клавиша d. имитирующая дверцу
case Escape: return: //нажата клавиша Escape - конец моделирования
} } } //снова ожидаем нажатия клавиши
Для полноты картины осталось написать функцию main(). Она выглядит совсем
просто:
#include "Stove.h"
int main()
Stove myStove;
myStove.runO:
return 0;
4.4. Человека имитирует объект
Приведенный вариант моделирующей программы, в которой события в системе
порождает человек, сидящий за клавиатурой, нельзя считать удовлетворитель-
ным прежде всего с точки зрения его очень ограниченной общности. В самом
4.4. Человека имитирует объект 101
деле, кому нужно программное обеспечение, для функционирования которого
требуется постоянное присутствие, более того, активное вмешательство челове-
ка? Результаты такого моделирования заведомо будут носить субъективный ха-
рактер, зависящий от привычек конкретного человека и его темперамента (он
ведь может и забыть нажать на клавишу), и поэтому не будут иметь никакой
ценности. Участие человека должно сводиться только к одному — запуску про-
цесса моделирования нажатием клавиши Enter или щелчком кнопки мыши, а все
события внутри этого процесса должны генерироваться программно. К тому же
в работе большинства моделируемых систем отдельный человек как объект во-
обще не принимает участия и никаких «рычагов управления» не имеет. Поэтому
при моделировании работы печи действия человека должен имитировать специ-
альный объект, имеющий, как и печь, свой протокол класса и методы доступа.
Итак, необходимо описать еще один класс — Человек, объект которого будет вто-
рым участником имитационной модели и станет посылать сообщения объекту
класса Печь. Какую же информацию применительно к модели должен хранить
объект нового класса? Ясно, что в данной ситуации нас не интересуют фамилия,
имя, отчество и другие паспортные данные этого человека, а также его физиче-
ские данные, привычки и пр. А интересует нас только одно — сколько времени
осталось до его очередного воздействия на печь, или, говоря научным языком, по
какому закону распределен поток событий, порождаемый этим человеком. Если
быть точным, то на самом деле мы имеем два потока — воздействия на кнопку и
воздействия на дверцу, кроме того, строго говоря, распределения этих потоков
зависят от состояния печи (вряд ли кто-то станет открывать дверцу, когда гото-
вится пища!). Однако чтобы сделать первый пример предельно ясным и четким,
мы не будем перегружать логику работы системы излишними подробностями.
Будем считать, что промежутки времени между двумя последовательными воз-
действиями на печь распределены равномерно на некотором временном интер-
вале (предположим, продолжительностью 2 минуты), а тип воздействия (кнопка
или дверца) при наступлении очередного события разыгрывается с вероятно-
стью 0,5 для каждого.
Таким образом, можно сделать вывод, что протокол класса Человек будет выгля-
деть так (листинг 4.3).
Листинг 4.3. Класс Человек class Man
{
private:
int time_to_action: //время, оставшееся до следующего
//воздействия на печь
static const int МАХТ1МЕ=120: //максимальный промежуток времени
//между воздействиями
public:
Man(int а=60): time_to_action(a) {} //конструктор, позволяющий объявлять
//объекты класса Man с инициализацией и без
void run(Stove& stove): //метод-диспетчер для класса Человек
}:
Как же программно реализовать механизм обмена сообщениями между человеком
и печью? Для этого протокол класса Печь нужно дополнить еще одной переменной,
102 Глава 4. Простейший пример: моделирование микроволновой печи
значение которой являлось бы индикатором текущего воздействия на печь. На-
зовем эту переменную current action. Согласно описанию, она может принимать
три значения: «Дверь», «Кнопка», «Ничего». Для этой переменной удобно соз-
дать отдельный тип-перечисление:
enum action {door, button, none}:
Таким образом, метод run() для класса Печь при вызове должен будет всего лишь
опросить переменную current action и, в зависимости от ее значения (а не от
кода нажатой клавиши), вызвать соответствующий метод-обработчик. Это из-
бавляет от необходимости применять системно-зависимые функции ввода-выво-
да, повышает стандартность класса, делает его переносимым и обеспечивает его
использование в любой системе. Коды обработчиков, разумеется, не претерпе-
вают никаких изменений. Важный момент — после возврата управления мето-
дом-обработчиком необходимо вернуть переменной current_action ее нормаль-
ное значение — «Ничего». Метод run() для класса Печь теперь будет выглядеть
так (листинг 4.4).
Листинг 4.4. Метод run() для класса Печь
void Stove::run(void)
{
if (current_action-“button) //воздействие на кнопку
{
take_button():
current_acti on-none:
}
else if (current_action“door) //воздействие на дверь
{
take_door();
current_action-none:
}
else //воздействия нет
if (time>0) time--:
else if (time-“O) food_ready(): //время истекло, пища готова
}
Рассмотрим логику работы метода run() для класса Человек. Этот метод при вы-
зове опрашивает переменную time_to_action. Если ее значение положительно, то
оно просто уменьшается на единицу, и больше ничего не происходит. Если же
значение этой переменной равно нулю, необходимо смоделировать воздействие
на печь и заново разыграть значение time to action. Теперь мы уже можем четко
объяснить, что значит «смоделировать воздействие на печь». С программной
точки зрения это означает — изменить значение переменной current action
объекта Печь. Здесь возникает один нюанс. Исходя из принципов объектного
проектирования переменную current action нужно объявить как закрытую, так
как она отражает внутренние подробности реализации. Но в то же время метод
run() класса Человек должен иметь возможность изменить ее значение. Проблема
решается, если объявить класс Человек дружественным классу Печь. Такое реше-
ние является вполне логичным с точки зрения здравого смысла — в реальности
человек, управляющий печью, действительно знает о ней все и имеет доступ к
полному набору управляющих воздействий. Подытожим наши рассуждения, пе-
kA. Человека имитирует объект 103
реписав протокол класса Печь. Новые строки, отсутствующие в предыдущем ва-
рианте, выделены (листинг 4.5).
Листинг 4.5. Модифицированный класс Печь
#include<stdio.h>
enum lamp {on. off}:
enum door {open, close}:
enum power {on. off}:
enum action {door, button, none}:
class Stove
//возможные состояния лампочки
//возможные состояния дверцы
//возможные состояния силового элемента
//возможные воздействия на печь
private:
lamp current_lamp:
door current_door:
power current_power:
action current action;
int time:
void take_door(void):
void take_button(void);
void food_ready(void):
public:
friend class Man;
Stoved;
void run();
//текущее состояние лампочки
//текущее состояние дверцы
//текущее состояние силового элемента
//текущее воздействие на печь
//сколько времени осталось готовить пищу:
//значение -1 соответствует выключенному питанию
//обработка события, связанного
//с изменением положения дверцы
//обработка события, связанного
//с нажатием кнопки
//обработка события «Пища готова»
//класс Человек объявляется
//дружественным классу Печь
//метод-конструктор
//метод-диспетчер
}:
//метод-конструктор. Создает печь с включенной лампой, открытой дверцей.
//выключенным питанием и без необходимости обрабатывать воздействие со стороны человека
Stove::StoveО
{
current_lamp-on:
current_door=open:
cu rrent_powe r=o ff:
current_acti on=none:
time=-l:
}
//метод run() для класса Человек
void Man::run(Stove& stove)
{
if (time_to_action>0) time--:
else
{
time_to^action-rand()^MAXTIME+l: //розыгрыш времени до следующего воздействия
if (randCWO <5) //розыгрыш вида воздействия
stove.current_action-door: //это и следующее присваивание
//возможны только потому, что класс
//Man объявлен как друг класса Stove
el se stove.current_acti on-button;
}
}
104 Глава 4. Простейший пример: моделирование микроволновой печи
Осталось запустить процесс моделирования, то есть написать функцию mainO
(листинг 4.6). Описания классов Stove и Man поместим в заголовочный файл
Classes.h.
Листинг 4.6. Функция main
#1 nclude<ctime>
include "Classes.h"
#define TOTALTIME 50000
int mainO
{
Stove myStove:
Man Vasiliy(lO):
srand((unsigned)t1me(0)):
for (Int 1-0: i<TOTALTIME: i++)
//здесь содержатся описания классов Stove
//и Man
//общее время моделирования
//создаем объект класса Печь
//создаем объект класса Человек.
//вызывается конструктор с параметрами
//инициализация датчика случайных чисел
//основной моделирующий цикл с потактовым
//опросом всех объектов
{
Vasiliy.run(myStove):
myStove.runO:
}
return 0:
}
Совершенно очевидно, что второй из использованных нами подходов к модели-
рованию обладает большей общностью, чем первый. Насколько он применим к
моделированию других, более сложных систем, когда требуется не только «поиг-
рать» с объектами, но и что-то исследовать? На эту тему мы поговорим в сле-
дующей главе.
Выводы
1. Совокупность значений полей данных должна полностью характеризовать
состояние объекта в текущий момент времени и определять его дальнейшее
поведение.
2. Если поле данных может принимать некоторый ограниченный набор осмыс-
ленных значений, его удобно описывать как перечисление — enum.
3. Каждому событию в системе соответствует закрытый метод-обработчик, вы-
зовом которых управляет открытый метод-диспетчер run().
4. Время наступления события разыгрывается с помощью ГСЧ в соответствии
с описанием системы. Для отсчета времени, оставшегося до наступления со-
бытия, используется отдельное поле данных типа int. Если наступление со-
бытия в данный момент не ожидается, этому полю данных удобно присвоить
значение -1, так как значение 0 является признаком наступления события.
5. Поведение всех участников системы следует имитировать программно, с по-
мощью объектов описанных классов. При этом нужно решить вопрос о зада-
нии отношения дружественности между классами, исходя из логики работы
моделируемой системы.
Глава 5
Общая схема построения
объектных моделирующих
программ
Основные вопросы, рассматриваемые
в данной главе:
□ Как спроектировать объектную
моделирующую программу: советы
и рекомендации
□ Отсчет времени для объекта: один
поток событий — одно поле данных
□ Обобщенная схема основного цикла
моделирования
□ Как собирать статистику
□ Параллельное моделирование: плюсы
и минусы
□ time-driven или event-driven: что
лучше?
106 Глава 5. Общая схема построения объектных моделирующих программ
5.1. Объектный анализ
Успех программного продукта в целом и быстрота его отладки в частности во
многом зависят от того, насколько удачно программа спроектирована, насколько
много мыслительных усилий потрачено при работе «за столом» — без компью-
тера. Особенно значимо это для объектно-ориентированных приложений. Если
объектная модель построена неудачно, программа либо вообще не будет рабо-
тать, либо ее результаты можно будет смело отправлять в мусорную корзину
сразу же по получении. Поэтому при написании программ имитационного моде-
лирования очень важно построить такую объектную модель, которая соответст-
вовала бы основным реалиям моделируемой системы и процессам, в ней проис-
ходящим. Примеры подобного рода анализа в достаточном количестве
представлены во второй части книги. Здесь же сформулируем некоторые реко-
мендации общего характера.
1. Выделить в системе объекты, наличие которых, а также роль, которую они иг-
рают, являются принципиальными для системы и не могут быть проигнори-
рованы.
2. Попытаться сгруппировать объекты по каким-то общим признакам и сделать
вывод — какие совокупности объектов могут быть описаны в рамках одного
класса или классов, связанных отношением наследования.
3. Решить, всякий ли объект системы несет в себе настолько содержательное
информационное наполнение и функциональность, что «достоин» собствен-
ного класса, либо он может быть представлен как поле некоторого более
«важного» класса. Как правило, постоянно присутствующие в системе (еще
говорят — персистентные) объекты (например, серверы) следует описывать
отдельным классом, а временные (например, клиентские заявки) — нет.
4. Иногда в виде класса целесообразно описывать объекты, не имеющие явного
материального эквивалента, например «Движение», «Производство», «Груп-
пировка». Обычно объекты «материальных» классов выступают в них в каче-
стве полей данных.
5. Решить, нужны ли отношения дружественности между какими-то парами
классов.
6. Для объектов каждого класса сформировать набор данных, который полно-
стью идентифицирует состояние объекта в текущий момент и достаточен для
принятия решения о дальнейшем поведении объекта в любой ситуации. Каж-
дый элемент этого набора должен быть описан как поле данных (переменная
состояния) класса с соответствующим типом. В качестве полей данных могут
выступать как скалярные величины, так и массивы.
7. Решить, какие поля данных следует объявить закрытыми (private), какие —
защищенными (protected), а какие — открытыми (public).
8. Если полем данных является объект другого класса — решить, как он соотно-
сится с данным классом. Если имеет место отношение композиции, то есть
один объект вложен в другой, является его составной частью и вне «хозяина»
существовать не может (например, хвост у собаки), то такой объект следует
5.2. Модельное время 107
хранить по значению. Если же включаемый в число полей данных объект не
принадлежит целиком объемлющему объекту, может существовать независи-
мо от него и использоваться другими объектами, его лучше описать как ука-
затель. На языке Cache Object Script [93] в таких случаях можно использо-
вать конструкцию Relationship — задать связь между объектами разных
классов (но нельзя задать связь «многие ко многим»), в C++ — действовать
по обстоятельствам.
9. Оценить, не является ли набор полей данных избыточным, то есть нет ли по-
лей, значения которых можно вычислить по остальным. В реляционной ал-
гебре такая ситуация называется нарушением третьей нормальной формы. Ее
наличие может нарушить целостность данных, так как поля данных объекта
нельзя будет изменять независимо друг от друга. Вычисление избыточных
полей данных следует оформить в виде методов класса. Отметим, что в ред-
ких случаях это правило можно нарушать, но тогда вся ответственность за
контроль над непротиворечивостью данных ложится на программиста. Внут-
ри метода, который меняет какую-то из связанных зависимостью перемен-
ных, значения зависимых переменных нужно перевычислить. Типичный при-
мер — комплексные числа. Чтобы повысить эффективность работы с ними,
в полях данных комплексного числа хранятся четыре значения — действи-
тельная часть, мнимая часть, модуль и аргумент. Одна из этих пар значений
однозначно определяет другую.
10. Решить, с какими значениями полей данных объект начнет участвовать в про-
цессе моделирования. Эти значения использовать при написании конструк-
тора по умолчанию.
11. Рассмотреть каждое поле данных класса и ответить на вопрос, в результате
наступления каких событий в системе значение этого поля может изменить-
ся. Каждому такому событию следует сопоставить некоторый метод класса,
который будет его обрабатывать. Это может быть метод как данного класса,
так и другого класса, если изменение поля данных есть косвенный результат
события, очевидным образом соотносимого с объектом другого класса. Отно-
шение «событие — метод-обработчик» можно считать взаимно-однозначным,
а отношение «изменение переменной — метод-обработчик» довольно слож-
ное и имеет в общем случае вид «многие ко многим»: значение переменной
может измениться в результате наступления нескольких событий, а наступле-
ние события может повлечь за собой изменение значений нескольких полей
данных, и даже у разных объектов. Кроме того, изменение переменной — это
не всегда событие, то есть метода-обработчика может и не быть.
12. При написании кода методов-обработчиков следует тщательно сверяться с описа-
нием моделируемой системы, алгоритмами ее функционирования и принятия
решений, чтобы корректно отразить реакцию объекта (объектов) на событие.
5.2. Модельное время
При проведении имитационного эксперимента программа должна постоянно ге-
нерировать всевозможные события, происходящие в системе. Простейшие из них —
108 Глава 5. Общая схема построения объектных моделирующих программ
это прибытие новой заявки и завершение обслуживания заявки на сервере. Воз-
никает вопрос — в какой момент генерировать то или иное событие? Для этого
каждый поток событий и каждая случайная величина, описывающая работу си-
стемы (например, длина заявки), должны быть заданы своей функцией распре-
деления вероятностей — эта функция задается обычно при описании системы на
этапе постановки задачи. Рассмотрим, что нужно сделать на уровне реализации.
Предположим, что некоторый объект А принимает и обрабатывает случайный
поток событий. В протокол класса для объекта А вводим целочисленную пере-
менную состояния /, имеющую следующий смысл: время, которое осталось до
наступления ближайшего события из потока. Если объект принимает несколько
потоков, то для каждого потока создаем свою переменную. При этом очень важ-
но, чтобы все величины системы, так или иначе связанные со временем, были
приведены к одной единице измерения — той, которая выбрана в качестве одно-
го такта модельного времени (выбор такта модельного времени подробно рас-
смотрен в разделе 7.2 главы 7 и последующих главах). На каждом такте от значе-
ния переменной t отнимается единица. Если t стало равным нулю, вызывается
обработчик события, после чего значение t разыгрывается заново, в соответствии
с законом распределения. Генератор случайных чисел может вызываться как из
самого метода-обработчика, так и из метода A. run О — на усмотрение программи-
ста в зависимости от конкретной ситуации.
В качестве альтернативного проектного решения, чтобы не усложнять описание
реального объекта искусственными переменными, можно создать класс Timer,
экземпляры которого возьмут работу со временем на себя, каким-то образом
взаимодействуя с объектами системы. Очевидно, что эту, как и любую другую
задачу, можно решать по-разному, и осознанный выбор всегда остается за разра-
ботчиком.
5.3. Основной цикл
Как уже отмечалось в предыдущей главе, кроме методов-обработчиков, которые,
как правило, следует делать закрытыми, у класса должен существовать открытый
метод-диспетчер, который мы по аналогии с классом Thread языка Java [71] назва-
ли run(). Этот метод производит декремент переменных времени объекта, опраши-
вает значения других переменных состояния и принимает решение о том, следует
ли в данный момент вызывать какой-то закрытый метод-обработчик или же не
вызывать никакого. Метод run О играет роль связующего звена между классом и
его пользователем, в то время как методы-обработчики относятся к внутренним
особенностям реализации логики (сейчас модно говорить о бизнес-логике) класса.
В зависимости от этой логики run() может иметь параметры, а может и не иметь.
При использовании такого подхода структуру функции main() можно унифициро-
вать для довольно широкой совокупности моделирующих программ (листинг 5.1).
Листинг 5.1. Общая схема функции main() моделирующих программ
//подключение header-файлов, содержащих протоколы классов
#1nclude<ctime>
5.4. Параллельное моделирование и порядок обхода объектов 109
#include<cstdlib>
#include "Classi.h"
#include "Class2.h"
//задание общего времени моделирования
#define TOTAL_TIME 10000
int main()
{
/Объявление объектов. Это может происходить по-разному: вызов
конструктора без параметров, конструктора с параметрами объявление
массива объектов.*/
Classi Objectl:
ClassZ 0bject2[5]:
Class3 ОЬЗес13(параметры конструктора):
//инициализация ГСЧ
srand((unsigned)time(0));
//основной цикл - потактовый опрос всех объектов системы
for(int i=Q:i<TOTAL_TIME;i++)
{
Objectl.run():
for(int j-0: j<5: j++)
0bject2[j].run():
0bject3.run():
} //завершение тела основного цикла
return 0:
} //завершение программы
Например, метод Stove.run() (см. предыдущую главу) не имеет параметров, а ме-
тод Man.run(Stove& stove) имеет параметр — ссылку на печь, которой управляет
объект класса — владельца метода. При желании можно, видимо, добиться того,
чтобы метод run О в любом случае не принимал параметров и для всех классов
имел бы одинаковый прототип. Так, ссылку на объект класса Stove можно было
бы сделать переменной состояния класса Man, но тогда в функции mainO до вызо-
ва конструктора класса Man для него нужно было бы подготовить объект Stove —
печь, которой человек будет управлять. Решение в конкретной ситуации опять-
таки — на усмотрение разработчика.
В теории имитационного моделирования такая схема называется time-driven
(управляемая временем), в отличие от альтернативной схемы event-driven
(управляемая событиями), рассматриваемой в следующем разделе.
5.4. Параллельное моделирование
и порядок обхода объектов
Параллельное дискретно-событийное моделирование относится к тем областям
компьютерных наук, которые в данный момент еще нельзя назвать вполне сфор-
мировавшимися и в которых вопросов больше, чем ответов. Работы в области
110 Глава 5. Общая схема построения объектных моделирующих программ
параллельного моделирования ведутся в основном в стенах академических лабо-
раторий, и ни одна из имеющихся систем пока не получила повсеместного при-
знания. На протяжении 90-х годов прогнозы ведущих ученых и даже названия
их работ были далеки от оптимизма. В обзорной работе [89] сказано: «Для разра-
ботки и анализа эффективных параллельных моделирующих программ требуются
значительные усилия. Не существует “серебряной пули”, позволяющей програм-
мисту гарантированно написать хорошую программу. Вообще говоря, параллель-
ное моделирование — это очень трудная задача». А названия работ «Выживет ли
параллельное дискретно-событийное моделирование?» [65] и «Параллельное
дискретно-событийное моделирование: факт или фикция?» [51] говорят сами за
себя. Чем же вызван такой скептицизм авторитетных ученых? Его причина — те
проблемы, которые в большом количестве возникают перед исследователем, ко-
гда речь заходит о параллельной реализации проверенных и надежных методов.
Хорошо известно, что при некорректном распараллеливании, например методов
вычислительной математики, может оказаться так, что решается совсем другая
задача, отличная от той, которую решала бы последовательная программа. Для
имитационного моделирования подобная опасность имеет еще более ярко выра-
женную форму, что, разумеется, не добавляет надежности результатам работы
параллельных программ. Коротко опишем возникающую проблему, при этом
имея в виду, что речь идет о распараллеливании календарно-событийного
(event-driven) метода моделирования.
В этом методе время изменяется не непрерывно, а скачками. Вместо того чтобы
для каждого объекта вести счетчик времени до наступления ближайшего события,
используется глобальный календарь событий. Программа разбивается на неко-
торое множество параллельных логических процессов. Каждый процесс плани-
рует изменения состояния некоторого объекта системы и заносит в централизо-
ванно ведущийся календарь описание события, а также планируемое время его
наступления. Каждый раз моделируется событие с наименьшим временем насту-
пления, которое затем удаляется из календаря.
Здесь и возникает основная проблема, получившая название causality error (на-
рушение причинно-следственной зависимости) и описанная во всех фундамен-
тальных работах (например, [64], [78], [79]). Если моделировать ближайшие
события параллельно для всех объектов, возникает вопрос — можно ли одновре-
менно моделировать события, происходящие в разные моменты модельного вре-
мени? Пусть для объекта А ближайшее событие произойдет в момент времени
10, а ближайшее для объекта В событие Х2 — в момент времени 20. Если по ре-
зультатам события Xt оказалось необходимым смоделировать событие Х3 для В,
время наступления которого меньше 20 (например, 15), и обработчику события
Х3 нужно знать состояние объекта В именно в момент времени 15 — происходит
causality error. Ведь мы моделируем событие, происходящее в момент времени
20, до того, как смоделировали событие, происходящее в момент времени 151 По-
этому, прежде чем решить, можно ли одновременно моделировать Xt и Х2, нужно
знать — будет ли порождено событие Х3, но чтобы это узнать, нужно сначала вы-
полнить Xt.
В [89] приведен конкретный пример. Друг напротив друга стоят два танка про-
тивоборствующих сторон. Сначала стреляет первый танк, через несколько секунд —
5.4. Параллельное моделирование и порядок обхода объектов 111
второй, для каждого танка известна вероятность поражения цели. В этой ситуа-
ции параллельно моделировать выстрелы нельзя, так как, если первый танк по-
пал, разыгрывать попадание или промах второго танка уже нет смысла.
На преодоление таких ситуаций при параллельном моделировании и затрачива-
ются усилия ученых. К настоящему времени общепринятым стало разделение
подходов к параллельному моделированию на консервативный (conservative)
и оптимистичный (optimistic). При консервативном подходе нарушения причин-
но-следственной связи не допускаются в принципе путем постоянного дина-
мического прогнозирования (lookahead) будущих событий. При оптимистичном
подходе время на то чтобы «заглянуть в будущее» не тратится, поэтому причин-
но-следственная ошибка возможна. При ее обнаружении задействуется механизм
отката (rollback), который отменяет все изменения переменных, произошедшие
после наступления ошибки, и восстанавливает состояние системы. Подробно
протоколы обеих стратегий, а также экспериментальные параллельные архитек-
туры, их реализующие, рассмотрены в уже цитировавшемся обзоре [89]. Там же
приведена сравнительная таблица характеристик обеих стратегий, которую мы
воспроизведем (табл. 5.1).
Таблица 5.1. Сравнительная характеристика стратегий параллельного моделирования
Стратегия Консервативная Оптимистичная
Принцип Недопущение нарушений причинно-следственной зависимости Нарушения допускаются; при их возникновении реализуется механизм отката
Параллелизм Выражен в меньшей степени Выражен в максимальной степени
Синхронизация Блокирование «опасных» процессов; возможны тупики, если не принимать предупредительных мер Механизм отката восстанавливает предыдущее сохраненное состояние
Прогнозирование Является «узким местом», сильно влияет на производительность. Реализуется динамически Не влияет на производительность
Конфигурация Требует статической конфигурации логических процессов Может меняться динамически
Память Требует меньше памяти, чем оптимистичная стратегия Требует больше памяти для сохранения состояний; более сложное управление распределением памяти
Реализация Простота реализации и структур данных Более сложная реализация и операции над данными
Обзор имеющихся языков моделирования и библиотек, реализующих одну или
обе описанные стратегии, содержится в работе [86]. В ней описаны и подвергну-
ты сравнению языки APOSTLE, Parsec, ModSim, YADDES, а также библиотеки
112 Глава 5. Общая схема построения объектных моделирующих программ
CPSim, GTW, ParaSol, PSK, Simkit, SPaDES, SPEEDES, TWOS и WARPED.
По результатам сравнения сделан вывод о большей предпочтительности опти-
мистичной стратегии.
Отметим, что при распараллеливании схемы моделирования, описанной в 5.3,
causality error не возникает, так как параллелизм обработки объектов заключает-
ся в одновременном выполнении на каждом такте основного цикла метода run()
для всех объектов, что соответствует одному и тому же моменту модельного вре-
мени. Но это вовсе не значит, что в данном случае с параллелизмом вообще нет
проблем. Здесь возникают другие проблемы, связанные с выбором порядка обхо-
да объектов.
Как показывает опыт, при обычной последовательной реализации порядок обхо-
да объектов внутри основного моделирующего цикла может быть произвольным
и не оказывает определяющего влияния на результаты моделирования1. Если по
логике работы системы с объектом А происходит нечто, вызывающее изменения
в объекте В, совсем необязательно, чтобы опрос А стоял в теле цикла раньше:
forCint i-0:i<MAX;i++)
{
A.runO:
B.runO:
}
Если раньше опрашивается объект В, искомое событие для него будет сгенери-
ровано на следующем такте цикла. Теоретически, можно предположить следу-
ющую ситуацию: в системе действует некоторая политика, и объект В обязатель-
но должен узнать о произошедшем с объектом А событии первым, раньше
других объектов. Тогда, конечно, опросы АиВ должны следовать жестко друг за
другом. Но такая ситуация свойственна скорее не техническим, а человеческим
системам. А их моделирование — это уже совсем другая наука.
Ситуация резко меняется, если рассмотреть кажущуюся вполне естественной
возможность параллельной реализации вызовов методов run О в такте основного
цикла. Эту возможность легко осуществить, например, средствами Java, исполь-
зуя концепцию параллельных потоков. Если пустить этот параллелизм «на само-
тек», результаты моделирования могут быть серьезно искажены. Представим
себе, что объект А — некая промышленная установка с флюктуирующей темпе-
ратурой, иногда «зашкаливающей» за предельно допустимое максимальное зна-
чение. Когда это происходит, срабатывает система аварийной защиты (объект
В). Программно объект В меняет состояние путем опроса объекта А, который
происходит в теле метода B.runO.
При параллельной реализации потоков порядок их взаимного выполнения друг
относительно друга зависит от порядка выделения потокам программы квантов
времени центрального процессора, который определяется алгоритмами диспет-
1 Автор был бы весьма благодарен за приведение контрпримеров для этого утверждения.
5.4. Параллельное моделирование и порядок обхода объектов 113
черизации операционной системы. Если результат выполнения программы зави-
сит от этого порядка, явление называется гонкой (race condition) и считается не-
допустимым. Предположим, на i-м такте цикла метод B.runO опросил состояние
объекта А до того, как метод A.runO его изменил (повысил температуру так, что
она стала выше максимальной). А на следующем, (i + 1)-м такте случилось об-
ратное — метод A.runO успел понизить температуру объекта А до того, как ее
считал метод B.runO. При фиксированном порядке обхода объектов такого, ко-
нечно, произойти не мож£т, а при параллельном — вполне реально. Тогда имита-
ционный эксперимент не покажет срабатывания аварийной защиты, хотя в ре-
альной системе оно бы произошло, так как в ней процессы в Л и В являются
непрерывными, а не дискретными. Возможна и обратная ситуация. На i-м такте
вызовы пройдут в порядке (А, В), на (i + 1)-м — в порядке (В, А). Тогда в имита-
ционном эксперименте объект В проведет в активизированном состоянии боль-
ше времени, чем провел бы на самом деле. Даже если эти случайности в итоге
статистически компенсируют друг друга и общее время работы системы аварий-
ной защиты окажется достоверным, все равно будут искажены другие важные
характеристики объекта В — например, период занятости или функция распре-
деления для потока включений.
Таким образом, «лобовая» реализация естественного параллелизма программы
может привести к ее некорректному выполнению, так как нечетко определенные
ийи плохо поддерживаемые информационные зависимости между объектами
могут стать причиной непредвиденных осложнений — сами того не подозревая,
мы можем решать совсем другую задачу.
Существует множество способов борьбы с «гонками», позволяющих синхрони-
зировать и согласовывать зависимые потоки. К ним относятся, например, такие
известные средства системного программирования, как сигналы и семафоры.
В языке Java для этой цели предусмотрены мониторы и пара важных методов,
waitO/notifyO, применение которых требует от разработчика определенного ис-
кусства и понимания существа дела, так как некорректное использование мони-
торов может привести к состоянию тупика (deadly embrace). Подробнее об этом
можно прочитать, например, в [71, гл. 7].
Если говорить об объектно-ориентированных возможностях, то здесь на помощь
может прийти один из паттернов проектирования, называемый Observer (Наблю-
датель) [32]. Применительно к рассмотренному примеру объект А выступит в
качестве субъекта, а объект В — в качестве наблюдателя. Субъект, располагая ин-
формацией о своем наблюдателе, немедленно уведомляет его об изменении, ко-
торое может привести к рассогласованности состояний, то есть о превышении
максимально допустимой температуры. Программно это реализуется в виде вы-
зова метода NotlfyO, входящего в стандартный интерфейс класса Subject, внутри
которого вызывается метод Update!) для связанного с субъектом наблюдателя,
входящий в стандартный интерфейс класса Observer. Если в методе Update!) реа-
лизована логика реакции наблюдателя на изменение состояния субъекта, эта ре-
акция гарантированно появится в том же такте моделирования, и произошедшее
с субъектом событие не будет упущено моделирующей программой.
114 Глава 5. Общая схема построения объектных моделирующих программ
5.5. Сбор статистики
Имитационный эксперимент ставится с целью получения результата, в качестве
которого выступают различные характеристики изучаемой системы. Поэтому
изящно запрограммировать с помощью объектов то, как система «живет и ды-
шит», хоть и крайне важно, но недостаточно — само по себе это ничего не дает.
Необходимо еще каким-то образом снимать по ходу моделирования нужные по-
казания и сохранять их для последующей обработки. Трудно дать общие реко-
мендации о методе вычисления, периодичности сохранения и методе статистиче-
ской обработки результатов, поскольку это существенно зависит от того, что
именно мы хотим исследовать. Текущие показания изучаемой величины, напри-
мер длины очереди, можно записывать либо в динамический массив, либо, если
их слишком много, — в файл. После завершения эксперимента этот массив или
файл можно будет подвергнуть статистическому анализу и на основании этого
получить с той или иной достоверностью моменты случайной величины и вы-
двинуть гипотезу о законе ее распределения, а также представить результаты в
графическом виде. Если же нас интересует только среднее значение, то можно
обойтись и без массивов, регулярно пересчитывая среднее на каждом такте ос-
новного цикла. В самом деле, пусть = —---------среднее значение величины а
i
за i тактов. Тогда
дг«+1) = = ср “J
ср i+l i+l
= ^(1. 1 ) + 5tL.
i+l 1+1
Если а является некоторой нетривиальной характеристикой, не входящей в пе-
ременные состояния классов, то для вычисления а, может потребоваться написа-
ние отдельного закрытого метода класса.
Подробнее вопрос сбора статистики при проведении имитационного экспери-
мента рассматривается в [8].
5.6. Пример моделирования на C++
с помощью событийной схемы
Для сравнения удобства и ясности двух подходов приведем пример довольно
простой моделирующей программы на C++, разработанной на основе календар-
но-событийной (event-driven) схемы. Эта программа детально рассмотрена в ра-
боте [63], где описывается свободно распространяемая библиотека SimPack —
набор классов и служебных функций, которые могут быть использованы внутри
С++-программы для реализации алгоритмов моделирования систем.
В крайне упрощенном виде моделируется циклическое выполнение заданий,
попеременно занимающих единственный процессор и один из четырех дисков.
5.6. Пример моделирования на C++ с помощью событийной схемы 115
Работы разделены на два класса — 0 и 1. Работы класса 1 имеют приоритет, вы-
ражающийся в возможности вытеснения с процессора заявок класса 0. Модели-
руется довольно большое количество циклов работ, где под циклом понимается
выполнение работы сначала на процессоре, затем на одном из дисков. Исполь-
зуемые распределения длительностей обслуживания — экспоненциальное и Эр-
ланга.
Листинг 5.2. Пример реализации календарно-событийной схемы моделирования
#include ".../../queueing/queueing.h” //header-файл с описанием
//библиотечных классов и функций
#define nO 6 //число работ класса 0
#define nl 3 //число работ класса 1
#define nt nO+nl //общее число работ
#define nd 4 //число дисков
enum {BEGIN_TOUR-1. REQUEST_CPU. RELEASE_CPU. REQUEST_DISK. RELEASEJHSK};
//названия событий
struct tokenjnfo i //структура для описания работы
1 int cis: //класс (приоритет) работы
int un; //номер диска, к которому произошло //текущее обращение
double ts: //время начала текущего цикла
} task[nt+l]: //объявление массива структур. //содержащего информацию о работах
Token a_token: //переменная для хранения копии //работы, с которой связано //текущее событие. Класс Token - //библиотечный, он моделирует //обслуживаемую заявку
Facility *disk[nd+l]; //массив указателей на диски. Класс //Facility - библиотечный. //моделирует обслуживающее //устройство
int nts-500: //количество моделируемых циклов //работ (продолжительность //моделирования)
double tc[2]-{10.0. 5.0 }. //среднее время обслуживания на ЦПУ //для классов работ
td-30.0. sd-2.5: //среднее время обслуживания работ //на диске и среднее отклонение
int main()
{
int icount. i. j, event. n[2]:
double t. s[2]. rn:
struct token_info *p:
n[0]-n[l]-0:
s[0]-s[l]-0.0: for(i“l;i<-nt:i++) task[i].cls-(i>nO) ? 1:0: //назначение приоритетов работам
init_simpack(LINKED); //инициализация модели
Facility cpu(O.l); //инициализация ЦПУ. Первый аргумент //конструктора - id устройства
116 Глава 5. Общая схема построения объектных моделирующих программ
for(1-1; 1<-nd: i++) //инициализация дисков. //id устройства - порядковый номер //диска
disk[i]-new Facility(i.l);
for(i=l: i<-nt: i++)
{
a_token-1;
event_list.schedule(BEGIN_TOUR, 0.0, a_token); //инициализация списка
//событий. event_list - библиотечный класс, schedule(a.b.c) - метод,
//заносящий в список событий событие а со временем наступления b
//и клиентским объектом с
}
icount-О:
while(nts) //основной моделирующий цикл
icount++:
eventjist.next_event(event. a_token); //выборка из списка ближайшего
//события, event и a_token - //выходные аргументы
i-a_token.id: //определяем, какая именно работа //участвует в событии
p=task+i: //получение информации об этой //работе путем копирования указателя //на ее информационную структуру //в переменную р
switch(event) //обработка события в зависимости
г //от его типа
I case BEGIN_TOUR: //начало цикла работы
a_token.id-i:
p->ts“time(): //сохранение времени начала цикла
event_list.schedule(REQUEST_CPU. 0.0. a_token): //следующее событие
//для a_token - запрос к ЦПУ. Время наступления - немедленно
update_arrivals(): //вызов функции из библиотеки //SimPack
break; case REQUEST_CPU: //запрос к ЦПУ
J—p->cls: //какой приоритет у запрашивающей //работы
a_token.id-i:
if (cpu.preempt(a_token. j)—FREE) //может ли a_token с учетом своего
f //приоритета немедленно занять ЦПУ
1 rn=expntl(tc[j]); //да. может. Разыгрываем время //обслуживания
a_token.id“i:
event_list.schedule(RELEASE_CPU. rn. a_token): //заносим в список
//очередное ближайшее событие - освобождение ЦПУ работой a_token.
//которое произойдет через пп единиц времени
}
break;
case RELEASE_CPU: //завершение обслуживания на ЦПУ
a_token.id“i: cpu.release(a_token): //моделирование освобождения //устройства с помощью
Листинг 5.2. Пример реализации календарно-событийной схемы моделирования 117
//библиотечного метода класса //Facility
p->un-random(1. nd): //равновероятный розыгрыш //запрашиваемого диска
event_list.schedule(REQUEST_DISK. 0.0. a token): //заносим в список
//очередное ближайшее событие - запрос a_token к диску. Наступает
//немедленно
break;
case REQUEST_DISK: //запрос к диску
a_token.id-i:
if (disk[p->un]->request(a_token. 0)——FREE) //свободен ли выбранный
1 //диск
1 rn-erlang(td, sd): //розыгрыш длительности //обслуживания на диске
a_token.id-i: event_li st.schedule(RELEASE_DISK. гп. a token): //заносим в список
//очередное ближайшее событие для a_token - освобождение диска, которое
//произойдет через гп единиц времени
}
break:
case RELEASE_DISK: //освобождение диска
a_token.id-i: disk[p->un]->release(a_token); //работа a_token освобождает диск //p->un
j-p->cls: //в целях сбора статистики //считываем приоритет ушедшей //с диска работы
t-timeO;
s[j]'s[j]+t-p->ts: //наращиваем суммарную длительность //циклов работ класса j
n[j]++: //инкремент количества циклов работ //класса j
update_completions(): //вызов библиотечной функции Simpack
a_token.id=i:
event_list.schedule(BEGIN_TOUR. 0.0. a_token): //a_token начинает
//новый цикл
nts--: //декремент общего счетчика туров
break:
}
} report_stats(): printf("\n\n“): //библиотечная функция SimPack
//вычисление средней длительности цикла для работ каждого из классов
printf("class 0 tour time“^.2f\n”. s[0]/n[0]);
printfCclass 1 tour time-fc.2f\n”. s[l]/n[l]):
return 0: ‘
}
В приложении 4 приведена реализация этой же модели с помощью схемы, рас-
смотренной в 5.3, с сохранением, где это возможно, прежних имен переменных.
Объем исходного кода, включая полный протокол класса, функцию mainO и опи-
сания внешних имен, составляет (без комментариев) около 3,5 Кбайт. Никакого
другого специального кода для компиляции программы не требуется. Объем же
118 Глава 5. Общая схема построения объектных моделирующих программ
текста приведенной календарно-событийной программы (без комментариев) —
около 2 Кбайт, однако для ее компиляции необходимо еще довольно много биб-
лиотечного кода:
О протокол объекта event list, реализующего основную структуру данных —
календарь событий;
О протокол класса Facility;
О тексты функций update arrival О, update completionsO, time().
Объем кода, перечисленного в здесь, конечно же, намного больше 1,5 Кбайт.
Выводы
1. Для любой моделирующей программы функция malnO строится по единой
схеме: инициализация объектов, основной моделирующий цикл и, возможно,
вывод (на экран или в файл) результатов моделирования. Слово «возможно»
употреблено в связи с тем, что вывод результатов может происходить не в
функции mainO, а внутри методов классов. Основной цикл, в свою очередь,
включает последовательный вызов метода-диспетчера для всех объектов си-
стемы. Результаты моделирования не зависят от порядка обхода объектов.
2. Программа, выполняющая имитационное моделирование, может быть распа-
раллелена. При распараллеливании, однако, возникает ряд проблем, сущест-
во которых зависит от выбранной методики моделирования. Основной про-
блемой для календарно-событийной схемы является нарушение причинно-
следственной зависимости между событиями. Для ее решения разработаны
две стратегии — консервативная и оптимистичная. Исследования по этой
теме интенсивно ведутся и в настоящее время.
3. Для моделирования систем с очередями непрерывное изменение времени
является более простым и удобным для реализации, чем календарно-собы-
тийная схема. Последняя, однако, может оказаться более производительной и
требовать меньших затрат времени на моделирование. Ответ на этот вопрос
зависит от особенностей конкретной моделируемой системы.
4. Чтобы научиться писать моделирующие программы, их нужно писать — уме-
ние в данном случае может прийти только через опыт. Внимательное изу-
чение последующих глав и выполнение упражнений поможет приобретению
такого опыта.
Часть II
Моделирование
и анализ экспериментов
на практике
Глава 6
Регулярный входной поток,
отсутствие очередей,
естественный отсчет
времени: моделирование
больничной палаты
Основные вопросы, рассматриваемые
в данной главе:
□ Какие нужны классы и какую
информацию они должны хранить
в полях данных
□ Пациенты в палате: массив
или список?
□ Особенности реализации методов
□ Проверка результатов рассуждениями
□ Синтез системы с заданными
свойствами: подбор параметров
6.2. Классы и объекты 121
6.1. Описание
В среднем за день в палату больницы поступают двое больных. Здоровье челове-
ка оценивается по определенной шкале. Каждый больной проходит тест, резуль-
таты которого равномерно распределены на интервале от 30 до 44 баллов. Когда
в палате нет свободных мест, больные с оценкой выше 41 баллов на лечение не
принимаются. Всего в палате 25 мест. Больной выписывается из палаты, когда
его оценка становится выше 49 баллов. Оценка больного меняется в течение су-
ток на величину, равномерно распределенную на интервале от -0,2 до 1,2 бал-
лов. Когда в палате нет свободных мест, но поступает потенциальный больной,
из нее выписывается больной, оценка которого равна или выше 47 баллов. Тре-
буется оценить следующие величины: среднее время пребывания больного в па-
лате, загрузку палаты, число отказов в лечении, число выписанных досрочно.
6.2. Классы и объекты
Описанная система является довольно простой, тем не менее, объектное проек-
тирование имитационной программы следует провести со всей серьезностью и
тщательностью. Прежде всего, не вызывает сомнений необходимость введения
класса Палата, который будет представлен в программе ровно одним объектом.
Этот объект должен содержать до 25 пациентов, каким-то образом представлен-
ных в классе Палата. Стоит ли пациентов описывать как отдельный класс? Здесь
нужно учесть, какую информацию о каждом пациенте мы должны хранить в
процессе моделирования. Это, во-первых, количество дней, проведенных боль-
ным в палате (их «возраст»), во-вторых, текущая оценка его состояния в баллах.
Такой объем информации представляется вполне достаточным для того, чтобы
организовать для пациента отдельный класс. Альтернативой этому решению
было бы описание в качестве полей данных класса Палата двух массивов или спис-
ков, в которых хранятся, соответственно, «возраст» больных и их оценки. Однако
такой вариант организации программы не в полной мере соответствует идеоло-
гии объектного программирования. В самом деле, количество пунктов информа-
ции о пациенте, подлежащей отслеживанию, может изменяться. В первом случае
эти изменения будут инкапсулироваться в протоколе класса Пациент, а во втором
случае придется модифицировать класс Палата, вводя в него обработку все новых
и новых массивов. Кроме того, будет трудно при необходимости отслеживать из-
менения состояния отдельных пациентов с течением времени.
Количество объектов класса Пациент является переменной величиной, и придется
хранить их в массиве или связном списке. Остановимся на связном списке как
на более универсальном решении, так как для массива нужно задавать размер,
который в большинстве случаев неизвестен. Можно сказать, что в данной задаче
имеется естественное ограничение — 25, но это скорее исключение, чем правило.
Дело в том, что описанная система обладает одной существенной особенно-
стью — в ней отсутствует очередь. А если очередь существует и ее длина не огра-
ничена, а именно такая ситуация наиболее характерна для систем обслуживания,
использование массива заданной длины является не слишком хорошим решением.
122 Глава б. Регулярный входной поток, отсутствие очередей, естественный отсчет времени
С одной стороны, всегда есть опасность того, что количество заявок в системе
превысит заказанную компилятору длину массива, с другой — задание длины
массива «с запасом» приведет к необоснованному расходу памяти, которая поч-
ти всегда является критическим ресурсом.
Есть еще одна причина, по которой в данной задаче следует предпочесть список.
Речь идет о выписке выздоровевших больных. Палата на период лечения являет-
ся контейнером для своих пациентов, а пациенты составляют ее основное содер-
жимое, без них понятие палаты просто теряло бы смысл. Программно удаление
пациента — это удаление элемента (пациента) из содержащей его структуры дан-
ных (палаты). Поскольку этот удаляемый элемент может располагаться в любом
месте структуры, использование списка предпочтительнее, так как операции
вставки и удаления произвольного элемента выполняются для списков быстрее,
чем для массивов.
Далее возникает новый вопрос: как объявить указатель на голову списка объек-
тов класса Пациент — как поле данных класса Палата или как обычную перемен-
ную в функции mainO? Более предпочтительным представляется первый вариант.
В его пользу можно привести следующие доводы. При объявлении указателя на
голову списка пациентов в функции main() логику обхода всех элементов списка
нам придется реализовать тоже внутри основного моделирующего цикла в функ-
ции main(), что сильно нарушит ее общность и простоту. Кроме того, объект Пала-
та не будет иметь непосредственного доступа к своим пациентам, и нужно будет
позаботиться о том, чтобы указатель на голову списка пациентов передавать в
качестве параметра из метода в метод, начиная с метода run(). В принципе, ниче-
го страшного в этом нет, но важно принять такое проектное решение, которое
в наибольшей степени отвечает реальным взаимоотношениям объектов. Доступ
палаты к пациентам должен быть совершенно естественным и не требовать ка-
ких-то дополнительных программных усилий.
Подытожим наши рассуждения. В описание класса Палата войдут следующие пе-
ременные:
О текущее количество пациентов;
О указатель на голову списка объектов класса Пациент.
В описание класса Пациент войдет:
О текущая оценка состояния;
О текущее количество дней, проведенных в палате к настоящему моменту.
Заметим, что мы перечислили только те поля данных класса, которые меняются
в процессе моделирования. Назовем такие поля изменяемыми. В описание класса
войдут также неизменяемые поля, которые, будучи назначенными объекту при
инициализации, не меняются на всем протяжении процесса моделирования.
В данной задаче такими полями являются все перечисленные в условии число-
вые константы. Распределять их по классам следует так, чтобы методы одного
класса по возможности не обращались или обращались как можно меньше к не-
изменяемым полям данных другого класса. Выбранный вариант распределения
приведен далее в листинге программы.
6.3. События и методы 123
б.З. События и методы
Для класса Палата переменная, хранящая текущее количество пациентов, может
измениться в результате наступления одного из двух событий — прибытия и вы-
писки. Указатель на голову списка может измениться только в том случае, если
соответствующий голове списка пациент будет выписан, но такое событие
логично не выделять в отдельный метод, а обрабатывать внутри метода Выписка.
Текущая оценка состояния класса Пациент меняется каждый день и должна ра-
зыгрываться на каждом такте моделирующего цикла специальным методом, ко-
торый можно назвать Изменить оценку. Текущее количество дней просто увеличи-
вается на единицу на каждом такте цикла, что в отдельном методе не нуждается.
Теперь обсудим логику работы методов run(). Для класса Пациент метод run() всегда
и вне зависимости от каких-либо условий должен произвести инкремент количест-
ва дней, проведенных в палате, и вызвать метод Изменить оценку. Поэтому в данном
случае представляется разумным в виде исключения отказаться от метода run() для
класса Пациент, а указанную логику реализовать внутри метода run() для класса Па-
лата. Перечислим, что и в какой последовательности должен делать этот метод:
1. Произвести обход списка пациентов и каждому из них изменить оценку.
2. Произвести выписку тех пациентов, чья оценка свидетельствует о том, что
они выздоровели (набрали 49 и более баллов).
3. Разыграть прибытие двух новых пациентов. Для каждого из них метод Прибы-
тие произведет одно из следующих действий:
1) примет в палату на лечение при наличии свободных мест;
2) откажет в приеме, если свободных мест нет и оценка больного превышает
41 балл;
3) если свободных мест нет, но есть больной с оценкой, достигшей 47 баллов,
выпишет этого больного и примет нового больного на лечение, если его
оценка не превышает 41 балл;
4) откажет в приеме, если свободных мест нет и никто из больных не достиг
оценки 47 баллов.
4. Увеличить всем больным в палате количество проведенных в ней дней на
единицу.
5. Произвести действия по сбору требуемой статистики — коэффициента за-
грузки палаты и среднего числа дней, проведенных больным в палате (после
вызова метода Выписка).
Описание касса содержится в листинге 6.1 (со всеми необходимыми коммента-
риями).
Листинг 6.1. Файл Classes6.h с описанием классов Палата и Пациент
#1nc1ude<cstdio>
#1nclude<cstdlib>
#1nc1ude<ctime>
using namespace std:
124 Глава б. Регулярный входной поток, отсутствие очередей, естественный отсчет времени
#include "List.h" //подключаем файл, в котором описан шаблон //для связных списков, см. главу 2
FILE *sojourn: // в этот файл будут записываться данные //о длительности пребывания в палате выписанных //пациентов
FILE *ro: // в этот файл будут записываться ежедневные данные //о загрузке палаты
float ro_aver=0; // в этой переменной будет ежедневно пересчитываться //средняя загрузка системы, гл.5
float ill_aver=0: // в этой переменной будет пересчитываться среднее //время пребывания пациентов в палате
long int total=0L: // счетчик общего числа больных, претендовавших //на поступление в палату
long int reject1-OL: //счетчик числа больных, не принятых по причине //отсутствия свободных мест и оценки, превышающей //41 балл
long int reject2=0L: //счетчик числа больных, не принятых по причине //отсутствия свободных мест и больных, достигших //оценки 47 баллов
long int earlier-OL: //счетчик числа больных, выписанных досрочно
long int complete=OL; //счетчик числа больных, завершивших лечение // и выписанных здоровыми
long int entered=OL: //счетчик числа больных, принятых на лечение //в палату
class Pacient
{
long int id: //уникальный идентификатор больного, позволяет
//отслеживать «историю болезни»
float current_mark: //текущая оценка состояния больного
int days_in_hosp: //количество дней, проведенных в палате на данный
//момент
const static float bottom=-0.2: //минимальное значение, на которое может
//измениться в течение дня текущая //оценка состояния больного. Нестандартно
const static fl oat top=1.2: //максимальное значение, на которое может //измениться в течение дня текущая оценка //состояния больного. Нестандартно
const static float init_bottom=30.0: //минимальное значение оценки //состояния больного при поступлении //на лечение. Нестандартно
const static float init_top=44.0: //максимальное значение оценки //состояния больного при поступлении //на лечение. Нестандартно
public:
friend class Pal ata: //класс Палата объявлен
//дружественным классу Пациент.
//Альтернатива - написать public
//методы для чтения полей данных
//класса Пациент.
Pacientdnt d): days_in_hosp(d) // метод-конструктор
{
//Розыгрыш первоначальной оценки. Предполагается, что число знаков
//после запятой в значении оценки не более единицы.
current_mark=init_bottom+(float)(rand()fc((int)((init_top-init_bottom)*10)+l))/10:
Листинг 6.1. Файл CLasses6.h с описанием классов Палата и Пациент 125
id-total: //идентификатор-порядковый номер поступившего
//больного
}
void change_mark(); /7 изменение текущей оценки состояния больного
void PrintO; //вывод на печать информации о пациенте
}:
void Pacient::change_mark()
{
currentjnarkT~bottom+(fl oat)(rand(Ш(int)((top-bottom)*10)+1))/10;
}
void Pacient::Print()
{
printfC"Пациент Bld находится на лечении fcd дней, текущая оценка fc.2f\n". id.
days_i n_hosp. currentjnark):
}
class Pal ata
{
int current_nuniber: //текущее число пациентов в палате
ListNode<Pacient> *ill: //указатель на голову списка пациентов
const static int volume=25: //количество мест в палате
const static float borderl=41.0: //начальная оценка, при превышении
//значения которой и отсутствии
//свободных мест больной не принимается
//в палату на лечение. Нестандартно
const static float border2=47.0: //оценка, при достижении которой
//и отсутствии в палате свободных мест
//больной досрочно выписывается, чтобы
//освободить место новому больному.
//Нестандартно
const static float healthy=49.0: //оценка, при превышении которой
//больной считается вылечившимся.
//Нестандартно
public:
PalataO:
void run():
void arrival(-): //метод, обрабатывающий прибытие нового
//больного
void departure(List№de<Pacient> *рас); //метод, моделирующий выписку
//больного из палаты, рас -
//указатель на элемент списка,
//в котором хранятся данные
//об этом больном
}:
Pal ata::Palata() //метод-конструктор
{
current_number=O: //первоначально в палате нет больных
ill-NULL:
}
void Pal ata::run()
{
int i.j:
float ro_val:
L1stNode<Pacient> *ptr. *ptrl:
126 Глава б. Регулярный входной поток, отсутствие очередей, естественный отсчет времени
ptr-i11:
//Обход всех больных и пересчет их текущих оценок
for (i=O;i<current_number;i++)
{
ptr->0ata()->change_mark();
ptr=ptr->Next();
}
ptr-ill:
//Обход всех больных и выписка выздоровевших
while(ptr!-NULL)
{
if (ptr->Data()->current_mark>healthy) //если не объявить класс
//Палата другом класса
//Пациент, компилятор выдаст
//ошибку на этой строке
{
ptrl=ptr->Next();
departure(ptr):
ptr-ptrl;
}
else ptr=ptr->Next();
}
//прибытие двух новых больных
for(i-0: i<2: i++)
{
arrival О:
}
ptr-111;
//Обход всех больных и инкремент числа дней, проведенных в палате
for (1-0:i<current_number:i++)
{
ptr->Data()->days_in_hosp++:
ptr=ptr->Next();
}
//Вычисление текущей загрузки и запись в файл
ro_val=((float)(thi s->current_number))/vol ume:
fprintf(ro,"fcf\n". ro_val):
//пересчет средней загрузки. Число дней вдвое меньше total
ro_aver=ro_aver*(1-2.О/total)+2.0*ro_val/total;
}
void Palata::arrival()
{
int j:
Pacient *p=new Pacient(O): //создание нового объекта класса Пациент
ListNode<Pacient> *ptr:
int i:
total++:
if (current_number<volume) //в палате есть свободные места
Листинг 6.1. Файл Classes6.h с описанием классов Палата и Пациент 127
{
ListNode<Pacient> *lp=new ListNode<Pacient>(p,NULL); //создание нового
//элемента списка
//пациентов
if (current_number—O) ill“lp: //если это первый пациент.
//он становится головой списка
else ListAdd<Pacient>(il1.Ip): //иначе - добавление нового элемента
//в список
current_number++;
entered++:
return:
}
if (p->current_mark>borderl) //свободных мест в палате нет.
//начальная оценка превышает 41 балл
{
delete р: //удаление пациента
rejectl++:
return:
}
//Свободных мест нет. начальная оценка не превышает 41 балл
ptr-i11:
for (1“0:1<current_number:i++) //ищем пациента, которого можно
//досрочно выписать
{
If (ptr->Data()->current_mark >- border2) // пациент найден
{
departure(ptr); //выписка
ListNode<Pacient> *lp=new ListNode<Pacient>(p.NULL): //создание нового
//элемента списка
ListAdd<Pacient>(111,1р): //добавление нового элемента в список
current_number++:
entered++:
return:
}
}
delete p: //принять пациента в палату
//не удалось, удаляем объект
reject2++;
return:
}
void Palata::departure(ListNode<Pacient> *pac)
{
int j. sojourn_val:
//Выписываемый больной выздоровел
if (pac->Data()->current_mark > healthy) complete++:
//Досрочная выписка
else if (pac->Data()->current_mark >- border2) earlier++;
sojourn_val“рас->Data()->days_1n_hosp:
//Записываем в файл число дней, которое выписанный больной провел
//в палате
fprintfCsojourn. "fcd\n". sojourn_val):
//Пересчитываем среднее время пребывания в палате
ill_aver“ill_aver*(l-1.0/(complete+earlier))+1.0*sojourn_val/(complete+earlier);
128 Глава б. Регулярный входной поток, отсутствие очередей, естественный отсчет времени
current_number--:
//Удаляем элемент из списка
111=ListDelete<Pacient>(i11. рас):
return:
}
Поясним логику выражения из метода Paci ent: :change_mark() проведением его
пошаговой трассировки. Напомним, что нам необходимо сгенерировать число с
одним знаком после запятой, находящееся в пределах отрезка [-0,2; 1,2]. Основ-
ная трудность заключается в том, что функция rand() работает только с целыми
числами.
1. Вычисляем значение 10*(top-bottom)=14.0.
2. Приводим результат к целому типу. Приведение не изменит фактического
значения выражения, так как число с k знаками после запятой после умноже-
ния на 10* становится целым.
3. Прибавляем единицу, чтобы получить общее количество всех возможных сме-
щений текущей оценки с учетом минимального и максимального — всего их 15.
4. Генерируем с помощью функции rand() и операции взятия остатка от деления
случайное целое число, равномерно распределенное от 0 до 14.
5. Приводим его к типу fl oat.
6. Результат делим на 10 и прибавлением к нижней границе отрезка получаем
случайное значение смещения оценки, равномерно распределенное в задан-
ном диапазоне.
Отметим также, что инициализация const static-членов класса внутри определе-
ния класса (то есть совмещение инициализации с объявлением члена класса) по
стандарту допускается только для полей данных целочисленного типа, к кото-
рым относятся int, char, enum и др. Инициализация же поля данных const static
fl oat хоть и была «одобрена» используемым компилятором, стандартной не яв-
ляется и должна производиться вне определения класса (листинг 6.2). Напри-
мер:
О const static float bottom; — внутри определения класса Pacient;
О const float Pacient: :bottom=-0.2; — вне определения класса Pacient.
Листинг 6.2. Функция main()
#define N 32014 //количество дней - тактов моделирования
#include "Classes6.h" //включение header-файла с описанием классов
//Palata и Pacient. Внутри этого файла
//подключаются необходимые стандартные
//header-файлы
int mainO
{
Palata palata:
long int i:
srand((unsigned)time(O)): //инициализация генератора случайных чисел
//открытие файлов для сбора статистики
sojourn“fopen("sojourn"."wt"):
6.4. Анализ результатов 129
ro=fopen("ro". "wt"):
//основной моделирующий цикл
for(i=0L:i<N:i++)
palata.runO:
//закрытие файлов для сбора статистики
fclose(sojourn):
fclose(ro);
//печать результатов «прогона» программы
printf("06iuee число претендентов на лечение - Ш\п".total);
printfC"Полностью завершили лечение - fcldXn".complete):
printfCHe приняты не лечение (>41б) - fcld\n”.rejectl);
printfCHe приняты на лечение - fcld\n”.reject2);
printf("Выписано досрочно - fcldXn".earlier);
printfC"Всего принято на лечение - fcldXn".entered);
printfC"Средняя загрузка палаты - fcfXn",ro_aver);
printfC“Среднее время пребывания в палате - fcf\n”.ill_aver);
}
6.4. Анализ результатов
Десятикратный прогон программы с последующим усреднением выходных дан-
ных принес следующие результаты:
О общее число претендентов на лечение — 64 028;
О полностью завершили лечение — 24 561;
О не приняты на лечение (>41 балла) — 8718;
О не приняты на лечение — 23 290;
О выписано досрочно — 7344;
О всего принято на лечение — 32 020;
О средняя загрузка палаты — 0,997;
О среднее время пребывания в палате — 24,92.
Попробуем оценить реалистичность этих результатов. Оценим «грубой прикид-
кой» среднее время пребывания больного в палате. Каждый день оценка состоя-
ния больного в среднем увеличивается на 0,5 балла, так как (-0,2 + 1,2)/2 = 0,5.
Средняя начальная оценка составляет 37 баллов. Если бы больные не покидали
палату досрочно, а только при достижении оценки 50 баллов, среднее время пре-
бывания в палате составило бы (50 - 37)/0,5 = 26 дней. Но так как некоторая
часть больных выписывается досрочно, среднее время пребывания в палате
должно быть несколько меньше этой цифры, что мы и наблюдаем на самом деле.
Далее мы видим, что загрузку палаты можно считать практически равной едини-
це. Из элементарной теории массового обслуживания известно, что это означает
превышение интенсивности входного потока над интенсивностью обслужива-
ния. Проверим, так ли это. Интенсивность входного потока равна двум. Так как
средняя длительность обслуживания заявки равна 26, а в палате 25 мест, то сред-
130 Глава б. Регулярный входной поток, отсутствие очередей, естественный отсчет времени
няя интенсивность обслуживания равна 25/26, что существенно меньше двух.
Следовательно, результаты моделирования вполне реальны.
По результатам моделирования вычислим некоторые представляющие интерес
вероятности:
1. Вероятность отказа в приеме на лечение составляет (23 290 + 8718)/64028 « 0,5.
2. Вероятность отказа при условии, что начальная оценка превышает 41 балл,
составляет 8718/(64 028 (3/15)) « 0,68.
3. Вероятность отказа при условии, что начальная оценка не превышает 41 балл,
составляет 23 290/(64 028 (12/15)) ® 0,455.
4. Вероятность того, что больной, принятый на лечение, будет выписан досроч-
но, составляет 7344/32 020 « 0,23.
5. Вероятность того, что больной, претендующий на место в палате, будет при-
нят и затем выписан досрочно, составляет 7344/64 028 ® 0,115.
6. Вероятность того, что больной, принятый на лечение, завершит его, состав-
ляет 24 561/32 020 « 0,77.
7. Вероятность того, что больной, претендующий на место в палате, будет при-
нят и затем завершит лечение, составляет 24 561/64 028 ® 0,385.
По данным файла sojourn можно построить гистограмму (рис. 6.1) и функцию
распределения (рис. 6.2) для времени пребывания больного на лечении в палате.
Из гистограммы мы видим, что максимальная частота достигается действитель-
но в районе оценки математического ожидания — 26, а само распределение явля-
ется нормальным. Заметим, что задача нахождения распределения этой случайной
величины имеет и теоретическое решение, так как она сводится к классической
задаче случайных блужданий [4, гл. 3], однако это решение получить непросто.
Покажем теперь, как имитационное моделирование может использоваться в ка-
честве инструмента синтеза систем. Предположим, необходимо ответить на во-
прос: каково должно быть число мест в палате, чтобы вероятность отказа в прие-
ме на лечение не превышала заданной величины, к примеру 0,1? Чтобы ответить
на этот вопрос, необходимо построить график зависимости вероятности отказа
от вместимости палаты. Каждая точка этого графика получается многократным
прогоном (скажем, 10 раз) программы при одном и том же значении
Pal ata. vol ume с последующим усреднением по значениям искомой вероятности
для всех прогонов. Подобная процедура проводится для всех значений
Pal ata. vol ume в некотором диапазоне, который мы выбираем в качестве области
определения экспериментально вычисляемой функции.
Конкретно, если речь идет о вероятности отказа в приеме на лечение, получаем
следующий график (рис. 6.3). Из графика видно, что для обеспечения вероятно-
сти отказа не более 0,1 необходимо расширить палату довольно существенно —
до 47 мест.
Такие же зависимости можно получить и для остальных вероятностей
(рис. 6.4—6.9).
6.4. Анализ результатов 131
Рис. 6.1. Гистограмма длительности пребывания пациента в палате
Число дней, проведенных в палате
Рис. 6.2. Функция распределения длительности пребывания пациента в палате
132 Глава 6. Регулярный входной поток, отсутствие очередей, естественный отсчет времени
Вероятность отказа Вероятность отказа
Рис. 6.4. Зависимость вероятности 2 от числа мест в палате
6.4. Анализ результатов
133
Рис. 6.5. Зависимость вероятности 3 от числа мест в палате
Рис. 6.6. Зависимость вероятности 4 от числа мест в палате
134
Глава 6. Регулярный входной поток, отсутствие очередей, естественный отсчет времени
Рис. 6.7. Зависимость вероятности 5 от числа мест в палате
Рис. 6.8. Зависимость вероятности б от числа мест в палате
6.4. Анализ результатов 135
Коэффициент загрузки Вероятность
Рис. 6.9. Зависимость вероятности 7 от числа мест в палате
136 Глава 6. Регулярный входной поток, отсутствие очередей, естественный отсчет времени
Наибольший интерес среди зависимостей представляет изображенная на рис. 6.7,
так как эта функция единственная из всех является немонотонной. Объясним,
почему это так. Проанализируем, какие факторы влияют на вероятность того,
что больной будет принят и затем досрочно выписан. Здесь следует уяснить, что
увеличение числа мест в палате приводит, с одной стороны, к увеличению вероят-
ности того, что больной будет принят в палату, а с другой — к уменьшению веро-
ятности того, что он затем будет выписан досрочно. При малых значениях числа
мест в палате первая вероятность растет быстрее, чем уменьшается вторая, по-
этому функция в целом возрастает. Рост первой вероятности постепенно замед-
ляется, а уменьшение второй, наоборот, увеличивается. Поэтому в некоторой точке
(примерно 25-26 мест) исследуемая вероятность достигает максимума, после чего
монотонно убывает.
Задачу синтеза объекта Палата можно поставить и по-другому: при какой вмести-
мости палаты коэффициент ее загрузки не менее заданного значения (например,
0,95)? Чтобы найти ответ на вопрос с помощью многократных прогонов имита-
ционной программы, строим зависимость коэффициента загрузки от числа мест
(см. рис. 6.10). Интересно, что коэффициент загрузки убывает с ростом числа
мест значительно медленнее, чем рассмотренные ранее вероятности. Объясним и
этот результат. Мы уже выяснили, что средняя длительность лечения больного
26 дней. Учитывая то, что каждый день поступает двое больных, необходимо
иметь палату как минимум на 52 места, чтобы место в палате не было предметом
дефицитного спроса. Действительно, если обратить внимание на график, можно
заметить, что в районе значения 52 имеется точка перегиба, начиная с которой
скорость убывания резко возрастает и значения функции начинают быстрее от-
даляться от единицы. Но так как среднее число больных не может быть сущест-
венно меньше 52, функция в асимптотическом приближении не может убывать
быстрее, чем гипербола у = k/x, где k = 52 (с довольно хорошим приближением).
Из графика видим, что загрузку не менее 95 % обеспечивает палата на 50 мест,
при большем количестве мест палата будет недогружена с точки зрения постав-
ленного требования.
Написанную программу можно модифицировать для проверки того, как влияет
то или иное условие на результаты моделирования. Модификацию можно про-
извести в следующих направлениях:
1. Разыгрывать случайным образом число прибывших больных, сохраняя сред-
нее неизменным, чтобы проверить влияние дисперсии случайного размера
группы новых больных на показатели работы палаты.
2. В методе run О сначала принимать новых больных, затем разыгрывать новые
оценки, после чего выписывать выздоровевших.
3. Использовать при розыгрыше случайных величин не только равномерное, но
и другие распределения.
4. Считать больного здоровым не просто при первом достижении определенной
оценки, а в случае, если в течение некоторого количества дней оценка не
опускается ниже заданного значения.
Выводы 137
5. В методе arrival О досрочно выписывать не первого пациента с оценкой, до-
стигшей 47 баллов, встреченного в порядке обхода списка, а случайным обра-
зом выбирать его из всех пациентов, для которых выполняется это условие.
6. В файле Classes6.h объявить глобальную переменную ignore, означающую чис-
ло начальных тактов, которое следует пропустить, прежде чем начинать сбор
статистики. Оценить ее можно так: сделать несколько пробных запусков про-
граммы с ignore=0 и по последовательности записанных в файл значений ха-
рактеристики, зависящей от установления стационарного режима (например,
коэффициента загрузки), оценить значение ignore. Дальнейшие прогоны про-
граммы при том же наборе входных данных следует проводить уже с ненуле-
вым значением ignore. Например, при входных данных, заданных в условии
рассмотренной задачи, это значение равно 14-15, и при количестве тактов
моделирования 32 000 оно практически не влияет на итоговый результат. По-
этому, чтобы не утомлять себя излишними экспериментами и не зависеть от
времени вхождения в стационарный режим, длительность моделирования
следует выбирать как можно большей.
Разумеется, объектно-событийная модель системы при этом не изменяется. Упо-
мянутые эксперименты с моделью оставляем читателю в качестве упражнения.
Выводы
1. Поля данных класса, проектируемого для имитационной модели, можно раз-
делить на изменяемые и неизменяемые. Неизменяемые поля следует описы-
вать как константы.
2. Структуру, в которой отсутствует естественная упорядоченность элементов,
лучше описывать связным списком, для чего можно использовать имеющий-
ся шаблон.
3. Указатель на голову списка лучше сделать одним из полей данных класса.
4. Счетчики, используемые для сбора статистики, объявляются как глобальные
переменные.
5. Метод-диспетчер run О необходим не для каждого класса.
6. Результаты моделирования хорошо согласуются с правдоподобными рассуж-
дениями и не противоречат здравому смыслу.
7. Построив зависимости характеристик системы от значений входных парамет-
ров модели, можно синтезировать систему, удовлетворяющую заданным тре-
бованиям.
Глава 7
Последовательное
обслуживание
с блокировками
и ограниченным буфером:
производственная поточная
линия
Основные вопросы, рассматриваемые
в данной главе:
□ Масштабирование времени:
как соотнести длительности
реальной единицы времени
и такта моделирования?
□ Очередь ограниченной длины:
массив или список?
□ Взаимодействие объектов: обмен
сообщениями
□ Настройка связей при инициализации
объектов
□ Как повысить производительность?
□ Можно ли доверять имитационному
моделированию: аналитическая
проверка
7.1. Описание системы 139
7.1. Описание системы
На поточной линии предприятия выполняются две операции (соответственно,
имеется два рабочих места) в строгой последовательности, то есть вторая опера-
ция всегда следует после первой. Обрабатываемые изделия громоздки, поэтому
на одной поточной линии могут одновременно находиться только восемь изде-
лий, включая уже обрабатываемые. В соответствии с предлагаемым планом меж-
ду рабочими местами поточной линии выделяется пространство, достаточное
для размещения двух изделий, а перед первым рабочим местом — для четырех
изделий. Стратегия текущего управления производством на линии заключается
в том, что обработка изделия, которое не может разместиться в пределах линии
из-за недостатка свободного пространства, откладывается (рис. 7.1).
Поступление
изделий
Очередь к 1-му
рабочему месту
1-е рабочее место
Очередь ко 2-му
рабочему месту
2-е рабочее место
Удаление
изделий
Отложенные изделия
Рис. 7.1. Схема производственной поточной линии
Обследования поточной линии показали, что интервалы времени между запро-
сами на обработку изделий распределены экспоненциально с математическим
ожиданием, равным 0,4. Времена обработки изделий также распределены экспо-
ненциально, причем на первом рабочем месте обработка занимает 0,25 единиц
времени, а на втором — 0,5 единиц времени. Предполагается, что изделия авто-
матически транспортируются от первого рабочего места ко второму за очень ма-
лый промежуток времени. Если очередь ко второму рабочему места заполнена
до конца, то есть в ней ожидают обработки два изделия, то первое рабочее место
блокируется, так как изделие с него не может быть убрано. На заблокированное
рабочее место не может поступить для обработки другое изделие.
Для оценки предлагаемой схемы необходимо собрать за период, равный 300 еди-
ницам времени, статистику по следующим величинам:
О загрузка рабочих мест;
О время обработки одного изделия на поточной линии;
О число изделий, обработка которых отложена;
О число изделий, находящихся в очереди к каждому рабочему месту;
О доля времени, в течение которого первое рабочее место заблокировано.
140 Глава 7. Последовательное обслуживание с блокировками и ограниченным буфером
7.2. Модельное время
Задача, рассмотренная в предыдущей главе, имела естественную и удобную еди-
ницу измерения времени — день, в течение которого происходили все значи-
мые для системы события. Соответственно, все измеряемые интервалы времени
были целочисленными — их длина выражалась целым количеством дней. Теперь
же ситуация несколько иная — интервалы времени, которые нужно моделиро-
вать, описываются экспоненциальным распределением и уже не являются целы-
ми числами. Кроме того, очевидно, что единица измерения времени, предложен-
ная в описании системы, нас никак не может устроить — она слишком крупна
для того, чтобы с ее помощью можно было подробно проследить за потоками со-
бытий. В самом деле, математические ожидания всех случайных временных ве-
личин меньше единицы, следовательно, с вполне значимой вероятностью за одну
единицу времени в системе может произойти по несколько событий из каждого
потока, и потактовый опрос объектов никак не сможет их отследить. Поэтому за
единицу отсчета модельного времени нужно принять величину, в несколько раз
меньшую той, которая задана в условии задачи.
Здесь важно уяснить следующее. Модельное время — это, в первую очередь, цело-
численная величина, единицей измерения которой является один такт основного
цикла моделирования в функции ma1n() (см. главу 5). В условии задачи время
может быть задано в каких угодно единицах, назовем их условными. Необходимо
ответить на вопросы: можем ли мы считать один такт модельного времени экви-
валентным этой единице? Обеспечивает ли такой выбор необходимую точность
моделирования? Если нет (например, при небольших или дробных значениях
времени), необходимо масштабировать условную единицу, то есть принять один
такт модельного времени равным k условных единиц, где k < 1 — коэффициент
масштабирования.
При этом возникает ряд проблем, наиболее важные из которых таковы:
О Как оценить коэффициент масштабирования?
О Как масштабирование времени реализовать программно?
О Масштабирование не должно оказывать влияние на конечные результаты мо-
делирования.
Ответить на первый вопрос можно двумя способами — экспериментальным
и теоретическим. Экспериментальный способ предполагает задание последова-
тельности коэффициентов масштабирования kt = k, k2 = k2, ..., k, - k'..., где значе-
ние k может быть равно 2, 3 и т. д., но, конечно, не более 10 — вряд ли стоит
уменьшать единицу измерения модельного времени более чем на порядок. При
каждом значении k, проводится некоторое количество прогонов модели (напри-
мер, 10) и результаты усредняются. Если аналогичные результаты эксперимента
при коэффициенте kM можно будет считать статистически теми же, что и при k,,
далее единицу измерения времени можно не уменьшать. Иными словами, коэф-
фициент масштабирования увеличивается до достижения устраивающей иссле-
дователя статистической сходимости результатов.
7.3. Классы и объекты 141
Теоретический способ заключается в следующем. Пусть в постановке задачи фи-
гурируют N случайных величин, заданных функциями распределения Ft(x),
F2(x), .... Fn(x). Далее задаем требуемую точность в виде достаточно малого чис-
ла е. В качестве коэффициента масштабирования принимаем 10“₽, где натураль-
ное число р выбирается из условия р = min((max F(1(T* )) < е). Смысл этого ус-
k I
ловия таков: за единицу модельного времени принимается такое значение, что
вероятность того, что любая из генерируемых случайных величин примет мень-
шее значение, пренебрежимо мала.
Следует также отметить, что коэффициент масштабирования должен быть до-
статочно велик и для того, чтобы погрешность округления, получающаяся из-за
приведения результата работы ГСЧ к целому типу, не оказывала существенного
влияния на результат всего эксперимента.
Реализовать программно масштабирование времени несложно. Здесь все зависит
от вида функции распределения. Так, в случае экспоненциального распределе-
ния F(i) = l-e ^ прототип ГСЧ имеет вид (см. главу 3) float get_exp(float mu).
Если выбран коэффициент масштабирования К (равный 10, 100 или другой ве-
личине), ГСЧ следует вызывать с параметром р/К, что снижает интенсивность
потока событий ровно в К раз.
Масштабирование времени не повлияет на безразмерные статистические показа-
тели, такие как коэффициент загрузки, средняя длина очереди, число заявок в
системе и др. Показатели же, имеющие временной смысл (среднее время пребы-
вания в системе, длительность ожидания в очереди, среднее время блокировки),
следует перед выводом на печать или в файл разделить на К. В этом случае под-
робности масштабирования времени будут полностью скрыты.
7.3. Классы и объекты
Откажемся от заманчивой идеи разработать общую объектную модель для про-
извольной конвейерной системы, что увело бы нас далеко в сторону, и будем ре-
шать конкретную задачу. Итак, имеем два рабочих места. Логика их функциони-
рования различна, поэтому для каждого места создадим отдельный класс —
соответственно, WorkPl acel и WorkPl асе2. А стоит ли создавать класс для описания
обслуживаемых заявок? Нам нужно отслеживать только одну характеристику
каждой заявки, которая проходит через конвейер, — время, проведенное в систе-
ме, чтобы после ее схода с конвейера зафиксировать его и записать в файл сбора
статистики. Представляется, что для хранения одного только времени пребыва-
ния нет смысла оформлять заявку отдельным классом, тем более что логика ее
обработки реализуется в методах классов, описывающих рабочие места. Таким
образом, накопленные времена пребывания находящихся в системе заявок будем
хранить в некоторой структуре, являющейся полем данных рабочего места — как
первого, так и второго. Что же это за структура?
В отличие от предыдущей задачи, где расположение больных в палате и после-
довательность, в которой они покидали ее, не характеризовались каким-либо
порядком, поступление заявок на рабочие места происходит строго в порядке
142 Глава 7. Последовательное обслуживание с блокировками и ограниченным буфером
очереди, а покинуть рабочее место может только та заявка, которая в данный мо-
мент обслуживается. Поэтому текущие времена пребывания заявок в системе це-
лесообразно хранить в целочисленном массиве, размер которого равен размеру
буфера для ожидания плюс один (чтобы учесть обслуживаемую в данный мо-
мент заявку). В процессе моделирования значения массива будут сдвигаться на
единицу вправо (движение очереди) либо переходить из одного массива в дру-
гой, что будет соответствовать переходу заявки в очередь ко второму рабочему
месту. Пустое место в буфере либо на обслуживании можно обозначать значени-
ем-1. Итак, перечислим поля данных первого рабочего места и их характери-
стики.
Неизменяемые поля:
О размер буфера (в данной задаче — 4);
О интенсивность входного потока (в данной задаче — 1/0,4 = 2,5);
О интенсивность обслуживания (в данной задаче — 1 /0,25 = 4).
Изменяемые поля:
О время, оставшееся до поступления новой заявки;
О время, оставшееся до завершения обслуживания заявки (-1, если обслужива-
ние в данный момент не происходит);
О длина очереди (число заявок, находящихся в буфере первого рабочего места);
О заблокировано ли в данный момент рабочее место (1 или true — да, 0 или
false — нет);
О массив для хранения текущих времен пребывания заявок в системе. Его раз-
мер равен размеру буфера плюс один, чтобы учесть также и обслуживаемую в
данный момент заявку (в данной задаче — 5).
Перечислим поля данных второго рабочего места и их характеристики:
Неизменяемые поля:
О размер буфера (в данной задаче — 2);
О интенсивность обслуживания (в данной задаче — 1/0,5 = 2).
Изменяемые поля:
О время, оставшееся до завершения обслуживания заявки (-1, если обслужива-
ние в данный момент не происходит);
О длина очереди (число заявок, находящихся в буфере второго рабочего места);
О массив для хранения текущих времен пребывания заявок в системе. Его раз-
мер равен размеру буфера плюс один (в данной задаче — 3).
Однако перечисленными полями данных вопрос не исчерпывается. Здесь возни-
кает новая задача — как организовать взаимодействие объектов. При заверше-
нии обслуживания заявки на первом рабочем месте для решения ее дальнейшей
«судьбы» нужно проверить, заполнен ли буфер второго рабочего места. Если
да — первое рабочее место блокируется, если нет — нужно «протолкнуть» заявку
дальше, то есть вызвать некоторый метод для второго рабочего места. Иными
словами, первое рабочее место должно для принятия решения каким-то образом
7.3. Классы и объекты 143
опросить второе, то есть иметь с ним связь. Теперь рассмотрим ситуацию, когда
завершается обслуживание заявки на втором рабочем месте. В этом случае нуж-
но проверить, не заблокировано ли первое рабочее место, если да — разблокиро-
вать его, переведя «застрявшую» заявку в очередь ко второму рабочему месту.
Таким образом, и второе рабочее место должно иметь связь с первым.
Правильная организация связи между объектами — очень важная задача. Вот
что было сказано по этому вопросу более 30 лет назад в [8]: «...Не представляет
труда построить модель из отдельных компонентов, каждый из которых будет
соответствовать действительности, однако после “сшивания” отдельных частей
получаемая в результате модель может вести себя не так, как имитируемая ре-
альная ситуация. Поэтому не следует слепо предполагать, что имитационная мо-
дель как единое целое является в достаточной степени точной только потому,
что каждая из составляющих ее частей, рассматриваемая изолированно от дру-
гих, представляется вполне адекватной описываемому процессу. Это предостере-
жение особенно важно по той причине, что цель имитационного моделирования
заключается в воспроизведении поведения всей функциональной системы в це-
лом, а не отдельных ее частей».
К сожалению, в C++ отсутствует возможность явного синтаксического описа-
ния связей между классами, такая, например, как обеспечиваемая конструкцией
Relationship в Cache Object Script [37]. Для решения проблемы можно предло-
жить следующие варианты:
О отказаться от классов WorkPl acel и WorkPl асе2 и «уложить» всю модель в один
класс. Такое решение хотя и возможно в данном случае, в общем случае не-
приемлемо. В дальнейшем мы будем рассматривать более сложные системы,
состоящие из множества совершенно различных по своей природе объектов,
которые должны будут взаимодействовать друг с другом. Объединение их всех
в один класс противоречит всей концепции объектно-ориентированного под-
хода к моделированию систем;
О воспользоваться механизмом шаблонов и сделать класс объекта, на который
производится ссылка, параметром шаблона;
О воспользоваться механизмом наследования. Описать абстрактный класс WorkPl асе,
из которого вывести классы WorkPl acel и WorkPl асе2. Тогда взаимные ссылки
в производных классах можно будет описать как ссылки на объект базового
класса, а именно WorkPl асе *w;
О описать взаимные ссылки как void* (бестиповый указатель). И каждый раз,
когда мы захотим обратиться по этой ссылке к объекту, потребуется выпол-
нять явное приведение типа указателя.
Естественно, описанная проблема является только проблемой реализации, но ни
в коей мере не исходной модели. Мы воспользуемся последним из предложен-
ных вариантов, но следует отметить, что стандарт разрешает предварительное
объявление класса. При использовании этой синтаксической конструкции вза-
имные ссылки можно представлять указателями на объекты конкретных клас-
сов, а не бестиповыми, например:
class WorkPlace2: //предварительное объявление класса Workplace?
class WorkPlacel
144 Глава 7. Последовательное обслуживание с блокировками и ограниченным буфером
{
Workplace? *w2:
public:
h
class Workplace?
{
WorkPlacel *wl:
public:
}:
Такой исходный код будет компилироваться и работать. Но давайте все же по-
пробуем применить бестиповые указатели и, столкнувшись с необходимостью
постоянного приведения типов, увидим сами, почему такое решение не очень по-
пулярно у профессионалов. В нашем случае приведение типов указателей потре-
буется всего в нескольких местах внутри методов классов и, казалось бы, боль-
шой проблемы не составит. Но если мы используем бестиповый указатель в
некотором библиотечном классе и задача приведения типа будет перекладывать-
ся на пользователя этого класса, такого допущения (авось, пользователь не за-
будет) разработчик-профессионал делать не вправе. Как справедливо заметил
автор работы [47], «если вы свято в это верите, советуем вам вкладывать свои
сбережения только в государственные ценные бумаги — вам вряд ли повезет в
этой жизни». Подчеркнем также, что очень большие проблемы могут возникнуть
и при удалении объекта операцией delete по void-указателю — операция delete
корректно работает только с типизированными указателями. Наименьшим злом
будет немедленный крах программы.
И заключительный штрих — для облегчения взаимодействия объектов классы
WorkPl acel и WorkPl асе2 описываем как дружественные друг другу, ничем при этом
не погрешив против их реальных взаимоотношений.
7.4. События и методы
Как для первого, так и для второго рабочего места существуют два типа собы-
тий — поступление в очередь новой заявки и завершение обслуживания заявки
непосредственно на рабочем месте. Строго говоря, для первого рабочего места
можно придумать третий тип события — разблокировка. Однако логика обработ-
ки такого события ничем не отличается от логики обработки события Завершение
обслуживания для первого рабочего места, с тем лишь дополнением, что перемен-
ная, указывающая на состояние блокировки, переключается в ноль. Мы не будем
здесь подробно рассматривать логику обработки каждого события, так как она
подробно отражена далее в комментариях к программному коду (листинг 7.1).
Поясним лишь различия в прототипах соответствующих методов для разных
классов.
Прототипы методов Прибытие и Завершение обслуживания для второго рабочего места
имеют вид void Arrival (int t): и int Completed;, в отличие от аналогичных мето-
7.4. События и методы 145
дов для первого рабочего места, которые не имеют ни параметров, ни возвраща-
емых значений.
Почему так происходит? Параметр метода Прибытие — это то время, которое по-
ступившая заявка уже провела в очереди и на обслуживании на первом рабочем
месте. Именно это значение и будет занесено в нужный элемент массива времен
пребывания заявок, находящихся на втором рабочем месте. Метод же Завершение
обслуживания возвращает в вызывающий метод run() окончательное время пребы-
вания заявки в системе, которое заносится методом run() в файл сбора статисти-
ки. Для методов первого рабочего места ничего этого не требуется.
И еще один штрих. Поскольку объекты WorkPlacel и WorkPlace2 должны в числе
своих полей данных содержать указатели друг на друга, инициализация этих
указателей может быть выполнена только после того, как созданы оба объекта.
Чтобы не делать их открытыми членами класса, в протоколы классов добавлен
метод PutNeighbourO, который вызывается для каждого из объектов в функции
mainO, получая в качестве аргумента указатель на другой объект (листинг 7.2).
Таким образом, в данной ситуации нам приходится идти на нарушение извест-
ного принципа объектно-ориентированного программирования, в оригинале зву-
чащего так: «Resource Acquisition is Initialization» — «Захват ресурсов есть ини-
циализация». Согласно этому принципу, все поля данных объекта должны
инициализироваться только в конструкторе, чтобы сразу же после создания объ-
екта пользователь имел возможность работать с ним без необходимости допол-
нительно конфигурировать поля данных. Однако если полями являются указа-
тели на другие объекты моделируемой системы, причем объектные ссылки —
перекрестные, этот принцип приходится нарушать. Либо требуется перестра-
ивать всю объектную модель. С подобной ситуацией мы неоднократно встретим-
ся в последующих главах и рассмотрим другие методы ее решения.
Листинг 7.1. Реализация классов
//Файл Classes?.h с описанием классов.
#include<cstdio>
#include<cmath>
#include<cstdlib>
#include<ctime>
using namespace std: FILE *qul: //указатель на файл co статистикой по очереди //к первому рабочему месту
FILE *qu2: //указатель на файл со статистикой по очереди //ко второму рабочему месту
FILE *soj: //указатель на файл со статистикой по времени //общему времени пребывания заявок в системе
Int K-100: //коэффициент масштабирования времени
long Int entered-=OL: //счетчик поступлений в систему (не отложенных //заявок)
long int completed-=OL: //счетчик для заявок, завершивших обслуживание
long int rol-OL: //переменная для подсчета загрузки первого // рабочего места - в ней накапливается время, //в течение которого происходило обслуживание //заявок
146 Глава 7. Последовательное обслуживание с блокировками и ограниченным буфером
long Int ro2-0L: //аналогично для второго рабочего места
long int postponed-OL; //счетчик числа отложенных заявок
long int blocked-OL; //переменная для подсчета суммарного времени
//блокировки первого рабочего места
long int int_blocked-OL: //переменная для подсчета числа вхождений
//первого рабочего места в состояние блокировки
float get_exp(float mu) //генератор случайных чисел, распределенных
//экспоненциально (см. главу 3)
{
int r_num: float root, right:
r_num-rand(): /Получение случайного целого
/числа*/
right-((float)r_num)/(RAND_MAX+l): /*проекция на интервал (0:1)*/
root--log(l-right)/mu: /*вычисление значения обратной
/функции*/
return(root):
}
//Протокол класса Первое рабочее место
class WorkPlacel
{
int to_complete:
int q_length:
int *sojourn;
void *wp:
int to_arrival:
bool locked:
const static int buffer-4:
const static float stream_rate-2.5:
const static float serv_rate-4.0:
public:
friend class WorkPlace2:
void ArrivalO;
void Complete!):
WorkPlacel0
{
int i;
//сколько времени осталось
//до завершения обслуживания
//сколько заявок находится в буфере
//массив времен пребывания заявок
//в системе
// ссылка на второе рабочее место
//сколько времени осталось
//до прибытия следующей заявки
//заблокировано ли первое рабочее
//место
//размер буфера
//интенсивность входного потока
//Нестандартно (см. главу 6)
//интенсивность обслуживания
//Нестандартно
//обработка прибытия новой заявки
//обработка завершения обслуживания
//конструктор
to_complete--l: //первоначально обслуживания нет
q_length-0; //буфер пуст
locked-0: //блокировки нет
to_arrival-(int)get_exp(stream_rate/K): //разыгрывается время
//до прибытия первой заявки
if (to_arrival--0) to_arrival-l: //если, несмотря на масштабирование
//оно все же оказалось меньше 0.5
sojourn-new int [buffer+1]: //выделение памяти под массив времен
//пребывания
for(i-0:i<=buffer:i++) sojourn[i]--l: //заполнение массива -
//первоначально заявок нет
}
void run(); //метод-диспетчер
Листинг 7.1. Реализация классов 147
void PrintO; //вывод на печать текущего состояния
//первого рабочего места
void PutNeighbour(WorkPlace2 *w) { wp=w: } //установление связи
//со вторым рабочим местом
-WorkPlacel!) { deleted sojourn: } //деструктор
}:
//Протокол класса «Второе рабочее место»
class WorkPlасе2
{ int to_complete: //сколько времени осталось до завершения //обслуживания
int qjength: //сколько заявок в буфере
int *sojourn: //массив для хранения времен пребывания //заявок в системе
void *wp: //ссылка на первое рабочее место
const static int buffer-=2: //размер буфера
const static float serv_rate-=2.0: //интенсивность обслуживания
//Нестандартно
int Complete!): //обработка завершения обслуживания
void Arrival(int t): public: friend class WorkPlacel: //обработка поступления новой заявки
WorkPlace2() //метод-конструктор
{
int i: *
to_complete--l: //обслуживания нет
q_length-=0: //в буфере нет заявок
sojourn-new int [buffer+1]: //выделение памяти под массив времен пребывания
for(i-=0:1<-!buffer:i++) sojourn[i]-=-l: //инициализация массива
}
void PutNeighbour(WorkPlacel *w) { wp-w: } //установление связи с первым
//рабочим местом
void run(): //метод-диспетчер
void PrintO: //печать текущего состояния объекта
-WorkPlace2() { deleted sojourn; } //деструктор
}:
void WorkPlacel::Print()
{
int 1:
printfCflo прибытия новой заявки осталось Bd\n", to_arrival):
printfCflo завершения обслуживания осталось Bd\n". to_complete):
if (locked) printft"Рабочее место заблокировано^");
else printfCРабочее место не заблокировано^");
printfCB буфере И заявок\п”. q_length);
printf(“Отложено Bld заявок\п", postponed):
for (1-0:i<-buffer:i++)
printf("Bd-e место: время пребывания в системе Bd\n". i+1. sojourn[i]);
}
void WorkPlacel::Arrival()
{
to_arrival-4int)get_exp(streani_rate/K): //разыгрываем время до прибытия
//следующей заявки
if (to_arrival—0) to_arrival-4: //если, несмотря на масштабирование.
//оно округлилось до нуля
148 Глава 7. Последовательное обслуживание с блокировками и ограниченным буфером
If (q_length“buffer) { postponed++: return:} //если нет мест
//для ожидания, заявка
//откладывается
else entered++:
if (sojourn[buffer]“-l) //если рабочее место не обслуживает
if (locked) //...по причине блокировки
{
sojourn[buffer-q_length-l]-0: //заносим вновь прибывшую заявку в конец
//очереди
q_length++: //увеличиваем на единицу текущую длину очереди
return;
}
else //...по причине отсутствия заявок
{
sojourn[buffer]-=0: //ставим заявку на обслуживание
to_complete*(int)get_exp(serv_rate/K): //. .и разыгрываем для нее
//время обслуживания
if (to_complete“0) to_complete-l:
return:
}
else //рабочее место занято обслуживанием.
//но в буфере есть свободные места
{
sojourn[buffer-q_length-l>0: //ставим заявку в буфер
q_length++; //инкремент длины очереди
}
return:
}
void WorkPlacel::Complete()
{
int 1. trap:
//Проверяем, может ли второе рабочее место принять обслуженную заявку.
//Для обмена информацией между объектами выполняем приведение типов
if (((WorkPlace2*)wp)->q_length==((WorkPlace2*)wp)->buffer) //не может
{
locked-=true: //первое рабочее место
//блокируется
int_blocked++: //новое вхождение в состояние
//блокировки
to_complete=-l: //обслуживания нет
return:
}
else if (sojourn[buffer-l]!-=-l) //буфер не пуст
{
tmp=sojourn[buffer]:
for(i-buffer: i>0: i--) //продвижение очереди
sojourn[i]-sojourn[i-1]:
sojourn[0>-l; //хвост очереди становится пуст
q_length--: //декремент длины очереди
to_complete-=(int)get_exp(serv_rate/K): //первая заявка из очереди
//поступает на обслуживание
if (to_complete—0) to_complete=l;
((WorkPlace2*)wp)->Arrival(tmp): //обслуженная заявка переходит
//на второе рабочее место
}
Листинг 7.1. Реализация классов 149
else
{
tmp-=sojourn[buffer]:
((WorkPlace2*)wp)->Arrival(trap):
sojourn[buffer]--1:
to_complete--l:
}
}
void WorkPlacel::run()
{
int i:
if (to_complete>0) to_complete--:
if (to_complete“0) Completed:
if (to_arrival>0) to_arrival--;
if (to_arrival—0) ArrivalO:
if (to_complete>0) rol++:
//буфер первого рабочего места
//пуст
//обслуженная заявка переходит
//на второе рабочее место
//первое рабочее место
//становится пустым
//обслуживания нет
if (locked) blocked++;
i
fprintf(qul.”Bd\n". q_length):
f or (i -=0: i <=buffer: i ++)
if (sojourn[i]>“0) sojourn[1]++:
}
void WorkPlace2::PrintO
//обслуживание продолжается
//обслуживание завершено
//новая заявка еще не прибыла
//прибыла новая заявка
//инкремент времени, в течение
//которого первое рабочее место
//работает
//инкремент времени, в течение
//которого первое рабочее место
//простаивает из-за блокировки
//запись в файл текущей длины
//очереди
//для всех заявок, находящихся
//на первом рабочем месте. -
//инкремент времени пребывания
{
int i:
printfC'flo завершение обслуживания осталось 1И\п". to_complete):
printfC'B буфере- fcd заявок\п". q_length);
for (i-0:i«-buffer;i++)
printfC'Bd-e место: время пребывания в системе Bd\n". i + 1. sojourn[i]);
}
void Workplace?::Arrival(int c)
{
if (sojourn[buffer]—-l) //обслуживания нет
{
sojourn[buffer]-c: //заявка сразу поступает на обслуживание
to_complete=(i nt)get_exp(serv_rate/K):
if (to_complete--0) to_complete-l:
return:
}
else
{
sojourn[buffer-q_length-l]-c: //заявка становится в очередь
q_length++;
}
return:
150 Глава 7. Последовательное обслуживание с блокировками и ограниченным буфером
int Workplace?::Complete()
{
int i. tmp:
tmp-sojourn[buffer]:
completed++:
if (sojourn[buffer-l]!--l) //очередь не пуста
{
ford-buffer: i>0: i--) //продвижение очереди
sojourn[i]=sojourn[i-1]:
sojourn[0]--l:
q_length--; //декремент длины очереди
to_complete-(int)get_exp(serv_rate/K): //розыгрыш времени обслуживания
if (to_complete—0) to_complete-l:
}
else //очередь пуста
{
sojourn[buffer]--l:
to_complete--l:
}
if (((WorkPlacel*)wp)->locked“true) //первое рабочее место заблокировано
{
((WorkPlacel*)wp)->locked-0: //разблокируем его
((WorkPlacel*)wp)->Complete(); //заблокированная заявка переходит
//на второе рабочее место
}
return(tmp); //возвращаем окончательное время
//пребывания обслуженной заявки
}
void Workplace?::run()
{
int i. p:
if (to_complete>0) to_complete--: //обслуживание еще не завершено
if (to_complete--0) //обслуживание завершено
{
p-Complete():
fprintf(soj.”!td\n". (float)p/K): //запись времени пребывания заявки
}
if (to_complete>0) го?++: //инкремент времени, в течение которого
//второе рабочее место занято обслуживанием
fprintf(qu?.”fcd\n". q_length): //запись в файл длины очереди
for(i-0:1<-buffer:i++) //инкремент времени пребывания
//для всех заявок, находящихся
//на втором рабочем месте
if (sojourn[i]>=0) sojourn[i]++:
}
Листинг 7.2. Функция main()
#include Classes?.h
#define N 300 //время моделирования
int mainO
{
long int 1:
//Открытие файлов для сбора статистики
qul-fopenCquel". ”wt"):
7.5. Анализ результатов 151
qu2-fopen( “que2". "wt"):
soj-fopen("soj". "wt"):
srand((unsigned)time(O)): //инициализация ГСЧ
//Создание объектов
WorkPlacel placel:
WorkPlace2 place2:
//Настройка связей между объектами
placel.PutNei ghbour(&place2):
place2.PutNeighbour(&placel):
//Количество тактов моделирования учитывает коэффициент масштабирования
for(i-0L:1<K*N:1++)
{
placel.runO;
plасе2.run();
}
//Закрытие файлов сбора статистики
fclose(soj);
fclose(qul);
fclose(qu2);
//Вывод результатов моделирования, не требующих статистической обработки файлов
printfC"Поступило в систему fcld заявок\п“. entered):
printfC"Завершили обслуживание fcld заявок\п". completed):
printfC"Коэффициент загрузки первого рабочего места fcf\n”. (float)rol/(N*K)):
printfC"Коэффициент загрузки второго рабочего места W. (float)го2/(N*K)):
printf('Отложено fcld заявок\п". postponed):
printf("Общая длительность блокировки fcld\n". (float)blocked/K);
printf("Количество вхождений в состояние блокировки fcld\n“, 1nt_blocked):
}
7.5. Анализ результатов
Расчеты, проведенные при различных значениях масштабирующего коэффи-
циента К, показали, что все основные статистики, полученные при К = 100 и
К = 500, совпадают с точностью до двух знаков после запятой. Все приведенные
далее результаты получены при К = 500:
О всего заявок во входном потоке — 750;
О поступило в систему — 558 (вероятность поступления — 0,744);
О отложено — 192 (вероятность откладывания — 0.256);
О проведено первым рабочим местом в состоянии блокировки — 43,4 % от об-
щего времени;
О средняя длительность одной блокировки — 2,56 единиц времени;
О коэффициент загрузки первого рабочего места — 0,464;
О коэффициент загрузки второго рабочего места — 0,921;
О среднее время пребывания заявки в системе — 3 единицы времени;
О средняя длина очереди к первому рабочему месту — 2,195;
О средняя длина очереди ко второму рабочему месту — 1,491;
О коэффициент задержки (slowdown) = 3/(0,25 + 0,5) = 4.
152 Глава 7. Последовательное обслуживание с блокировками и ограниченным буфером
Из полученных результатов можно сделать вывод, что узким местом системы
является второе рабочее место. Такой вывод вполне закономерен, так как интен-
сивность обслуживания на втором рабочем месте равна 2, а это меньше интен-
сивности входного потока в систему, которая равна 2,5.
Исследуем теперь, каким образом можно улучшить показатели функционирова-
ния системы. Очевидно, что это можно сделать четырьмя способами:
О увеличить число мест для ожидания у первого рабочего места;
О повысить производительность первого рабочего места;
О увеличить число мест для ожидания у второго рабочегр места;
О повысить производительность второго рабочего места.
Разумеется, изменение каждого из перечисленных четырех параметров по-разно-
му влияет на показатели. Исследуем характер этого влияния с помощью разра-
ботанной программы.
Рассмотрим влияние указанных параметров на следующие характеристики системы:
О вероятность откладывания поступившей заявки;
О среднее время пребывания заявки в системе;
О вероятность пребывания первого рабочего места в состоянии блокировки.
На графиках, которые далее будут представлены вниманию читателя, можно за-
метить существенную по сравнению с предыдущей задачей (см. главу 6) особен-
ность: флуктуации (негладкость) кривых, которые то ослабевают, то усиливают-
ся на различных участках области определения. Это означает, что исследуемая
случайная величина подвержена влиянию дисперсии, вызывающей ее видимые
отклонения от математического ожидания как в одну, так и в другую сторону.
В задаче из шестой главы все исходные случайные величины описывались рав-
номерным распределением, а в этой главе — экспоненциальным. Для сравнения:
равномерно распределенная на отрезке [0; 2х] случайная величина имеет мате-
матическое ожидание х и среднеквадратичное отклонение х/-УЗ, а экспоненци-
альная случайная величина с математическим ожиданием х имеет среднеквадра-
тичное отклонение, также равное х. Кроме того, в задаче о больничной палате
входной поток был вообще не случайным, а детерминированным, в отличие от
задачи с производственной линией. Тем не менее, на всех графиках общая тен-
денция к убыванию или возрастанию, несмотря на флуктуации, прослеживается
достаточно четко. Существует специальная теория сглаживания эксперимен-
тальных кривых [28], [31], которую можно применить и в данном случае, однако
мы не будем этим заниматься, так как темой книги является объектное модели-
рование, а не обработка экспериментальных данных.
К чему же приведет увеличение размера буфера первого рабочего места? График
на рис. 7.2 показывает наличие флуктуаций, вызванных влиянием дисперсии чис-
ла отложенных заявок, возрастающей с увеличением размера буфера, но убыва-
ющий характер зависимости хорошо виден. Качество функционирования системы
в целом, конечно, повыситься не может — среднее время пребывания (рис. 7.3)
и вероятность блокировки (рис. 7.4) возрастают. Это происходит потому, что все
большая часть заявок, которые раньше откладывались, поступают в систему, пере-
полняя ее и участвуя в формировании среднего времени пребывания.
7.5. Анализ результатов 153
Рис. 7.2. Зависимость вероятности откладывания от размера буфера первого рабочего места
Рис. 7.3. Зависимость среднего времени пребывания заявки в системе от размера буфера
первого рабочего места
154 Глава 7. Последовательное обслуживание с блокировками и ограниченным буфером
Рис. 7.4. Зависимость вероятности блокировки от размера буфера
первого рабочего места
Увеличение размера буфера второго рабочего места несколько улучшает показа-
тели, но принципиально сохраняются те же тенденции. Вероятность откладыва-
ния заявки убывает (рис. 7.5), соответственно, среднее время пребывания заявки
в системе возрастает (рис. 7.6). Вероятность же блокировки (рис. 7.7) на этот раз
убывает, так как появляются новые места, на которые можно «протолкнуть» об-
служенную на первом рабочем месте заявку.
Повышение быстродействия первого рабочего места (рис. 7.8-7.10) не решает
проблем, так как узкое место системы — второе рабочее место. Среднее время
пребывания заявки в системе асимптотически стремится к некоторой величине,
не меньшей среднего времени обслуживания на втором рабочем месте, а вероят-
ность блокировки растет, так как первое рабочее место более интенсивно пыта-
ется подавать заявки на второе место, быстродействие которого недостаточно.
Увеличивающееся быстродействие второго рабочего места все меньше и меньше
влияет на показатели работы первого, и система постепенно распадается на две
независимые подсистемы типа Л//Л//1/4 и Л//Л//1/2 по Кендаллу—Башарину.
Вероятность откладывания (рис. 7.11) асимптотически стремится к вероятности
нахождения в очереди к первому рабочему месту четырех заявок. Среднее время
пребывания заявки в системе (рис. 7.12) асимптотически стремится к среднему
времени пребывания заявки в подсистеме первого рабочего места, а вероятность
блокировки (рис. 7.13) убывает до нуля.
7.5. Анализ результатов 155
Рис. 7.5. Зависимость вероятности откладывания от размера буфера второго рабочего места
Рис. 7.6. Зависимость среднего времени пребывания заявки в системе от размера буфера
второго рабочего места
156 Глава 7. Последовательное обслуживание с блокировками и ограниченным буфером
Рис. 7.7. Зависимость вероятности блокировки от размера буфера второго рабочего места
Быстродействие первого рабочего места
Рис. 7.8. Зависимость вероятности откладывания от быстродействия первого рабочего места
7.5. Анализ результатов 157
Рис. 7.9. Зависимость среднего времени пребывания заявки в системе от быстродействия
первого рабочего места
Рис. 7.10. Зависимость вероятности блокировки от быстродействия первого рабочего места
158 Глава 7. Последовательное обслуживание с блокировками и ограниченным буфером
Рис. 7.11. Зависимость вероятности откладывания от быстродействия второго рабочего места
Рис. 7.12. Зависимость среднего времени пребывания заявки в системе от быстродействия
второго рабочего места
7.5. Анализ результатов 159
Рис. 7.13. Зависимость вероятности блокировки от быстродействия
второго рабочего места
Воспользуемся теперь построенными зависимостями для решения некоторых раз-
новидностей задач синтеза. Предположим, необходимо обеспечить вероятность
откладывания поступающей заявки не более 0,2. Для этого нужно модифициро-
вать систему одним из следующих способов:
О увеличить буфер первого рабочего места до 13-15 мест;
О увеличить буфер второго рабочего места до 6-7 мест;
О незначительно повысить производительность второго рабочего места (до
2,1-2,2 заявок в единицу времени);
О значительно повысить производительность первого рабочего места (свыше
10 заявок в единицу времени).
Выбор проектировщиком одного из этих способов может быть обусловлен эко-
номическими или какими-либо иными соображениями.
Теперь изменим условие — среднее время пребывания заявки в системе не долж-
но превышать двух единиц времени (заметим, что минимальное среднее время
равно 0,75 — сумме средних времен обслуживания на рабочих местах). Здесь ра-
зумный вариант только один — повысить быстродействие второго рабочего мес-
та до 2,5. Варианты, связанные с уменьшением размера буферов, вряд ли стоит
рассматривать.
160 Глава 7. Последовательное обслуживание с блокировками и ограниченным буфером
И наконец, как снизить вероятность блокировки первого рабочего места до 0,4?
Если не уменьшать буфер первого рабочего места, возможны следующие вари-
анты:
О увеличить буфер второго рабочего места до 4;
О снизить быстродействие первого рабочего места до 3,5, так как второе рабочее
место все равно не справляется со столь интенсивным входным потоком;
О повысить быстродействие второго рабочего места до 2,3-2,4.
Для каждой задачи синтеза мы рассмотрели только варианты, заключающиеся
в изменении лишь одного из параметров системы, все остальные при этом оста-
ются неизменными. Ясно, что достичь большего эффекта можно, изменяя сразу
несколько параметров. Для реализации таких вариантов плоских графиков уже
недостаточно — необходимо строить многомерные функции. Принципиальных
затруднений эта задача не вызывает, но требует достаточно мощного вычисли-
тельного ресурса, трудолюбия от проектировщика и понимания того, какие воз-
можности модифицировать систему следует принять во внимание, а какие от-
бросить как нереальные.
7.6. Аналитический подход
В предыдущем разделе этой главы мы привели большое количество статистиче-
ского материала, который был получен многократными прогонами представлен-
ной ранее программы. При его рассмотрении не может не возникнуть вопрос:
несмотря на видимую логичность всех рассуждений, положенных в основу про-
ектирования программы, корректным ли инструментом исследования мы поль-
зуемся? Действительно ли приведенные цифры и графики имеют отношение к
поведению именно той системы, которую мы описали, или же их правдоподоб-
ность только кажущаяся? Развеять сомнения в корректности методики имита-
ционного моделирования может только использование альтернативного подхода
к решению той же самой задачи. Таким альтернативным подходом является ана-
литическое моделирование. Мы применим его именно к решению данной задачи,
относительная простота которой позволяет описать систему с помощью уравне-
ний. При исследовании в последующих главах более сложных систем применять
аналитический подход будет значительно сложнее, однако совпадение результа-
тов различных решений одной задачи даст определенную уверенность в том, что
та же методика будет давать правильные результаты и в других случаях.
Основными особенностями рассматриваемой системы, делающими возможным
построение вероятностной модели, являются отсутствие возвратов заявок и экс-
поненциальность всех определяющих случайных величин. Введем следующие
обозначения: X — интенсивность входного потока; Ц] — интенсивность обслужи-
вания заявок на первом рабочем месте; ц2 — интенсивность обслуживания зая-
вок на втором рабочем месте; Nt — размер буфера первого рабочего места; N? —
размер буфера второго рабочего места.
Можно предложить следующее пространство состояний: (k„ k2,0), ki = 0,.... М + 1;
k2 = 0,..., N2 + 1, и (ть N2 + 1, 1), тг = 1.+ 1, где для состояния (а, Ь,с) а —
7.6. Аналитический подход 161
количество заявок в подсистеме первого рабочего места, b — количество заявок в
подсистеме второго рабочего места, с — индикатор блокировки. Разумеется, пер-
вое рабочее место может находиться в состоянии блокировки в том и только в
том случае, если буфер второго рабочего места заполнен до отказа. Таким обра-
зом, общее число состояний равно (Nt + 2)(7V2 + 2) + Nt + 1.
Для простоты и повышения наглядности модели рассмотрим случай = N2 = 2.
В этом случае число состояний равно 19. Пронумеруем их:
1)(0, 0, 0); 6) (1,1,0); И) (2, 2,0); 16) (3. 3, 0);
2) (0, 1, 0); 7) (1, 2, 0); 12) (2, 3, 0); 17) (1, 3, 1);
3) (0, 2, 0); 8) (1, 3, 0); 13) (3, 0, 0); 18) (2, 3, 1);
4) (0, 3, 0); 9) (2, 0, 0); 14) (3, 1, 0); 19) (3, 3, 1);
5) (1, 0, 0); 10) (2, 1, 0); 15) (3, 2, 0).
Из условия баланса, состоящего в том, что поток, вводящий в состояние, равен
потоку, выводящему из него, составляем систему линейных уравнений для веро-
ятностей стационарных состояний, где P(i,j, k) — вероятность состояния (г, j, k):
ХР(0, 0, 0) = р2Р(0, 1, 0);
(X + р2)Р(0, 1, 0) = ptP(l. 0, 0) + р2Р(0, 2, 0);
(X + р2)Р(0, 2, 0) = щР( 1, 1, 0) + р2Р(0, 3, 0);
(X + р2)Р(0, 3, 0) = HtP(l, 2, 0) + р2Р(1, 3, 1);
(X + р,)Р(1, 0, 0) = ХР(0, 0, 0) + р2Р(1, 1, 0);
(X + Ц! + р2)Р(1, 1, 0) = ХР(0, 1, 0) + Ц1Р(2, 0, 0) + р2Р(1, 2, 0);
(X + Ц! + р2)Р(1, 2, 0) = ХР(0, 2, 0) + рЛ2. 1, 0) + р2Р(1, 3, 0);
(X + р, + р2)Р(1, 3, 0) = ХР(0, 3, 0) + рЛ2, 2, 0) + р2Р(2, 3, 1);
(X + pt)P(2, 0, 0) = ХР( 1, 0, 0) + р2Р(2, 1, 0);
(X + pj + р2)Р(2, 1, 0) = ХР(1, 1, 0) + р(Р(3, 0, 0) + р2Р(2, 2, 0);
(X + р, + р2)Р(2, 2, 0) = ХР(1, 2, 0) + р(Р(3, 1, 0) + р2Р(2, 3, 0);
(X + Р| + р2)Р(2, 3, 0) = ХР(1, 3, 0) + р(Р(3, 2, 0) + р2Р(3, 3, 1);
PjP(3, О, 0) = ХР(2, 0, 0) + р2Р(3. 1, 0);
(Pi + р2)Р(3, 1,0) = ХР(2, 1,0) + р2Р(3, 2, 0);
(Pt + р2)Р(3, 2, 0) = ХР(2, 2, 0) + р2Р(3, 3, 0);
(р, + Ц2)ДЗ, 3, 0) = ХР(2, 3, 0); (X + р2)Р(1, 3, 1) = ptP(l, 3, 0);
(X + р2)Р(2, 3, 1) = ХР(1, 3, 1) + PtP(2, 3, 0);
р2Р(3, 3, 1) = ХР(2, 3, 1) + ptP(3, 3, 0).
162 Глава 7. Последовательное обслуживание с блокировками и ограниченным буфером
Подставив значения параметров, получаем матрицу системы линейных уравне-
ний, в которой последнее уравнение заменяется условием нормировки — сумма
вероятностей всех состояний равна единице:
-2,5 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 -45 2 0 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 -45 2 0 4 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 -45 0 0 4 0 0 0 0 0 0 0 0 0 2 0 0
25 0 0 0 -65 2 0 0 0 0 0 0 0 0 0 0 0 0 0
0 25 0 0 0 -8,5 2 0 4 0 0 0 0 0 0 0 0 0 0
0 0 25 0 0 0 -8,5 2 0 4 0 0 0 0 0 0 0 0 0
0 0 0 25 0 0 0 -85 0 0 4 0 0 0 0 0 0 2 0
0 0 0 0 25 0 0 0 -85 2 0 4 0 0 0 0 0 0 0
0 0 0 0 0 25 0 0 0 -85 2 0 4 0 0 0 0 0 0
0 0 0 0 0 0 25 0 0 0 -85 2 0 4 0 0 0 0 0
0 0 0 0 0 0 0 2,5 0 0 0 -85 0 0 4 0 0 0 2
0 0 0 0 0 0 0 0 2,5 0 0 0 -4 2 0 0 0 0 0
0 0 0 0 0 0 0 0 0 25 0 0 0 -6 2 0 0 0 0
0 0 0 0 0 0 0 0 0 0 25 0 0 0 -6 2 0 0 0
0 0 0 0 0 0 0 0 0 0 0 25 0 0 0 -6 0 0 0
0 0 0 0 0 0 0 4 0 0 0 0 0 0 0 0 -45 0 0
0 0 0 0 0 0 0 0 0 0 0 4 0 0 0 0 25 -45 0
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
Система линейных уравнений была решена с помощью пакета Maple [14], [15].
Получены следующие результаты (округлены с точностью до трех знаков):
1) 0,041; 7) 0,047; 13)0,024; 19)0,184.
2) 0,051; 8) 0,062; 14) 0,022;
3) 0,061; 9) 0,020; 15) 0,028;
4) 0,067; 10) 0,031; 16) 0,031;
5) 0,027; И) 0,042; 17) 0,055;
6) 0,035; 12) 0,075; 18) 0,097;
Вероятность откладывания заявки равна сумме вероятностей тех состояний,
в которых буфер первого рабочего места полностью занят, то есть состояний 13,
14, 15, 16, 19. Получается 0,289. Среднее число заявок в системе из полученного
вектора стационарных вероятностей равно 3,831. Интенсивность поступлений в
систему равна интенсивности входного потока, умноженной на вероятность того,
что буфер первого рабочего места заполнен не полностью, так как в противном
случае заявка не поступает в систему, а откладывается. Имеем 2,5 • (1 - 0,289) =
= 1,7775. Тогда, по теореме Литтла, среднее время пребывания заявки в системе
равно 3,831/1,7775 = 2,155. Вероятность блокировки равна сумме вероятностей
состояний, для которых третье число равно 1, то есть состояний 17, 18, 19. Име-
ем 0,336.
Для сравнения с результатами имитационного эксперимента вновь обратим вни-
мание на графики, изображенные на рис. 7.2-7.4. Значению = 2 на каждом из
них соответствует начальная точка кривой. Нетрудно заметить, что результаты
Задание для самостоятельной работы 163
имитационного моделирования находятся в полном соответствии с результата-
ми, полученными из аналитической модели, а значит, наши программы достой-
ны доверия.
Выводы
1. Коэффициент масштабирования времени должен быть таким, чтобы все со-
бытия в системе происходили в целочисленные моменты времени, и погреш-
ностью округления при генерации случайных величин можно было бы пре-
небречь. Масштабирование не влияет на безразмерные характеристики
системы. Характеристики, измеряемые в единицах времени, требуют итого-
вой корректировки.
2. Очереди с ограниченным количеством мест следует представлять массивами.
3. Связь между объектами реализуется с помощью перекрестных указателей —
бестиповых (с последующим приведением) или имеющих тип (с предвари-
тельным объявлением класса).
4. Инициализация связей производится не в конструкторах, а после создания
объектов с помощью специальных методов классов.
5. Совпадение результатов имитационной и аналитической модели свидетель-
ствует о корректности используемой методики имитационного моделиро-
вания.
Задание для самостоятельной работы
Предложите объектную модель конвейерной системы, состоящей из произволь-
ного количества этапов (рабочих мест), для которой рассмотренная задача будет
частным случаем. Количество рабочих мест задается некоторой глобальной кон-
стантой или макроопределением, каждое рабочее место имеет свой набор число-
вых характеристик.
Глава 8
Последовательное
обслуживание
с возвращениями:
производственная линия
с пунктами технического
контроля и настройки
Основные вопросы, рассматриваемые
в данной главе:
□ Обратная связь: заявки возвращаются
на повторное обслуживание
□ Как моделировать многоканальный
узел обслуживания
□ Очередь неограниченной длины:
массив или список?
□ Вычисляемая характеристика
объекта: поле данных или метод?
□ Повышение производительности
системы: увеличить количество
или улучшить качество?
8.1. Описание системы 165
8.1. Описание системы
Собранные телевизоры на заключительной стадии производства проходят ряд
пунктов технического контроля. В последнем из этих пунктов проверяется на-
стройка телевизоров. Если при проверке обнаружилось, что телевизор работает
некачественно, он направляется в пункт настройки, где настраивается заново.
После перенастройки телевизор снова направляется в последний пункт контро-
ля для проверки качества настройки. Телевизоры, которые сразу или после не-
скольких возвратов в пункт настройки прошли фазу заключительной проверки,
направляются в цех упаковки.
Схема системы контроля и настройки телевизоров показана на рис. 8.1. Кружка-
ми обозначены телевизоры, причем пустыми кружками — телевизоры, которые
ожидают заключительной проверки, а перечеркнутыми — телевизоры, которые
еще не прошли настройки и либо настраиваются, либо стоят в очереди к пункту
настройки.
Возврат настроенных телевизоров
илидающис кишрилм
телевизоры Контролеры
Рис. 8.1. Система контроля и настройки на производственной линии
Время между поступлениями телевизоров в пункт контроля для заключитель-
ной проверки распределено равномерно на интервале 3,5-7,5 мин. В пункте за-
ключительной проверки параллельно работают два контролера. Время, необхо-
димое на проверку одного телевизора, распределено равномерно на интервале
6-12 мин. В среднем 85% телевизоров проходят проверку успешно и направля-
ются на упаковку. Остальные 15% возвращаются в пункт настройки, обслужи-
ваемый одним рабочим. Время настройки распределено равномерно на интерва-
ле 20-40 мин.
Необходимо сымитировать работу пунктов контроля и настройки в течение
480 мин для оценки времени, затрачиваемого на обслуживание каждого телеви-
зора на последнем этапе производства, а также загрузки контролеров и настрой-
щика.
166 Глава 8. Последовательное обслуживание с возвращениями
8.2. Модельное время
Никаких проблем со временем в этой задаче не возникает. Естественной еди-
ницей модельного времени является секунда. Необходимости масштабировать
время нет, так как равномерное распределение для этой задачи предполагает це-
лочисленные значения (в секундах) всех генерируемых случайных величин в за-
данных диапазонах. Моделируется не непрерывное производство, поэтому общая
длительность моделирования также выбрана разумно — 8 ч, то есть типичная
продолжительность одного рабочего дня.
8.3. Классы и объекты
В отличие от ранее рассмотренных схем, данная характеризуется наличием об-
ратной связи — заявка может совершить цикл и вернуться на то место, где уже
один или несколько раз побывала. Из теории стохастических сетей известно
[24], что при наличии циклов даже в случае экспоненциальное™ всех распре-
делений, описывающих систему, результирующие входные потоки в отдельные
узлы теряют пуассоновское свойство. Если к тому же учесть, что экспоненциаль-
ных распределений в задаче нет вообще, становится очевидной невозможность
или крайняя трудоемкость моделирования системы аналитическими средствами,
из чего следует целесообразность применения имитационного моделирования.
Перечислим наиболее существенные особенности моделируемой системы — как
уже знакомые нам, так и новые, — которые помогут нам лучше спроектировать
программу:
О наличие многоканальных узлов обслуживания — в пункте контроля находят-
ся два контролера, работающие параллельно. Чтобы расширить общность мо-
дели, будем предполагать наличие произвольного числа контролеров;
О отсутствие каких-либо ограничений на длину очереди в узлах обслуживания;
О наличие двусторонней связи между пунктами контроля и настройки.
Введем два класса — Пункт Контроля (Control) и ПунктНастройки (Debugger). Каж-
дый из этих классов будет представлен в программе одним объектом. Здесь воз-
никает вопрос: почему не представить отдельным объектом каждого контролера
и каждого настройщика, определив предварительно классы Контролер и Настрой-
щик? В этом случае каждому узлу будет соответствовать количество объектов, со-
ответствующее количеству каналов обслуживания, которыми он располагает
(в данном случае — 2 и 1). Здесь необходимо принять во внимание то, насколько
однородными являются различные каналы многоканального узла, насколько
различается информация, которую каждый из них должен хранить, и насколько
велик объем этой информации. В данной задаче каналы являются абсолютно од-
нородными, а объем информации составляет ровно одну единицу — время, ос-
тавшееся до завершения обслуживания заявки (или значение -1, если канал
простаивает). Представление многоканального узла одним объектом удобно и с
8.3. Классы и объекты 167
той точки зрения, что существуют параметры, относящиеся ко всему узлу в це-
лом: входной поток, длина очереди, — которые при отсутствии «объемлющего»
объекта непонятно к чему относить.
Так как необходимо собрать статистику о времени пребывания заявки (теле-
визора) в системе, движение каждой заявки придется отслеживать от начала до
конца, поэтому ограничиться только параметром Количество заявок или Длина оче-
реди мы не можем. Введем еще один класс — Телевизор (TV), который будет пред-
ставлен в системе переменным числом объектов. Особой функциональности от
этого класса не требуется, его объекты будут хранить только уникальный иден-
тификатор и время, проведенное телевизором в системе с момента поступления.
Какие структуры данных понадобятся для поддержания актуальной информа-
ции о состоянии узла? Заявки, находящиеся в данный момент на обслуживании,
удобно хранить в виде массива указателей на объекты класса TV, так как их коли-
чество имеет заданную верхнюю границу — число каналов узла, i-й элемент мас-
сива соответствует г-му каналу обслуживания и будет содержать указатель на
обслуживаемый объект либо NULL — в случае простоя. Потребуется также еще
один массив — для хранения времен, оставшихся до завершения обслуживания
текущей заявки каждым из узлов. Длина очереди является переменной величи-
ной, для которой верхняя граница не определена, поэтому для хранения заявок,
находящихся в очереди, лучше использовать список. Шаблон для списков уже
подготовлен нами ранее, осталось лишь подставить в него нужный тип, в данном
случае TV. Характерной особенностью использования шаблона в настоящей зада-
че является то, что удаление объектов, соответствующее событию перехода из
очереди на обслуживание, может происходить только из головы списка, причем
сам объект физически не удаляется из памяти, а просто меняет свою дисло-
кацию внутри системы. Напомним, что в предыдущем примере использования
шаблона (см. главу 6) из списка мог быть удален произвольный объект, так как
выписка из палаты происходила не в порядке очереди, а по достижении больным
определенного рейтинга.
Вопрос взаимодействия объектов ничем не отличается от рассмотренного в гла-
ве 7. Покинув пункт настройки, телевизор обязательно попадает в пункт контро-
ля, а покинув пункт контроля, может попасть в пункт настройки, а может и не
попасть. Таким образом, завершение обслуживания заявки в одном из узлов
приводит или может привести к поступлению этой же заявки в другой узел, по-
этому каждый из узлов должен располагать адресом другого, чтобы отправить
туда обслуженную им заявку. Способ хранения адреса остается прежним — бес-
типовый указатель void*, приводимый при обращении к нужному типу.
Параллелизм обслуживания (в данном случае — наличие двух одновременно ра-
ботающих контролеров) реализуется в этой и других задачах последовательным
образом путем опроса в некотором порядке объектов-серверов на каждом такте
модельного цикла. На правильность реализации модели это никак не влияет.
Перечислим на основе проведенных рассуждений поля данных введенных нами
классов.
168 Глава 8. Последовательное обслуживание с возвращениями
8.3.1. Класс Пункт_Контроля
Неизменяемые поля данных:
О нижняя граница для интервала времени между поступлениями новых заявок
(3,5 мин = 210 с);
О верхняя граница для интервала времени между поступлениями новых заявок
(7,5 мин = 450 с);
О нижняя граница для длительности обслуживания заявки (6 мин = 360 с);
О верхняя граница для длительности обслуживания заявки (12 мин = 720 с);
О количество параллельных каналов (2);
О вероятность удачного прохождения контроля (0,85) — частный случай мар-
шрутной вероятности движения заявки в сети обслуживания.
Изменяемые поля данных:
О время, оставшееся до прибытия новой заявки;
О текущая длина очереди;
О список заявок, находящихся в очереди;
О массив времен, оставшихся до завершения обслуживания текущей заявки каж-
дым из каналов;
О массив указателей на заявки, находящиеся на обслуживании в каждом из ка-
налов;
О указатель на объект класса ПунктНастройки.
Заметим, что текущая длина очереди является избыточным полем — ее вычис-
ление можно оформить в виде метода, «пробегающего» по списку от начала до
конца и подсчитывающего количество элементов. Представляется, однако, ра-
зумным избегать этой трудоемкой процедуры и хранить длину очереди отдель-
ным полем данных, производя при необходимости его инкремент и декремент.
Впрочем, это дело вкуса разработчика, и принципиального значения для проек-
тирования программы не имеет. То же самое относится и к такой динамической
характеристике, как количество в узле занятых каналов. В отличие от длины
очереди, в число полей данных она не включена. Для ее вычисления в протоко-
лы обоих классов добавлен специальный метод busyO.
8.3.2. Класс Пункт.Настройки
Неизменяемые поля данных:
О нижняя граница для длительности обслуживания заявки (20 мин = 1200 с);
О верхняя граница для длительности обслуживания заявки (40 мин = 2400 с);
О количество параллельных каналов (1).
О Хотя в условии задачи узел Пункт Настройки и не является многоканальным,
в программной реализации лучше предусмотреть такую возможность, чтобы
в дальнейшем можно было экспериментировать с добавлением дополнитель-
ных настройщиков, если настройка окажется узким местом системы.
8.4. События и методы 169
Изменяемые поля данных:
О текущая длина очереди;
О список заявок, находящихся в очереди;
О массив времен, оставшихся до завершения обслуживания текущей заявки каж-
дым из каналов;
О массив указателей на заявки, находящиеся на обслуживании в каждом из ка-
налов;
О указатель на объект класса Пункт Контроля.
Во время нахождения заявок в узлах контроля и настройки для них на каждом
такте модельного времени придется производить инкремент времени, проведен-
ного в системе. Это можно сделать посредством написания дополнительного
метода для класса TV либо непосредственно — объявив классы Пункт_Контроля
и Пункт Настройки друзьями класса TV.
8.4. События и методы
Для каждого из классов (за исключением, конечно, класса TV) имеют места ровно
два типа событий — поступление заявки и завершение обслуживания. Следова-
тельно, кроме метода run() и служебных методов наподобие PrintO для них не-
обходимо написать два метода — прибытие (arrival О) и завершение обслужива-
ния (completed). Обсудим некоторые нюансы этих событий и их отражение в
прототипах и вызовах методов. Метод completed при вызове требует передачи
параметра — номера канала, который завершил обслуживание. Поэтому его про-
тотип выглядит так:
void complete(int i):
Прибытие же заявки в пункт контроля возможно в двух вариантах — прибытие
новой заявки извне на первый контроль и прибытие заявки на повторный кон-
троль с пункта настройки. В первом случае параметр не требуется, так как созда-
ется новый объект класса TV, во втором случае требуется параметр — указатель
или ссылка на уже существующий объект. Писать два отдельных метода нет не-
обходимости. Прототип метода следует задать в таком виде:
void arrival(TV *t):
и в случае прибытия новой заявки вызывать метод с параметром NULL. Заявка,
прибывающая в пункт настройки, уже заведомо существует в системе и обраба-
тывается исключительно по второму варианту — указатель в этом случае не дол-
жен быть нулевым. Подробнее логика методов отражена в комментариях к при-
веденному далее листингу программы (листинг 8.1).
Листинг 8.1. Файл Classes8.h с описанием протоколов классов
#include<cstdio>
#i nclude<cstdlib>
#include<ctime>
using namespace std;
#include "List.h" //подключение файла с описанием шаблона списка
170 Глава 8. Последовательное обслуживание с возвращениями
FILE *sojourn: //файл для сбора статистики времени пребывания заявки //в системе
FILE *quel: //файл для сбора статистики длины очереди к пункту //контроля
FILE *que2: //файл для сбора статистики длины очереди к пункту //настройки
int brak=O: //счетчик случаев выбраковки изделий в пункте //контроля
float rol_aver=0: //переменная для подсчета средней загрузки пункта //контроля
float ro2_aver: //переменная для подсчета средней загрузки пункта //настройки
float soj_aver: //переменная для подсчета среднего времени пребывания //заявки в системе
float quel_aver: //переменная для подсчета средней длины очереди //к пункту контроля
float que2_aver: //переменная для подсчета средней длины очереди //к пункту настройки
long int total: //счетчик модельного времени (количество секунд)
long int completed: //счетчик числа заявок, завершивших обслуживание //(телевизоров, ушедших на упаковку)
long int entered: //счетчик числа заявок, поступивших в систему
//Класс для представления заявок (телевизоров)
class TV
{
long Int id: //уникальный идентификатор заявки
Int seconds: //время, проведенное в системе
public:
//Объявление классов Control и Debugger дружественными
friend class Control:
friend class Debugger:
TV() //метод-конструктор
{
id-entered; //какой по счету поступила заявка, такой
//и идентификатор
seconds-O;
}
void PrintO:
long int getldO:
int getTimeO:
}:
//Вывод содержимого объекта
void TV::Print()
{
printf(”id=tld\n проведено в системе-td секунд\п”. id. seconds):
}
//Чтение идентификатора заявки
long int TV::getId()
{
return(id);
}
//Чтение проведенного в системе времени
int TV: igetTimeO
{
return(seconds):
Листинг 8.1. Файл Classes8.h с описанием протоколов классов 171
}
//Протокол класса Пункт_контроля
class Control
{ int to_arrival: //время до прибытия следующей заявки извне
int q_length: //текущая длина очереди
ListNode<TV> *queue: //список заявок, находящихся в очереди
int *to_served: //массив времен, оставшихся до завершения //обслуживания заявки в каждом из каналов, //в случае простоя -1
TV **serving: //массив указателей на заявки, находящиеся //на обслуживании в каналах, в случае //простоя канала NULL
void *debug: //указатель на пункт настройки
const static int arrl-210: //минимальное время между прибытиями заявок //извне
const static int arr2-450; //максимальное время между прибытиями //заявок извне
const static int serv1-360; //минимальное время проверки телевизора
const static int serv2-720: //максимальное время проверки телевизора
const static int volume-2: //количество каналов (контролеров)
const static int success-85: public: //процент успешных прохождений контроля
Control(): //конструктор
-Control(): //деструктор
void run(): //диспетчер
void arrival(TV *t): //прибытие телевизора на проверку
//установление связи с пунктом настройки void putDebug(Debugger *d) { debug-d:
}
void complete(1nt i): //завершение проверки телевизора
int busyO: //вычисление числа занятых контролеров
}:
//Протокол класса Пункт настройки
class Debugger
{ int q_length: //текущая длина очереди к настройщику
ListNode<TV> *queue: //список заявок, находящихся в очереди
int *to_served: //массив времен, оставшихся до завершения
TV **serving: //обслуживания заявки в каждом из каналов, //в случае простоя -1 //массив указателей на заявки, находящиеся
void *cont: //на обслуживании в каналах, в случае //простоя канала NULL //указатель на пункт контроля
const static int serv1=1200: //минимальное время настройки телевизора
const static 1nt serv2-2400: //максимальное время настройки телевизора
const static int volume-1: //количество настройщиков
public: Debugger!): //конструктор
-Debugger!): //деструктор
void run(): //диспетчер
void arrival(TV *t): //прибытие телевизора на настройку
//Установление связи с пунктом контроля
172 Глава 8. Последовательное обслуживание с возвращениями
void putCont(Control *с)
{
cont-c:
}
void completednt 1): //завершение настройки телевизора
Int OusyO; //вычисление числа занятых настройщиков
}:
//Конструктор для пункта контроля
Control::Control О
{
int i:
q_length=0:
queue-NULL:
//Розыгрыш интервала времени до прибытия заявки
to_arrival-arrl+rand()X(arr2-arrl+l);
//Выделение памяти под массивы
to_served=new 1nt[volume]::
serving=new TV *[volume]:
//Инициализация массивов
for(i =0:i <volume:1++) to_served[1]--1:
for(i-0:i<volume;1++) serving[i]=NULL;
}
//Деструктор для пункта контроля
Control::—Control()
{
deleted to_served:
delete [] serving:
}
//Вычисление числа занятых контролеров
int Control::busy()
{
int i. k;
k-0:
//Признак занятости: положительное время, оставшееся до завершения //Проверки телевизора
for(1-0:i<volume:i ++)
if (to_served[i]>0) k++:
return(k);
}
//Прибытие телевизора для прохождения контроля
void Control: arrival (TV *t)
{
Int 1;
TV *p:
if (!t) //телевизор новый, проверка первая
{
entered++: //инкремент счетчика поступлений в систему
p-new TVO: //создание нового объекта-телевизора
//Розыгрыш нового интервала времени между поступлениями
to_arri va1-arrl+rand()Х(arr2-arrl+l):
}
else p=t: //телевизор не новый, проверка повторная
//Поиск свободного контролера
for (1-0:i<volume:i++)
if (to_served[1]“-l) break:
//Свободный контролер найден
Листинг 8.1. Файл Classes8.h с описанием протоколов классов 173
if (i<volume)
{
//Ставим телевизор к нему на обслуживание
servingCi]=р;
//Разыгрываем время обслуживания
to_served[1]=servl+rand()Х(serv2-servl+l);
}
else //все контролеры заняты
{
//Создаем элемент списка для постановки телевизора в очередь
L1stNode<TV> *ptr=new ListNode<TV>(p.NULL);
//Если очередь пуста, делаем его головой списка
1f (q_length“0) queue-pt г:
//иначе ставим в конец очереди (хвост списка)
else ListAdd<TV>(queue,ptr):
q_length++: //инкремент длины очереди
}
return:
}
//Завершение проверки телевизора i-м контролером
void Control:: completed nt 1)
{ v
int a:
//Телевизор успешно прошел проверку
if ((rand()1100)<-(success-D)
{
completed++; //инкремент счетчика телевизоров.
//покинувших систему
a-serving[i]->getTime(): //считывание окончательного времени
//пребывания
fprintf(sojourn.,lf\n,,. ((float)a)/60); //запись в файл (в минутах)
//Пересчет среднего времени пребывания в системе
so j_aver-soj_aver*(1-1.0/completed)+((float)a)/completed:
//Удаление объекта из системы
delete servingCi];
}
else //телевизор не прошел проверку качества
{
//Отправка телевизора в пункт настройки
((Debugger*)debug)->а rri val(servi ng[1]);
brak++: //инкремент счетчика неудачных проверок
}
//Попытка загрузить освободившегося контролера
if (q_length—0) //очередь пуста
{
//Контролер объявляется простаивающим
to_served[1]--l:
serving[i]=NULL:
}
else //очередь не пуста
{
//Извлекаем из очереди первый телевизор и ставим его на проверку
servi ng[1]=queue->Data():
//Разыгрываем время проверки
to_served[i]-servl+rand()l(serv2-servl+l);
174 Глава 8. Последовательное обслуживание с возвращениями
//Головой списка-очереди становится следующий телевизор
queue-queue->Next():
q_length--: //декремент текущей длины очереди
}
}
//Метод-диспетчер
void Control::run()
{
int i:
float a:
ListNode<TV> *ptr:
//Декремент времени, оставшегося до завершения проверки каждым контролером
for(i-0:i<volume: i++)
{
if (to_served[i]“-l) continue: //контролер простаивает, декремент не нужен
--to_served[i]:
//Вызов метода-обработчика, имитирующего завершение проверки
if (to_served[i]==0) completed): }
to_arr1val--:
//Вызов метода-обработчика, имитирующего прибытие нового телевизора
if (to_arrival“0) arrival (NULL):
//Инкремент длительности пребывания в системе для телевизоров, находящихся
//на обслуживании
for(i-0:i<volume:i++)
if (to_served[i]>0) (serving[1]->seconds)++:
//Инкремент длительности пребывания в системе для телевизоров, ожидающих
//проверки в очереди (если очередь не пуста)
if (queue)
{
ptr-queue:
whlle(ptrl-NULL)
{
((ptr->Data())->seconds)++:
ptr-ptr->Next():
}
}
//Вычисление текущего коэффициента загрузки пункта контроля
а-((f1oat)busy())I volume;
//Пересчет среднего значения
rol_aver-rol_aver*(1-1.О/(total+1))+а/(total+1):
//Запись в файл текущей длины очереди на проверку
fpri ntf(quel."Xd\n”. q_length);
//Пересчет средней длины очереди
quel_aver-quel_aver*(l-1.0/(total+l))+((float)q_length)/(total+l):
return:
}
//Конструктор для пункта настройки
Debugger::Debugged)
{
int i:
q_length=0:
queue=NULL:
to_served-new 1nt[volume]:
serving=new TV *[volume]:
Листинг 8.1. Файл Classes8.h с описанием протоколов классов 175
for(1=0:1<volume:1++) to_served[1]=-1:
for(1-0:1<volume:1++) servi ng £ 1]=NULL;
}
//Деструктор для пункта настройки
Debugger::-Debugger()
{
delete[] to_served:
delete [] serving:
}
//Вычисление числа занятых настройщиков
Int Debugger::busyО
{
Int 1, k:
k-0:
for(1-0:i<volume:1++)
If (to_served[1]>0) k++:
return(k):
}
//Прибытие телевизора в пункт настройки
void Debugger: arrival (TV *t)
{
' int 1:
//Поиск свободного настройщика
for (1-0:i<volume:1++)
If (to_served[1]==-l) break:
//Свободный настройщик найден
//Телевизор поступает к нему на обслуживание
if (1<volume)
{
servlngti]-t:
to_served[1]=servl+rand()l(serv2-servl+l);
}
//Все настройщики заняты. Телевизор ставится в очередь
else
{
L1stNode<TV> *ptr-new ListNode<TV>(t.NULL):
If (q_lengtti—O) queue-ptr;
else ListAdd<TV>(queue.ptr):
q_length++;
}
return;
}
//Завершение настройки телевизора
void Debugger: complete(int 1)
{
int a;
//Отправка телевизора на новую проверку
((Control*)cont)->arrival(servlngti]):
//Очередь пуста, освободившийся настройщик отдыхает
if (q_length—0)
{
to_served[1]—1:
serving[i]-NULL:
}
176 Глава 8. Последовательное обслуживание с возвращениями
//Очередь не пуста. Телевизор, ожидающий первым, ставится на обслуживание
//к освободившемуся настройщику
else
{
serving[i]=queue->Data():
to_served[l]=servl+rand(Wserv2-servl+l):
queue=queue->Next():
qlength--:
}
}
//Диспетчер
void Debugger::run()
{
int 1:
float a;
ListNode<TV> *ptr:
for(i =0;i <volume:1++)
{
if (to_served[i]==-l) continue:
to_served[i]--:
if (to_served[i]“0) completed):
}
ford =0: i <vol ume: i++)
if (to_served[1]>0) (serving[i]->seconds)++:
if (queue!=NULL)
{
ptr-queue:
while(ptr!=NULL)
{
ptr->Data()->seconds++:
ptr=ptr->Next():
}
} .
a=((f1oat)busy())/volume:
//Пересчет средней загрузки пункта настройки
ro2_aver-ro2_aver*(1-1.0/(total+1))+а/(total+1);
fprintf(que2."Xd\n". q_length):
//Пересчет средней длины очереди в пункте настройки
que2_aver=que2_aver*(1-1.0/(total+1))+((f1oat)q_length)/(total+1):
}
Листинг 8.2. Функция main()
#define N 28800 //общее время моделирования - 8 часов
#include “Classes8.h"
int main()
{
//Открытие файлов для сбора статистики
quel=fopenC’quer. "wt"):
que2-fopen("que2“. "wt");
sojourn=fopen("sojourn”. "wt"):
//Инициализация генератора случайных чисел
srand((unsigned)time(O)):
//Инициализация объектов
8.5. Анализ результатов 177
Control с:
Debugger d:
//Установление связи между ними
c.putDebug(Sd):
d.putCont(Sc):
//Основной моделирующий цикл
for(total=OL:total<N;total++)
{
c.runO:
d.runO:
}
//Закрытие файлов сбора статистики
fclose(sojourn):
fclose(quel):
fclose(que2):
//Вывод на печать результатов имитационного эксперимента
printf(’’Всего поступлений в систему Xld\n", entered):
printf( "Успешно прошли проверку Xld\n”. completed):
prlntf("4ncno неудачных проверок Xd\n". brak):
printf("Коэффициент загрузки пункта контроля X.3f\n". rol_aver):
prlntfC"Коэффициент загрузки пункта настройки X.3f\n". ro2_aver):
prlntf("Средняя длительность пребывания в системе X.3f\n". (float)soj_aver/60):
prlntfC"Средняя длина очереди в пункте контроля X.3f\n", quel_aver):
prlntfC"Средняя длина очереди в пункте настройки X.3f\n", que2_aver):
}
8.5. Анализ результатов
Тысячекратный прогон программы и усреднение результатов дали следующие
среднестатистические показатели работы системы при входных параметрах, за-
данных в условии задачи:
О всего поступлений в систему — 87 телевизоров;
О из них обслужено — 81 телевизоров;
О коэффициент загрузки пункта контроля — 0,90;
О коэффициент загрузки пункта настройки — 0,75;
О среднее время пребывания в системе — 21 мин 12 с;
О средняя длина очереди в пункт контроля — 0,58 телевизоров;
О средняя длина очереди в пункт настройки — 1,07 телевизоров.
Сразу же заметим, что все приведенные цифры не являются характеристиками
стационарного режима, а характеризуют работу системы за конкретное время — 8 ч.
Стационарного режима здесь нет, так как при средней интенсивности входного
потока 1/5,5 = 0,18 телевизоров в минуту средня^ интенсивность обслуживания
равна приблизительно 0,16 телевизоров в минуту. Для читателей, чувствующих
себя уверенно в вероятностно-статистических рассуждениях, в приложении 2 при-
ведено доказательство этого утверждения. Поэтому при увеличении общей дли-
тельности моделирования показатели меняются в худшую сторону. Так, например,
среднее время пребывания заявки в системе за 28 ч увеличивается до 30 мин.
178 Глава 8. Последовательное обслуживание с возвращениями
В качестве критериев эффективности системы примем два показателя: среднее
время пребывания в ней одного телевизора и среднее число телевизоров, выпу-
щенных за один рабочий день. Естественно, система работает тем эффективнее,
чем меньше первый показатель и чем больше второй. Предположим, что в силу
технологических причин сократить время контроля и настройки нельзя. Попро-
буем повысить эффективность функционирования экстенсивным путем — уве-
личением количества контролеров или настройщиков. Рисунки 8.2 и 8.3 показы-
вают, что этот путь бесперспективен.
Скачок наблюдается лишь при увеличении числа контролеров с 1 до 2. Объяс-
няется это тем, что при интенсивности входного потока 0,18 средняя интенсив-
ность обслуживания в пункте контроля возрастает от 1/9 = 0,11 до 2/9 = 0,22,
то есть становится выше интенсивности входного потока. Дальнейшее увеличе-
ние числа контролеров на показатели системы практически не влияет. А число
настройщиков, как видно из рис. 8.4 и 8.5, вообще не имеет никакого значения
(при данной вероятности успешной проверки!).
Попробуем пойти другим путем — интенсивным, то есть повысить не количест-
во, а качество. Рассмотрим, как влияет на показатели системы изменение вероят-
ности успешного прохождения контроля.
Количество контролеров
Рис. 8.2. Зависимость времени пребывания заявки в системе от числа контролеров
8.5. Анализ результатов 179
Количество контролеров
Рис. 8.3. Зависимость производительности системы от числа контролеров
Количество настройщиков
Рис. 8.4. Зависимость времени пребывания заявки в системе от числа настройщиков
180 Глава 8. Последовательное обслуживание с возвращениями
82,4 —
<и
э 81,8 --------
81,6 ----
О 10 20 30 40 50
Количество настройщиков
Рис. 8.5. Зависимость производительности системы от числа настройщиков
Вероятность успешной проверки
Рис. 8.6. Зависимость времени пребывания заявки в системе от вероятности успешной проверки
Выводы 181
Вероятность успешной проверки
Рис. 8.7. Зависимость производительности от вероятности успешной проверки
Как видно из графиков, изображенных на рис. 8.6 и 8.7, этот путь более перспек-
тивен. Заметим, что поведение зависимости, изображенной на рис. 8.6, при ма-
лых значениях вероятности успеха не является статистически представительным,
так как слишком малое число телевизоров успевают выйти из системы в течение
8 часов. При 100% успеха среднее время пребывания телевизора в системе при-
ближается к 9 — среднему времени обслуживания в пункте контроля. Произво-
дительность же системы имеет четкую тенденцию к росту. Таким образом, для
улучшения качества функционирования системы нет смысла принимать новых
работников, гораздо важнее повысить квалификацию сборщиков и настройщи-
ков, чтобы телевизоры поступали в пункт контроля в «товарном» состоянии
(если предположить, конечно, что контролеры работают абсолютно беспристра-
стно).
Выводы
1. Многоканальный узел с однородными каналами можно представить одним
объектом. Обслуживаемые заявки — это поле данных, являющееся массивом
указателей на объекты.
2. Очередь неограниченной длины следует представлять связным списком.
182 Глава 8. Последовательное обслуживание с возвращениями
3. Если характеристика объекта, требующаяся для сбора статистики, является
вычисляемой, следует оценить трудоемкость ее вычисления. Если она ока-
жется высока — ввести отдельное поле данных.
4. Если узел потенциально может быть многоканальным, следует учесть это при
проектировании класса для него, сделав число каналов полем данных.
5. При наличии обратной связи поток заявок на обслуживание имеет две со-
ставляющие — новые заявки (их надо порождать в системе) и уже сущест-
вующие, вернувшиеся на повторное обслуживание. Это обстоятельство сле-
дует учитывать при разработке соответствующего метода.
6. Существование стационарного режима в моделируемой системе необязатель-
но, если целью моделирования является получение характеристик за ограни-
ченный, сравнительно небольшой промежуток времени.
7. Экстенсивное развитие системы (ввод в действие нового оборудования, уве-
личение штата сотрудников) не всегда приводит к повышению эффективно-
сти ее функционирования. Значительно более эффективным может оказаться
повышение качества работы тех составляющих, которые уже присутствуют
в системе.
Задания для самостоятельной работы
1. В протоколах классов Control и Debugger много общего кода.'Предложите мо-
дель классов с применением наследования, позволяющую сократить повторе-
ния кода.
2. Промоделируйте при тех же условиях дисциплину случайного выбора теле-
визора из очереди контролером и настройщиком. Как изменится при этом
среднее время его пребывания в системе?
3. В моделируемой системе вероятность успешного прохождения проверки оди-
накова — проходит ли телевизор проверку в первый или в десятый раз. Более
реалистичным является предположение, что после каждой следующей на-
стройки вероятность успешной проверки возрастает. Приняв, например, за-
кон этого возрастания линейным, промоделируйте систему при таком пред-
положении и сравните результаты. Учтите, что класс TV придется
модифицировать — его объекты должны будут хранить дополнительную ин-
формацию о количестве пройденных проверок.
4. В классе Control объедините поля данных serving и to_served в одну структуру,
сделав массив указателей на эту структуру полем данных класса. Поле дан-
ных serving объявите в структуре как объект класса, а не как указатель на
него. Внесите соответствующие изменения в коды методов.
Глава 9
Замкнутая система
с неоднородными
каналами: моделирование
грузовых автоперевозок
Основные вопросы, рассматриваемые
в данной главе:
□ Специфика замкнутой системы
□ Многоканальный узел
с неоднородными каналами:
выбор свободного канала
□ Какие сущности описывать классом?
□ Выделение множества состояний
объекта
□ Новый режим работы системы: как
адаптировать программу?
□ Сравнение результатов для двух
режимов. Выявление узкого места
системы
184 Глава 9. Замкнутая система с неоднородными каналами
9.1. Описание системы
Моделируемая система состоит из одного бульдозера, четырех самосвалов и двух
механизированных погрузчиков. Бульдозер сгребает землю к погрузчикам. Для
начала погрузки перед погрузчиками должны лежать две кучи земли. Один са-
мосвал полностью загружается одной кучей. Каждая из куч относится только к
одному, «своему» погрузчику, одна — для одного, другая — для другого, то есть
перед каждым погрузчиком должно лежать по куче, с которой будет работать
(загружать самосвал) только этот погрузчик. Время, затрачиваемое бульдозера-
ми на подготовку фронта работ до начала погрузки, имеет распределение Эрланга
и состоит из суммы двух экспоненциальных величин, каждая из которых имеет
математическое ожидание, равное 4 мин (это соответствует эрланговскому рас-
пределению с математическим ожиданием 8 мин и дисперсией 32). Кроме нали-
чия земли для начала погрузки требуется погрузчик и порожний самосвал. Время
погрузки распределено экспоненциально с математическим ожиданием 14 мин
для первого погрузчика и 12 мин для второго.
Возвращение самосвала на погрузку
Самосвалы <
Работа
бульдозера
Очередь
на погрузку
Погрузка Перевозка Выгрузка
Рис. 9.1. Система грузовых автоперевозок
После того как самосвал загружен, он уезжает к месту разгрузки, разгружается
и вновь возвращается на погрузку. В пункте разгрузки находится только одно
разгрузочное место, где самосвал может разгрузиться, то есть два и более само-
свалов одновременно разгружаться не могут. Для краткости будем называть та-
кое разгрузочное место разгрузчиком (название условное, так как самосвал раз-
гружается сам). Время нахождения самосвала в пути распределено нормально,
причем в загруженном состоянии он тратит на дорогу в среднем 22 мин, а в по-
9.3. Классы и объекты 185
рожнем — 18 мин. Среднеквадратичное отклонение в обоих случаях равно 3 мин.
Время разгрузки распределено равномерно на интервале от 2 до 8 мин. После
погрузки каждого самосвала погрузчик должен «отдыхать» в течение 5 мин, а за-
тем вновь может приступать к погрузке.
На рис. 9.1 представлена схема моделируемой системы. Работа системы анализи-
руется в течение 8 ч, причем все операции, начавшиеся в конце этого периода,
должны быть завершены до окончания имитационного прогона.
9.2. Модельное время
За единицу модельного времени принимаем секунду. Случайные величины, рас-
пределенные равномерно, генерируем сразу в секундах. Случайные величины,
распределенные по закону Эрланга, экспоненциально и нормально, генерируем в
минутах, чтобы избежать потери точности при численном решении уравнений и
интегрировании, затем результат умножаем на 60 и округляем до ближайшего
целого числа. Дальнейшее масштабирование времени в этой задаче использовать
не будем.
9.3. Классы и объекты
Система, которую мы собрались моделировать, обладает особенностью, с кото-
рой мы еще не сталкивались, — она является замкнутой. Это означает, что какие-
либо внешние входные и выходные потоки отсутствуют, количество объектов в
системе постоянно и со временем не меняется. Поэтому в замкнутой системе от-
сутствует и понятие времени пребывания заявки в ней. В замкнутых системах
всегда существует установившийся режим. Аналитическое моделирование замк-
нутых систем гораздо более трудоемко по сравнению с моделированием откры-
тых, поэтому для их изучения имитационные модели особенно полезны. При от-
сутствии внешнего входного потока число заявок во всех очередях имеет
фиксированную верхнюю границу, поэтому в данной задаче нет необходимости
для моделирования очередей использовать списки. Очереди будем представлять
с помощью массивов. Другой особенностью является неоднородность каналов
в узле — погрузчики имеют разные показатели производительности.
Введем два класса — Пункт погрузки (Fuller) и Пункт разгрузки (Emptier). Хотя в ус-
ловии задачи количество погрузчиков и разгрузчиков фиксировано (соответст-
венно, 2 и 1), сделаем эти значения полями данных классов, чтобы повысить
общность моделирующей программы и облегчить эксперименты с ней в дальней-
шем. В связи с обобщением задачи на случай произвольного числа k погрузчи-
ков необходимо добавить пояснение. Погрузка начинается только тогда, когда
бульдозер нагребет k куч — по одной перед каждым погрузчиком. Соответствен-
но, время работы бульдозера при наличии k погрузчиков имеет распределение
Эрланга порядка k.
Рассмотрим вопрос — нужен ли отдельный класс для бульдозера. В процессе мо-
делирования нас интересует только одна единица информации о бульдозере —
186 Глава 9. Замкнутая система с неоднородными каналами
время, оставшееся до завершения создания фронта работ для погрузчиков. Но
эту информационную единицу вполне можно сделать полем данных класса Пункт
погрузки, так как бульдозер постоянно находится под контролем этого объекта,
а понятие времени пребывания в системе для бульдозера лишено смысла, пото-
му что он является сервером, а не заявкой. Поэтому отдельный класс для буль-
дозера вводить не будем.
Теперь разберемся, нужно ли выделять в отдельный класс самосвалы. В первую
очередь отметим, что в условии задачи не сказано, какую именно статистику
о работе системы нужно собрать. Это оставлено на усмотрение исследователя.
Главным показателем, несомненно, является производительность, выражаемая
в количестве земли, перевезенной в пункт разгрузки. Единицей ее измерения
примем 1 самосвал. Каждый из самосвалов в пунктах погрузки и разгрузки вы-
ступает в качестве заявки на обслуживание. Так как система замкнутая, время
пребывания заявки (самосвала) отслеживать не нужно, но, тем не менее, отдель-
ный класс для самосвалов ввести следует. Дело в том, что не на всем протяже-
нии производственного цикла самосвал находится «под патронажем» других
объектов — во время курсирования между пунктами погрузки и разгрузки он
выступает как самостоятельная обособленная единица, для которой нужно от-
слеживать время, оставшееся до прибытия в один из пунктов. И то и другое вре-
мя задано собственными законами распределения. Таким образом, вводим класс
Самосвал (HeavyCar), представленный в системе четырьмя объектами.
Проектирование класса HeavyCar затруднений не вызывает. Его полями данных
являются времена до завершения пути туда и обратно и состояние, в котором он
в данный момент находится. Примем для состояний следующую нумерацию:
О состояние 1 — в очереди к погрузчику;
О состояние 2 — непосредственно на погрузке;
О состояние 3 — загруженный самосвал движется к пункту разгрузки;
О состояние 4 — в очереди к разгрузчику;
О состояние 5 — непосредственно на разгрузке;
О состояние 6 — порожний самосвал движется к пункту погрузки.
Выходы самосвала из состояний 1, 2, 4, 5 будут моделироваться пунктами по-
грузки и разгрузки, из состояний 3 и 6 — самим самосвалом. Заметим, что номер
состояния является логически избыточным полем данных, так как его значение
можно вычислить из информации, поставляемой полями данных как самого са-
мосвала, так и других объектов. Все же включим состояние самосвала в число
полей данных для упрощения моделирования системы.
Самосвалу также нужна связь с пунктами погрузки и разгрузки для передачи им
сообщений о своем прибытии на них. Это достигается уже опробованным спосо-
бом — приведением «пустых» указателей.
Для класса Fuller динамическую информацию о погрузчиках будем хранить
в массивах. Эта информация включает указатель на загружаемый самосвал и
время, оставшееся до окончания его загрузки, время, оставшееся до окончания
«отдыха» погрузчика, наличие или отсутствие подготовленной бульдозером
кучи земли перед погрузчиком. Размеры этих массивов равны количеству по-
9.3. Классы и объекты 187
грузчиков. Необходим также массив, содержащий указатели на самосвалы, нахо-
дящиеся в очереди на погрузку, размер которого равен количеству самосвалов
в системе.
Аналоги этих массивов требуются и в качестве полей данных класса Emptier, за
исключением времени, оставшегося до окончания «отдыха» (так как отдыхать
некому — самосвал разгружается сам), и признаков готовности кучи земли (для
разгрузки это не требуется).
Приведем полные списки полей данных классов.
9.3.1. Класс HeavyCar
Неизменяемые поля:
О среднее время пути груженого самосвала (22);
О среднеквадратичное отклонение времени в пути груженого самосвала (3);
О среднее время в пути порожнего самосвала(18);
О среднеквадратичное отклонение времени в пути порожнего самосвала(З);
О порядковый номер (уникальный идентификатор);
%
О указатель на Пункт погрузки;
О указатель на Пункт разгрузки.
Изменяемые поля:
О состояние самосвала (от 1 до 6);
О время, оставшееся до прибытия на разгрузку;
О время, оставшееся до прибытия на погрузку.
9.3.2. Класс Fuller
Неизменяемые поля:
О количество погрузчиков (2);
О производительность бульдозера (по условию — 0,25 кучи в минуту);
О продолжительность отдыха после погрузки (300);
О массив производительностей погрузчиков (0,0714, 0,0833).
Изменяемые поля:
О время до окончания подготовки бульдозером фронта работ — двух куч;
О массив значений времени, оставшегося до окончания погрузки каждым по-
грузчиком;
О массив указателей на объекты класса HeavyCar, обслуживаемые в данный мо-
мент погрузчиками;
О массив указателей на объекты класса HeavyCar, находящиеся в очереди на по-
грузку;
О массив значений времени, оставшегося до окончания «отдыха» погрузчиков;
О массив признаков того, готова ли для погрузчика куча земли.
188 Глава 9. Замкнутая система с неоднородными каналами
9.3.3. Класс Emptier
Неизменяемые поля:
О количество разгрузчиков (1);
О минимальное время разгрузки (120);
О максимальное время разгрузки (480).
Изменяемые поля:
О массив значений времени, оставшегося до окончания разгрузки каждым раз-
грузчиком;
О массив указателей на объекты класса HeavyCar, обслуживаемые в данный мо-
мент разгрузчиками;
О массив указателей на объекты класса HeavyCar, находящиеся в очереди на раз-
грузку.
Количество самосвалов зададим как глобальную переменную, описанную в заго-
ловочном файле.
9.4. События и методы
Класс HeavyCar не имеет моделирующих методов, за исключением метода run(),
так как все события, происходящие с самосвалом, моделируются методами дру-
гих классов. С пунктом погрузки могут происходить следующие события:
О окончание работы бульдозера;
О прибытие в пункт очередного самосвала;
О окончание загрузки самосвала одним из погрузчиков;
О окончание «отдыха» одного из погрузчиков.
Для каждого из этих событий нужно написать метод-обработчик. По смыслу вы-
полняемых действий первый метод параметров не имеет, второй получает указа-
тель на объект класса HeavyCar, третий и четвертый методы получают целочис-
ленный порядковый номер погрузчика, завершившего погрузку или отдых.
Возможные события для пункта разгрузки — прибытие очередного самосвала
и окончание разгрузки самосвала одним из разгрузчиков.
Помимо моделирующих методов, все классы имеют еще некоторое количество
служебных методов, извлекающих ту или иную текущую информацию об объек-
те. Все эти методы довольно просты, подробно прокомментированы в листин-
гах 9.1, 9.2, поэтому здесь мы их перечислять не будем.
В этой задаче возникают также вопросы выбора дисциплины обслуживания,
связанные с неоднородностью каналов обслуживания, конкретнее — с различной
производительностью погрузчиков. Если в момент прибытия самосвала к пункту
погрузки имеется несколько свободных погрузчиков, самосвалу может быть на-
значен либо первый по порядку из них, либо имеющий наибольшую произво-
дительность. В программе реализован второй вариант как более гибкий. Пункт
9.4. События и методы 189
разгрузки моделируется как узел с однородными каналами, поэтому выбор сво-
бодного разгрузчика не имеет значения.
Условие завершения всех начатых работ, чтобы не усложнять программу, учтем
только при вычислении основной характеристики — количества разгруженных
самосвалов. Это сделано следующим образом. В момент завершения рабочего
дня, то есть истечения восьмичасового интервала времени, проверяется состоя-
ние каждого самосвала. Если самосвал находится в очереди на погрузку или дви-
жется порожняком, он оставляется без внимания. При любом другом состоянии
он должен «дойти» до разгрузки, поэтому к числу разгруженных самосвалов
прибавляется единица.
Листинг 9.1. Файл Classes').h, содержащий реализацию классов
#include<cstdio>
#include<ctime>
using namespace std; #include "erlang.h" //генераторы распределений Эрланга //и экспоненциального
#include "normal.h“ FILE *qul: ' //генератор нормального распределения //файл для сбора статистики о длине очереди //на погрузку
FILE *qu2; //файл для сбора статистики о длине очереди //на разгрузку
Int 5=4; //количество самосвалов
int completed=0: //счетчик разгруженных самосвалов
float quel_ave=0: //счетчик средней длины очереди на погрузку
float que2_ave=0: //счетчик средней длины очереди на разгрузку
long int total: //счетчик общего времени моделирования
float ro_fuller=0: //счетчик средней загрузки пункта логрузки
long int ro_buld-0L: float ro_emptier=0: //счетчик средней загрузки бульдозера //счетчик средней загрузки пункта разгрузки
long int path_full=OL: //счетчик времени, проведенного самосвалами в пути //на разгрузку
long int path_empty=OL: //счетчик времени, проведенного самосвалами //в пути на погрузку
long int take=0L: long int give=0L: //счетчик времени, проведенного в пункте погрузки //счетчик времени, проведенного в пункте разгрузки
class HeavyCar
{
const static float direct_ave=22.0;
const static float direct_disp=3.0:
const static float back_ave-18.0:
const static float back_disp=3.0:
int id: //порядковый номер самосвала
enum states {Que_Ful 1 er-1.Ful1er.Ful l_Move,Que_Empti er. Empti er. Empty_Move)
state: //текущее состояние самосвала
int to_pfull; //время до прибытия в пункт разгрузки
int to_pempty: //время до прибытия в пункт погрузки
void *f: void *e: public: //указатель на пункт погрузки //указатель на пункт разгрузки
//Пункты погрузки и разгрузки будут манипулировать самосвалами
friend class Fuller:
190 Глава 9. Замкнутая система с неоднородными каналами
friend class Emptier:
HeavyCar(int i): //метод-конструктор
void putFuller(Fuller *fl);
void putEmptier(Emptier *el):
void run(); //метод-диспетчер
void PrintO:
int StateO { return (state): }
}:
class Fuller
{
const static int volume=2;
const static float mu=0.25:
const static int rest-300:
float *perf:
int to_buldoser:
int *to_full;
HeavyCar **serving:
HeavyCar **queue:
int *to_rest:
int *isHeap:
public:
FullerCHeavyCar **h):
-FullerO:
void GroundReadyO;
void CompleteCint i);
void RestCompleteOnt 1):
void Arrival(HeavyCar *h):
//массив производительностей погрузчиков
//время до завершения работы бульдозера
//массив времен до завершения погрузки.
//-1 - в случае простоя
//массив указателей на загружаемые самосвалы
//массив указателей на самосвалы, ждущие
//в очереди
//массив времен до завершения отдыха,
//-1 в случае, если погрузчик находится
//не в состоянии отдыха
//массив признаков того, готова ли куча земли
//для погрузчика
//метод-конструктор
//метод-деструктор
//обработчик события: бульдозер подготовил кучи
//обработчик события: i-й погрузчик завершил
//погрузку
//обработчик события: i-й погрузчик «отдохнул»
//обработчик события: прибыл самосвал h
int BestAvailO:
void PrintO:
int qLengthO;
int StateOnt i):
int BusyO;
void run():
//выбор наилучшего свободного погрузчика
//вывод на печать содержимого объекта
//вычисление текущей длины очереди
//вычисление состояния i-ro погрузчика
//вычисление количества занятых погрузчиков
//метод-диспетчер
}:
class Emptier
{
const static int volume=l:
const static int borderl=120;
const static int border2=480:
int *to_empty: //массив времен до завершения разгрузки.
//-1 в случае простоя
HeavyCar **serving: //массив указателей на разгружаемые самосвалы
HeavyCar **queue: //массив указателей на самосвалы, ждущие
//в очереди
public:
EmptierO; //метод-конструктор
-EmptierO: //метод-деструктор
void Completednt i): //обработчик события: i-й самосвал завершил разгрузку
void Arrival(HeavyCar *h): //обработчик события: прибыл самосвал h
Листинг 9.1. Файл Classes9.li, содержащий реализацию классов 191
void PrintO: //печать содержимого объекта
int qLengthO:
int BusyO:
int FirstAvailO: //выбор первого no порядку свободного
//разгрузчика
void run(); //метод-диспетчер
};
HeavyCar::HeavyCar(int i)
{
id“i:
state“Que_Fuller: //первоначально все самосвалы стоят в очереди
//на погрузку
to_pfull--l:
to_pempty— 1:
}
void HeavyCar::putFuller(Fuller *fl)
{
f-fl:
}
void HeavyCar::putEmptier(Emptier *el)
{
e=el:
}
void HeavyCar::PrintO
{
switch(state)
{
case Que_Fuller: printf("Самосвал 2d находится в очереди на погрузкуХп". id): break:
case Fuller: printf("Самосвал 2d загружаетсяХп". id): break;
case Full_Move: printf("Самосвал 2d движется с грузом. Прибудет на разгрузку через
2.If минутХп". id. ((float)to_pfull)/60): break:
case Que_Emptier: printf('Самосвал 2d находится в очереди на разгрузкуХп”. id); break:
case Emptier: printf("Самосвал 2d разгружаетсяХп”. id): break:
case Empty_Move: printf("Самосвал 2d движется порожний. Прибудет на погрузку через
2.If минутХп”. id. ((float)to_pempty)/60): break:
}
}
void HeavyCar::runО
{
1f (state—3) { to_pfull--: //груженый самосвал находится в пути
if (to_pfull—0) { to_pfull--l: //самосвал прибыл в пункт разгрузки
((Emptier*)e)->Arrival(this): } //сообщаем об этом в пункт разгрузки
) else 1f (state—Empty Move) { to_pempty--: //порожний самосвал находится в пути
if (to_pempty—0) //самосвал прибыл в пункт погрузки
{
to_pempty--l:
192 Глава 9. Замкнутая система с неоднородными каналами
((Fuller*)f)->Arrival(this): //сообщаем об этом пункту погрузки
}
}
else: //обработка других состояний самосвала -
//в других классах
//Инкремент счетчика общего времени пребывания самосвалов в различных //состояниях
switch(state)
{
case Que_Fuller: take++: break: //очередь на погрузку
case Fuller: take++: break: //погрузка
case Full_Move: path_full++: break: //путь к пункту разгрузки
case Que_Emptier: give++: break: //очередь на разгрузку
case Emptier: glve++: break: //разгрузка
case Empty_Move: path_empty++: break: //путь к пункту погрузки
}
return:
)
Fuller::Fuller(HeavyCar **h)
//В начальном состоянии все погрузчики простаивают, все самосвалы ждут
//в очереди, бульдозеры приступают к работе
{
Int 1:
to_bul doser= (1 nt) (get_ei* 1 ang (mu. vol ume. 0.001) *60):
to_full=new intEvolume]:
to_rest=new IntEvolume]:
perf=new floatEvolume];
1sHeap= new IntEvolume]:
serving=new HeavyCar *Evolume]:
queue=new HeavyCar *ES]:
perfE0]=0.0714: //производительность первого погрузчика - 1/14 самосвала
//в минуту
perfEl]=0.0833: //производительность второго - 1/12 самосвала в минуту
for(1=0:1<volume;i++)
{
to_fullEi]“-l:
to_restEi]“-l:
1sHeapEi]=0:
servingE1]-NULL:
}
for(i-0:i<S:i++)
queueEi]“h[1]:
}
Fuller::-Fuller()
{
deleteE] to_full;
deleteE] perf;
deleteE] isHeap:
deleteE] to_rest:
delete E] serving:
delete [] queue:
}
void Fuller::Print()
{
int i:
Листинг 9.1. Файл Classes!).h, содержащий реализацию классов 193
if lto_buldoser>0) printfl"Бульдозер работает. Подготовит фронт работ через И
минутХп". to_buldoser/60):
else printfl"Бульдозер не работаетХп”):
printfC'B очереди на погрузку - Zd самосваловХп”. qLengthl));
for(i-0:i<S:i++)
{
if l!queue[i]) break:
printfl’'^d-й в очереди - самосвал Ns й\п". i+1. queue[i]->id):
)
forii-0:i <volume:i ++)
{
switchlStateli))
{
case 1:
printfC'^d-й погрузчик работает. Он обслуживает самосвал Ns fcd. До окончания погрузки
И минутХп".i+1.serving[i]->id.to_ful1[i]/60);
case 2:
printff^d-й погрузчик отдыхает. До окончания отдыха W минутХп".i+1.to_rest[i]/60):
case 3:
printf("fcd-ft погрузчик простаивает.Xn".i+1):
}
)
return:
}
int Fuller::State(int i)
//Вычисление состояния i-го погрузчика
{
if (serving[i]!=NULL) return(l): //работает
if (to_rest[1]>0) return(2): //отдыхает после погрузки
return(3): //свободен
}
int Fuller::qLengthl)
{
int i:
for(i=0:i<S:i++)
if (queue[i]==NULL) returnli):
return(S): //все места в очереди заняты
)
int Fuller::BestAvai1()
{
float max-0:
int k=-l:
forlint i-0:i<volume:i++)
{
//Нас интересуют только погрузчики, которые свободны и имеют
//подготовленную кучу земли. Из них выбираем наилучший
if ((Stateli)==3)&&lperf[i]>max)&&(isHeap[i]==l))
{
max=perf[i]:
k-i;
}
}
return(k);
)
int Fuller::Busy()
194 Глава 9. Замкнутая система с неоднородными каналами
{
int k-0:
fordnt i-0:i<volume:i++)
if (Stated)“1) k++:
return(k):
}
void Fuller::GroundReady()
{
int i. k, mi. j;
//Возле каждого погрузчика появилась куча земли
for(i-0;i<volume:1++)
isHeap[i]=l:
to_buldoser--1: //бульдозер прекращает работу
//На обслуживание поступает количество самосвалов, равное значению минимума
//длины очереди и числа погрузчиков
if (qLength()<volume) mi-qLengthO:
else mi-volume:
for(j-0;j<mi:j++)
{
k-BestAvail(): //выбираем наилучший из доступных
//погрузчиков
if (k—-l) return:
to_full[k]-(int)(get_exp(perf[k])*60): //разыгрываем длительность погрузки
serving[k]-queue[O]: //ставим на погрузку первый
//самосвал из очереди
queue[O]->state-Fuller: //переводим этот самосвал
//в состояние погрузки
for(i-0:i<(S-l):i++) //продвижение очереди
queue[1]=queue[i+l]:
queue[S-l]-NULL:
}
}
void Fuller::Arrival(HeavyCar *h) //в пункт погрузки прибыл пустой
//самосвал
{
int к. р:
к-BestAvailO: //выбираем наилучший из доступных
//погрузчиков
if (к---1) //такового нет. ставим самосвал
//в очередь
{
p-qLength():
queue[p]-h:
queueEp]->state-Oue_Ful1 er:
}
else //ставим самосвал на обслуживание
{
to_ful1[к]-(1nt)(get_exp(perf[к])*60):
serving[k]-h:
servi ng[к]->state-Ful1er:
}
}
void Fuller::Complete(int i) //i-й погрузчик завершил погрузку
{
Int j;
Листинг 9.1. Файл Classes9.h, содержащий реализацию классов 195
to_rest[i]“rest: //1-й погрузчик приступил к отдыху
isheap[i]-0:
to_full[1]--l: //i-й погрузчик временно не занимается
//погрузкой
serving[i]->state-Full_Move: //изменяем состояние загруженного самосвала
//Разыгрываем для загруженного самосвала время в пути до пункта разгрузки
serving[i]->to_pfull-(int)(get_normal(serving[i]->direct_ave. serving[i] ->
direct_disp.0.001)*60):
serving[i]-NULL; //на 1-м погрузчике самосвала нет
//Проверяем, все ли погрузчики освободились
for(j-0;j<volume;j++)
if (1sHeap[j]“l) break:
if (j—volume) //все освободились, запускаем бульдозер
to_buldoser-(1nt)(get_erlang(mu.volume.0.001)*60):
}
void Fuller::RestComplete(int 1)
{
int k;
to_rest[i]--l: //отдых завершен
if (qLength()=-0) return: //самосвалов нет. делать погрузчику нечего
if (isHeap[iJ—0) return: //куча земли не готова, делать погрузчику
//нечего
to_full[i]-(1nt)(get_exp(perf[i])*60): //работа есть, разыгрываем время погрузки
serving[i]-queue[0]: //ставим на обслуживание первый самосвал
//из очереди
queue[O]->state-Fuller: //изменяем состояние этого самосвала
for(k-0:k<(S-l);k++) //продвигаем очередь
queue[k]-queue[k+l]:
queue[S-l]-NULL;
}
void Fuller::run()
{
If (to_buldoser>0) to_buldoser--:
if (to_buldoser--0) GroundReady(): //бульдозер завершил работу
fordnt i-0:i<volume:1++)
{
if (to_full[1]>0) to_full[i]--;
if (to_full[i]—0) Completed): //1-й погрузчик завершил работу
If (to_rest[i]>0) to_rest[1]--:
if (to_rest[1]--0) RestCompleted): //1-й погрузчик завершил отдых
}
fprintf(qul."Sd\n". qLengthO): //запись текущей длины очереди
//к погрузчику в файл и пересчет
//средней длины очереди
//и коэффициента загрузки
//погрузчика
quel_ave=quel_ave*(l-1.0/(total+l))+((float)qLength())/(total+l):
ro_ful1 er-ro_f ul1er*(1-1.0/(total+1))+(((float)Busy())/vol ume)/(total +1):
if (to_buldoser>0) ro_buld++: //инкремент счетчика загрузки
//бульдозера
}
> Emptier::Emptier()
//В начальный момент времени пункт разгрузки пуст
{
int 1:
196 Глава 9. Замкнутая система с неоднородными каналами
to_empty=new 1nt[volume];
serving=new HeavyCar *[volume];
queue=new HeavyCar *[S]:
for(1=0:1<volume;1++)
{
to_empty[i]=-l;
serving[i]=NULL;
}
for(i-0:i<S:i++)
queue[i]=NULL:
}
Emptier::~Emptier()
{
delete[] to_empty;
delete [] serving:
delete [] queue:
)
void Emptier::PrintO
{
int 1:
printfC'B очереди на разгрузку - M самосваловХп”. qLengthO):
for(i«0;i<S:i++)
{
if (queue[i]==NULL) break;
printfC'Sd-й в очереди - самосвал Ns SdXn". i+1. queue[i]->id):
}
for(i =0;i <volume;i ++)
{
if (to_empty[i]>0)
printfCSd-й разгрузчик работает. Он обслуживает самосвал Ns fcd. До окончания погрузки
Xd минутХп”.i+1.serving[i]->id.to_empty[i]/60);
else printfC'fcd-fl разгрузчик простаивает.Xn".i+1);
)
return;
}
int Emptier::qLengthO
{
int i:
for(i-0:i<S:i++)
if (queue[i]==NULL) return(i);
return(S):
}
int Emptier::BusyО
{
int k=0;
forfint i=0:i<volume:i++)
if (to_empty[i]>0) k++:
return(k):
}
int Emptier::FirstAvail()
{
forOnt i=0:i<volume:i++)
if (to_empty[i]==-l) return(i);
return(-l):
)
Листинг 9.1. Файл Classes!), h, содержащий реализацию классов 197
void Emptier::Arrival(HeavyCar *h) //прибытие самосвала
{
int k. p:
k=FirstAvai1(): //какой разгрузчик свободен?
if (k==-l) //свободного разгрузчика нет
{
p=ql_ength():
queue[p]=h:
queue[p]->state=Que_Emptier:
}
else //свободный разгрузчик есть
{
to_empty[k]=borderl+rand()B(border2-borderl+l):
• serving[k]=h;
serving[k]->state=Emptier:
}
}
void Emptier::Complete(int i) //i-й разгрузчик завершил разгрузку
{
int j:
serving[i]->state=Empty_Move: //меняем состояние разгруженного самосвала
//Вычисляем для него время в пути до пункта погрузки
serving[i]->to_pempty=(int)(get_normal(serving[i]->back_ave. serving[i]-
>back_disp.0.001)*60);
if (qLength()==0) //очередь пуста
{
to_empty[i]--l:
serving[i]=NULL:
)
else //очередь не пуста, ставим первый
//самосвал на разгрузку, меняем его
//состояние и продвигаем очередь
{
to_empty[i]=borderl+rand()Ж(border2-borderl+l);
serving[i]=queue[O]:
serving[i]->state=Emptier;
for(j=0:j<(S-l):j++)
queue[j]=queue[j+l]:
queue[S-l]=NULL:
}
)
void Emptier::run()
{
for(int i=0:i<volume;i++)
{
if (to_empty[i]>0) to_empty[i]--:
//Разгрузка завершена. Вызываем метод-обработчик и делаем инкремент
//счетчика разгруженных самосвалов
if (to_empty[i]=-0) {
Completed);
completed++:
)
}
//Запись текущей длины очереди в файл, пересчет средней длины очереди
//и коэффициента загрузки разгрузчика
198 Глава 9. Замкнутая система с неоднородными каналами
fprintf(qu2.'ld\n". qLengthO):
que2_ave-que2_ave*(1-1.0/(total+1))+((f1 oat)qLength())/(total+1):
ro_emptier-ro_emptier*(1-1.0/(total+l))+(((float)Busy())/volume)/(total+1):
)
Листинг 9.2. Функция main()
#def1ne N 28800 //общая длительность моделирования
#1nclude "Classes9.h”
Int mainO
{
int i:
HeavyCar **masCar: //массив указателей на самосвалы и выделение
//памяти для него
masCar-new HeavyCar *[S]:
srand((unsigned)time(O)): //инициализация ГСЧ
//Открытие файлов для сбора статистики по длинам очередей
qul“fopen("quel". "wt"):
qu2-fopen("que2". "wt"):
//Инициализация самосвалов
for(1-0;1<S;1++)
masCar[1]-new HeavyCar(1+l):
//Инициализация пунктов погрузки и загрузки
Fuller fu(masCar);
Emptier em:
//Установление связи самосвалов с пунктами
for(1-0:1<S;1++)
{
masCar[1]->putFul1 er(&fu):
masCar[1]->putEmptier(&em):
}
//Основной моделирующий цикл
for(total-0L;tota1<N;tota1++)
{
for(i-0;1<S;1++)
masCar[1]->run():
fu.runO;
em.runO:
}
//Учет остаточного состояния самосвалов
for(1-0:1<S;1++)
1f ((masCar[i]->State()!-0ue_Ful1er)&&(masCar[1]->State()!-Empty_Move)) completed++:
//Очистка памяти, выделенной под объекты
for(i“0:i<S:1++)
delete masCar[1]:
delete [] masCar;
fclose(qul):
fclose(qu2):
//Вывод результатов моделирования
printf("Раз гружено самосвалов Sd\n". completed);
printf("Средняя длина очереди на погрузку r3f\n". quel_ave):
printf("Средняя длина очереди на разгрузку ^.3f\n”. que2_ave):
printf("Коэффициент загрузки пункта погрузки r3f\n". ro_fuller):
printf("Коэффициент загрузки пункта разгрузки S.3f\n". ro_emptier);
printff"Коэффициент загрузки бульдозера 2.3f\n". ((float)ro_buld)/total);
9.4. События и методы 199
printfC"Самосвалы: доля времени, проведенного в пути порожняком S.3f\n",
((f1 oat)path_empty)/(S*total)):
printfC"Доля времени, проведенного в пути гружеными J.3f\n“,
((fl oat)path_ful1)/(S*total)):
printfC'flonR времени, проведенного в пункте погрузки ^.3f\n". C(float)take)/(S*total));
printfC"Доля времени, проведенного в пункте разгрузки ГЗЙп".
C(float)give)/(S*total));
}
Система оставляет широкий простор для исследования и экспериментирования.
Рассмотрим подробнее взаимодействие бульдозера и пункта погрузки. Пока буль-
дозер не подготовит кучи для всех погрузчиков, ни один из них не приступает к
работе. В свою очередь, пока не «отработают» свои кучи все без исключения по-
грузчики, бульдозер не возобновит работу. Назовем такой режим работы опто-
вым. Нетрудно видеть, что он приводит к нерациональному простою бульдозера
и погрузчиков и может быть оправдан только в том случае, если по каким-либо
причинам технологического характера совместная работа бульдозера и погруз-
чиков невозможна. Если же такого ограничения нет, можно попробовать другой
режим. Если некоторый погрузчик завершил погрузку (то есть около него нет
кучи) и бульдозер в этот момент свободен, то бульдозер немедленно начинает
нагребать кучу около этого погрузчика. Такой режим работы назовем розничным.
Покажем, какие изменения следует внести в протокол класса Fuller, чтобы отра-
зить переход бульдозера с оптового на розничный режим.
Во-первых, нам потребуется еще одно поле данных — номер погрузчика, обслу-
живаемого бульдозером в данный момент. Во-вторых, изменится распределение
длительности работы бульдозера. Если k погрузчиков он обслуживал оптом за
эрланговское время k-ro порядка с интенсивностью р, то время обслуживания
одного погрузчика, разумеется, будет распределено экспоненциально с той же
интенсивностью. В-третьих, если бульдозер завершил работу, а к обслуживанию
(нагребанию кучи, а не загрузке самосвала!) готовы несколько погрузчиков,
вновь возникает проблема выбора, которой не было при оптовом режиме. Решим
ее по аналогии — выберем наиболее производительный из доступных погруз-
чиков. Для этого напишем еще один метод под названием BestHeapO. Метод
BestAvailО для этой цели не подходит, так как в нем накладывается условие, что
погрузчик обязательно должен быть свободен, а в методе BestHeap() он может на-
ходиться еще и в состоянии отдыха после погрузки. Заметим также, что метод
BestArrivaK) помогает выбрать погрузчик самосвалу, a BestHeapO — бульдозеру.
Далее приведен листинг 9.3 модифицированных методов класса Fuller. Строки,
относящиеся к реализации розничного режима, выделены. Полной переработке
подвергся метод GroundReadyO.
Листинг 9.3. Модифицированные методы класса Fuller
Fuller::Fuller(HeavyCar **h)
{
int i;
to_buldoser-(int)(get_exp(mu)*60):
to_full=new intlvolume]:
to_rest=new i ntlvol ume]:
200 Глава 9. Замкнутая система с неоднородными каналами
perf=new fl oat[volume]:
isHeap=new intEvolume]:
serving=new HeavyCar *[volume]:
queue-new HeavyCar *[S]:
perf[0]=0.0714:
perf[l]=0.0833:
for(i=0;i<vOlume;i++)
{
to_full[l]=-l;
to_rest[i]=-l:
1sHeap[i]=0:
serving[1]=NULL:
}
heaplng=BestHeap():
for(i=0:i<S:i++)
queue[i]=h[i]:
}
void Fuller::GroundReady()
{
int i. k.j.mi:
isHeap[heaping]=l: //куча для i-го погрузчика готова
j=heaping:
mi=BestHeap(): //бульдозер выбирает новый погрузчик
if (mi=-l) tobuldoser--1: //если не нашел - отдыхает
else //переход к обслуживанию другого погрузчика
{
to_buldoser-(1nt)(get_exp(mu)*60):
heaping=mi:
}
if ((qLength()=0)||(to_rest[j]!=-D) return:
//Если обслуженный погрузчик не отдыхает и очередь не пуста, ставим
//на обслуживание первый самосвал из очереди
toful 1 [ j]=( int) (get_exp(perf[ j]) *60):
serving[j]=queue[O]:
queueEO]->state-Ful1er:
for(i=0;i<(S-1):i++)
queueE1]=queueEi+l]:
queueES-l]=NULL;
}
void Fuller::Complete(int 1)
{
int j:
to_restEi]=rest:
isHeapEi]=O:
to_fullEi]=-l:
servingEi]->state=Full_Move:
servingEi]->to_pfull=(int)(get_normal(servingEi]->direct_ave. serving[i]-
>di rect_di sp.0.001)*60):
servingEi]=NULL:
//Если бульдозер не занят, ставим ему на обслуживание освободившийся погрузчик
if (to_buldoser=-l) {
tobul doser=( i nt) (getexp (mu) *60):
heaping=i;
}
}
9.5. Анализ результатов 201
int Fuller::BestHeapO //выбор бульдозером погрузчика
{
int i:
float max=0:
int k--l:
for(i=0;i<volume:i++)
{
//Погрузчик должен не иметь кучи. Переключение этого признака на О
//происходит в момент завершения погрузки самосвала
if ((perf[i]>max)&&(isHeap[i ]==()))
{
max=perf[i]:
k-i:
}
}
return(k):
}
9.5. Анализ результатов
Прогон программы для поставленной задачи принес следующие результаты:
О количество разгруженных самосвалов — 29;
О средняя длина очереди в пункте погрузки — 0,69;
О средняя длина очереди в пункте разгрузки — 0,023;
О коэффициент загрузки пункта погрузки — 0,38;
О коэффициент загрузки пункта разгрузки — 0,27;
О коэффициент загрузки бульдозера — 0,25;
О доля времени, проведенного самосвалами в пути порожняком — 0,25;
О доля времени, проведенного самосвалами в пути гружеными — 0,32;
О доля времени, проведенного самосвалами в пункте погрузки — 0,36;
О доля времени, проведенного самосвалами в пункте разгрузки — 0,07.
Предположим, что размеры пункта и технология погрузки не позволяют устано-
вить дополнительные бульдозеры и погрузчики, отсутствуют также более произ-
водительные модели этих устройств. Исследуем, каким образом скажется на
производительности системы увеличение количества самосвалов. На рис. 9.2-9.7
приведены зависимости характеристик системы от числа самосвалов для оптово-
го (сплошная линия) и розничного (пунктирная линия) режимов работы буль-
дозера. Сразу же отметим, что розничный режим лучше по всем показателям.
Обратимся к рис. 9.2. Из него видно, что при оптовом режиме увеличение числа
самосвалов до 8 позволит достичь производительности в 35-36 разгруженных
самосвалов. Дальнейшее увеличение количества самосвалов не имеет смысла,
так как остальные объекты системы просто не будут успевать их обслуживать.
При розничном режиме наличие 9 самосвалов приводит к существенно большей
производительности — 49-50 разгруженных самосвалов, но и здесь дальнейший
экстенсивный рост бесполезен.
202 Глава 9. Замкнутая система с неоднородными каналами
Количество самосвалов
Рис. 9.2. Зависимость производительности от количества самосвалов
Количество самосвалов
Рис. 9.3. Зависимость длины очереди на погрузку от количества самосвалов
9.5. Анализ результатов 203
Количество самосвалов
Рис. 9.4. Зависимость длины очереди на разгрузку от количества самосвалов
Количество самосвалов
Рис. 9.5. Зависимость загрузки пункта погрузки от количества самосвалов
204 Глава 9. Замкнутая система с неоднородными каналами
Количество самосвалов
Рис. 9.6. Зависимость загрузки пункта разгрузки от количества самосвалов
Количество самосвалов
Рис. 9.7. Зависимость загрузки бульдозера от количества самосвалов
Задания для самостоятельной работы 205
Из всех остальных рисунков тоже хорошо видно, что показатели системы утра-
чивают тенденцию к росту при количестве самосвалов, равном 8-9. Видимо, это
и является оптимальным количеством самосвалов для данной системы при усло-
вии, что габариты пункта погрузки или прилегающей к нему территории доста-
точны для стоянки как минимум 5 самосвалов при оптовом режиме и 3 самосва-
лов — при розничном. Лишь средняя длина очереди к пункту погрузки линейно
возрастает при увеличении числа самосвалов без тенденции к установлению ста-
ционарного режима, что свидетельствует о том, что пункт погрузки является
наиболее узким местом системы.
Выводы
1. Количество заявок в замкнутой системе постоянно, поэтому все очереди явля-
ются ограниченными и представляются массивами. Все объекты, участвующие
в работе системы, создаются до начала моделирования. Во время моделирова-
ния объекты не появляются и не исчезают. Время их пребывания в системе
отслеживать не нужно.
2. Если объект проходит достаточно длительный производственный цикл, по-
лезно выделить для него множество состояний, построить диаграмму перехо-
дов между ними (в данной задаче — это просто последовательная цепочка)
и каждому переходу сопоставить метод-обработчик события, инициировав-
шего этот переход. Использование состояний повышает четкость и нагляд-
ность разработки моделирующей программы.
3. В случае неоднородности каналов в многоканальном узле возникает пробле-
ма выбора заявкой одного из свободных каналов. Ее решение также может
быть получено из имитационной модели.
4. Путем изменения дисциплины обслуживания в одном или нескольких узлах
можно изыскать внутренние резервы системы и повысить ее производитель-
ность.
Задания для самостоятельной работы
1. Измените дисциплину выбора погрузчика так, чтобы выбирался не лучший
из свободных погрузчиков, а первый из свободных по порядку. Как изменят-
ся в этом случае результаты моделирования?
2. Попробуйте увеличить количество бульдозеров. Для оптового режима это
сделать просто — достаточно вызывать метод get erlangO с параметром k», где
k — количество бульдозеров, так как бульдозеры будут работать в k раз ин-
тенсивнее. Для розничного режима потребуется более существенная перера-
ботка кода, так как бульдозеры будут работать несинхронно. Как меняются
характеристики системы с ростом числа бульдозеров в обоих режимах?
3. Попробуем загружать самосвалы не полностью, а на какую-то часть 0 < а < 1.
Тогда средние времена работы бульдозера, погрузчика и разгрузчика умень-
206 Глава 9. Замкнутая система с неоднородными каналами
шатся в 1/а раз. Уменьшится, кроме того, и время в пути к пункту разгрузки,
но, видимо, уже не по линейному закону, а с более медленным убыванием
09_01.cdr от значения 22 в единице до значения 18 в нуле.
Будет ли максимальная производительность системы достигаться при пол-
ной загрузке самосвалов (а = 1) или же при каком-то другом значении а? По-
стройте зависимости производительности от а. Заметьте, что производитель-
ность в этих условиях следует вычислять как a*completed, где completed —
общее количество разгруженных самосвалов. Оптимальное значение а < 1
позволит говорить о том, что лучше обходиться более дешевыми моделями
самосвалов меньшей вместимости.
4. Обратите внимание на то, что код метода qLengthO для классов Fuller и Emptier
один и тот же, то же самое можно сказать и о методе Busy О. Попробуйте вос-
пользоваться этим и улучшить объектный дизайн программы, создав некий
абстрактный класс с методами qLengthO и Busy О, являющийся базовым для
классов Fuller и Emptier.
Глава 10
Замкнутая система
с раздельными очередями
и приоритетами:
моделирование
работы карьера
Основные вопросы, рассматриваемые
в данной главе:
□ Моделирование многоканального узла
с раздельными очередями
□ Обслуживание заявок
с относительным приоритетом
□ Разнотипные заявки: нужен ли общий
базовый класс?
□ Как реализовать двумерный массив
указателей?
□ Порядок инициализации объектов
□ Имитационная модель и задача
комбинаторной оптимизации
208 Глава 10. Замкнутая система с раздельными очередями и приоритетами
10.1. Описание системы
В карьере самосвалы доставляют руду от трех экскаваторов к измельчителю. По-
сле выгрузки руды у измельчителя самосвал всегда возвращается к одному и тому
же экскаватору, за которым он закреплен. Используются самосвалы грузоподъ-
емностью 20 и 50 т. За каждым экскаватором закреплены два 20-тонных и один
50-тонный самосвал. Очередь к измельчителю — общая. От грузоподъемности
самосвала зависят времена его погрузки, движения до измельчителя, разгрузки и
возвращения к своему экскаватору. Для 20-тонных самосвалов задаются следую-
щие параметры: время погрузки распределено экспоненциально с математиче-
ским ожиданием 5 условных единиц времени, время поездки до измельчителя
постоянно и равно 2,5, время разгрузки распределено экспоненциально с мате-
матическим ожиданием 2, время возвращения к экскаватору постоянно и равно
1,5. Для 50-тонных самосвалов соответственно имеем: время погрузки распреде-
лено экспоненциально с математическим ожиданием 10, время поездки до из-
мельчителя постоянно и равно 3, время разгрузки распределено экспоненциаль-
но с математическим ожиданием 4, время возвращения постоянно и равно 2.
Очереди к каждому экскаватору организованы по принципу «первым пришел —
первым обслужен». В очереди к измельчителю приоритет имеют большегрузные
самосвалы. Схема работы карьера приведена на рис. 10.1.
Требуется проанализировать функционирование всей системы в течение 480 еди-
ниц времени для определения загрузки экскаваторов и измельчителя и длины
очередей к ним.
10.3. Классы и объекты 209
10.2. Модельное время
В постановке задачи единица измерения времени не указана. Поэтому предполо-
жим, что все значения даны в минутах, и в качестве единицы модельного време-
ни примем секунду. Реализация этого соглашения такая же, как и в предыдущей
задаче. Постоянные величины задаем сразу в секундах, а результаты ГСЧ экспо-
ненциального распределения умножим на 60 и округлим до ближайшего целого
числа. Разумеется, в качестве масштабирующего коэффициента можно выбрать
и любое другое значение, которое экспериментатор считает подходящим.
10.3. Классы и объекты
Данная задача очень напоминает предыдущую. Совокупность экскаваторов мож-
но назвать пунктом погрузки, измельчитель — пунктом разгрузки, самосвалы
выполняют ту же самую работу, система является замкнутой, число заявок по-
стоянно. Однако существуют и отличия, привносящие в задачу новые, пока что
не встречавшиеся нам особенности. Перечислим их:
О в системе имеется многоканальный узел (экскаваторы) с раздельными очере-
дями к каналам, каждая заявка приписана к определенному каналу и не мо-
жет обслуживаться другим;
О в системе имеется узел (измельчитель), реализующий дисциплину обслужи-
вания с приоритетом;
О заявки, находящиеся в системе, являются разнотипными (20 т и 50 т), каж-
дый тип имеет собственные характеристики.
Сделав все эти наблюдения, попытаемся сохранить каркас объектной модели,
разработанной в предыдущей задаче, и овтановимся на том, что именно в ней
нужно изменить. В класс HeavyCar придется добавить информацию о типе само-
свала и номере экскаватора, к которому он приписан. Конечно, заманчиво было
бы «блеснуть» владением объектной технологией, создав абстрактный класс Са-
мосвал и выведя из него два производных класса — для 20- и 50-тонных самосва-
лов. Однако при ближайшем рассмотрении оказывается, что в данном случае
игра не стоит свеч. Набор полей для двух типов самосвалов одинаков, набор
и алгоритмы методов тоже ничем не различаются. Приоритет при выборе на
обслуживание — это логика работа измельчителя, а не самосвала. Различие за-
ключается только в количественном значении статических характеристик, но это
явно недостаточная мотивация для создания иерархической модели классов.
В классе Fuller коренным образом изменяется обработка поля данных queue. Так
как теперь пункт погрузки имеет не одну очередь, а несколько — столько, сколь-
ко экскаваторов, — для их информационной поддержки следует создать не мас-
сив, а матрицу указателей на объекты класса HeavyCar с числом строк, равным
числу очередей, и числом столбцов, равным сумме количеств 20- и 50-тонных са-
мосвалов, приписанных к одному экскаватору. Но число строк и столбцов зара-
нее может быть неизвестно, а объявить матрицу как HeavyCar *queue[][] компи-
лятор не позволит. Введение матричной нотации для указателей на объекты
210 Глава 10. Замкнутая система с раздельными очередями и приоритетами
потребует объявить поле данных queue как HeavyCar ***queue, что отнюдь не явля-
ется приятным для людей, не слишком искушенных в арифметике указателей, и,
самое главное, может сделать код зависимым от «способностей» конкретного компи-
лятора, отчего программа станет ненадежной. Поэтому надежнее оставить объявле-
ние поля queue таким же, как и раньше (то есть массивом указателей), но изменить
при этом процедуру индексации так, чтобы обращаться ку’-му по порядку самосвалу,
стоящему в очереди к i-му экскаватору, с помощью конструкции queue[i*(N20+N50)+j].
В класс Emptier добавим поле данных order для варьирования дисциплины об-
служивания. Условимся, что значение 0 обозначает отсутствие приоритетов, 1 —
приоритет 20-тонных самосвалов, 2 — приоритет 50-тонных самосвалов. Кроме
того, добавим вычисляемое поле данных S, равное количеству заявок (самосва-
лов) в системе и, естественно, определяющее максимальную длину очереди к из-
мельчителю. Для его вычисления нужно использовать информацию о количест-
ве экскаваторов и самосвалов у пункта погрузки, следовательно, объект Fuller
должен быть создан раньше, а указатель на него передан конструктору класса
Emptier в качестве параметра.
10.4. События и методы
Перечень событий и методов остается таким же, как в предыдущей задаче, меня-
ется только их алгоритмическое наполнение. В класс Emptier добавляется слу-
жебный метод choiceO, реализующий дисциплину обслуживания с приоритетом.
Этот метод вызывается из метода Complete О в момент завершения обслуживания
заявки и выбирает номер по порядку в очереди для той заявки, которую следует
выбрать для обслуживания в соответствии с заданным приоритетом. Сдвиг оче-
реди при этом, разумеется, происходит не от первой заявки, а от той, которая
была выбрана.
Нужно также определить смысл понятия «производительность» для данной си-
стемы. В предыдущей задаче мы могли измерять ее количеством разгруженных
самосвалов, так как все самосвалы были однотипны. Теперь же количество раз-
груженных самосвалов уже не определяет однозначно общую массу перевезен-
ной руды, поэтому последнюю придется подсчитывать отдельно. Делать это бу-
дем так: при окончании разгрузки 50-тонного самосвала увеличивать счетчик на
единицу, а при окончании разгрузки 20-тонного самосвала — на 0,4. Таким обра-
зом, производительность будем измерять общим количеством разгруженных 50-
тонных самосвалов (которое в итоге может оказаться дробным). Для подсчета
массы руды в тоннах это значение достаточно будет умножить на 50.
Далее приведен листинг программы (листинг 10.1). Так как ее код имеет много
общего с кодом программы из главы 9, комментарии в нем не столь подробны
и даны только для тех фрагментов, которые связаны с реализацией отличий.
Листинг 10.1. Файл ClasseslO.h, содержащий протоколы классов
#include<ctime>
#1nclude<cstdlo>
#include<cstdlib>
Листинг 10.1. Файл ClasseslO.h, содержащий протоколы классов 211
#include<cmath>
using namespace std:
FILE *qul;
FILE *qu2:
Int completed-!);
float weight-0; //счетчик перевезенной массы руды
float *quel_ave-NULL: //средние длины очередей к экскаваторам
float que2_ave-0: //средняя длина очереди к измельчителю
long int total:
int *ro_fuller-NULL: // коэффициенты загрузки экскаваторов
float ro_emptier-0:
long int path_full-OL:
long int path_empty-OL
long int take-OL:
long int give-OL:
class HeavyCar
{
int direct: //время в пути от экскаватора к измельчителю
int back: //время в пути от измельчителя к экскаватору
int id:
int pr: //1 - для 20-тонного. 2 - для 50-тонного самосвала
int host; // номер экскаватора, к которому приписан самосвал
int state:
int to_pfull;
int to_pempty:
void *f:
void *e:
public:
friend class Fuller:
friend class Emptier:
HeavyCardnt a. int b. int с): //метод-конструктор
void putFuller(Fuller *fl):
void putEmptier(Emptier *el):
void run():
void PrintO:
int Stated { return(state): }
}:
class Fuller
{
const static float perf20-0.2: //интенсивность загрузки 20-тонных
//самосвалов
const static float perf50-0.1: //интенсивность загрузки 50-тонных
//самосвалов
int *to_full;
HeavyCar **serving;
HeavyCar **queue;
public:
//Следующие поля данных объявлены открытыми, так как их значения //потребуются
конструктору класса Emptier для подсчета общего количества //самосвалов в системе
const static int volume-З: //количество экскаваторов
const static int N20-2; //количество 20-тонных самосвалов
//у одного экскаватора
212 Глава 10. Замкнутая система с раздельными очередями и приоритетами
const static int N50=l: //количество 50-тонных самосвалов
FullerO; -FullerO; //у-одного экскаватора
void PutCars(HeavyCar **h): void Completed nt i): void Arrival(HeavyCar *h): void PrintO; int qLengthdnt i): int Stated nt i): void run(): }: class Emptier { //метод для начального заполнения пункта //загрузки самосвалами
const static int volume=l: //количество измельчителей
const static float perf20=0.5: //интенсивность разгрузки 20-тонных самосвалов
const static float perf50=0.25: //интенсивность разгрузки 50-тонных самосвалов
const static int order=2; int *to_empty; HeavyCar **serving; HeavyCar **queue: // правило выбора заявок из очереди
int S; public: Emptier(Fuller *f); -Emptied): void Complete!int i): void Arrival(HeavyCar *h): void PrintO: int qLengthO: int BusyО: int FirstAvailO; void run(): //общее количество самосвалов - //максимальная длина очереди
int choice!): }: //выбор самосвала из очереди //в соответствии с приоритетом
//Метод-конструктор. Параметры: а - уникальный идентификатор, b - вид
//самосвала, с - номер экскаватора, к которому приписан самосвал
HeavyCar::HeavyCardnt a. int b. { id=a: pr=b: host=c: if (pr==l) { int с)
direct=150: //2.5 60 тактов модельного времени
back=90: //1.5 • 60 тактов модельного времени
}
else
{
direct=180;
back=120:
Листинг 10.1. Файл ClasseslO.h, содержащий протоколы классов 213
state=l:
to_pfull=-l;
to_pempty=-l;
}
void HeavyCar::putFuller(Full er *fl)
{
f-fl:
}
void HeavyCar::putEmpt1er(Emptier *el)
{
e=el:
}
void HeavyCar::PrintO
{
switch(state)
{
case 1: printfC"Самосвал 2d находится в очереди к экскаваторуХп”. id): break;
case 2; printfC"Самосвал 2d загружается\п". id): break:
case 3: printfC"Самосвал 2d движется с грузом. Прибудет к измельчителю через 2.If
минут\п". id. ((float)to_pfull)/60): break:
case 4: printfC“Самосвал 2d находится в очереди к измельчителкАп". id): break:
case 5: printfC"Самосвал 2d раэгружается\п". id): break:
case 6: printfC“Самосвал 2d движется порожний. Прибудет к экскаватору через 2.If
минут\п”. id. (Cfloat)to_pempty)/60); break:
}
}
void HeavyCar::run()
{
if (state==3)
{
to_pfull--:
if (to_pfull==0)
{
to_pfull=-l;
((Emptier*)e)->Arrival(this):
}
}
else if (state==6)
{
to_pempty--:
if (to_pempty==0)
{
to_pempty--l:
((Ful1 er*)f)->Arrival(this):
}
}
else:
switch(state)
{
case 1: take++: break: //в очереди к экскаватору
case 2: take++: break; //на погрузке
case 3: path_full++: break; //в пути к измельчителю
214 Глава 10. Замкнутая система с раздельными очередями и приоритетами
case 4: give++; break; //в очереди к измельчителю
case 5: give++: break; //на разгрузке
case 6: path_empty++: break: //в пути к экскаватору
}
return;
}
Fuller::Fuller()
{
int 1. j;
to_full=new int[volume]:
serving=new HeavyCar ‘[volume];
queue=new HeavyCar *[volume*(N20+N50)];
for(i=0:i<volume:i++)
{
to_full[i]--l:
serving[i]=NULL:
}
}
Fuller::-Fuller()
{
deleted to_full:
delete [] serving:
delete [] queue:
}
//В этот метод передается массив указателей, подготовленный в функции
//mainO
void Fuller::PutCars(HeavyCar **h)
{
int 1. j: float mu;
//Обход всех экскаваторов, заполнение очередей и постановка одного
//самосвала на погрузку
for(1=0;i<volume;i++)
{
//Первый самосвал из каждой очереди ставим на погрузку
if (h[i*(N20+N50)]->pr— 1) mu=perf20;
else mu=perf50:
toful 1 [1 ]=(int) (get_exp(mu)*60);
servi ng[i]=h[i *(N20+N50)]:
servi ng[i]->state=2:
//Остальные самосвалы ставим в очереди
for(j=0:j<(N20+N50-l):j++)
queue[i*(N20+N50)+j]-h[i*(N20+N50)+j+l]:
queued *( N20+N501+N20+N50- 1]=NULL:
}
}
void Fuller::Print()
{
int i. j;
for(i-0:i <volume:i ++)
{
printfCB очереди к Xd экскаватору - Id самосвалов\п”. i+1. qLength(i));
for(j=0;j<(N20+N50);j++)
Листинг 10.1. Файл ClasseslO.h, содержащий протоколы классов 215
{
if (queue[i*(N20+N50)+j]==NULL) break;
printfC'Xd-й в очереди - самосвал Ik W\n” j+1. queue[i*(N20+N50)+j]-> id);
}
}
for(1=0:i<volume:i++)
{
If (to_full[1]>0)
printfC'^d-й экскаватор работает. Он обслуживает самосвал IMd. До окончания загрузки
осталось Xd MHHyT\n".i+l.serving[i]->id.to_full[i]/60):
else printf("Xd-n экскаватор простаивает.\n".i+l);
}
return:
}
int Fuller::State(int 1)
{
if (servingti]1—NULL) return(l): //1 - экскаватор работает
return(O): //0 - экскаватор не работает
}
int Fuller::qLength(int i)
{
int j:
for(j=0:j<(N20+N50):j++)
if (queue[i*(N20+N50)+j]==NULL) return(j):
return(N20+N50):
}
void Fuller:;Arrival(HeavyCar *h)
{
int k. p: float mu:
//Определяем тип самосвала, интенсивность его загрузки и экскаватор
k=h->host:
if (h->pr=-l) mu=perf20:
else mu=perf50:
//Экскаватор занят, ставим самосвал в очередь к нему
if (State(k)--l)
{
p-qLength(k):
queue[k*(N20+N50)+p]=h:
queue[k*(N20+N50)+p]->state=l:.
}
//Экскаватор свободен, ставим самосвал на обслуживание
else
{
to_ful1[k]-(i nt)(get_exp(mu)*60);
serving[k]=h;
servi ng[k]->state=2:
}
}
void Fuller::Complete(1nt i)
{
int j; float mu:
216 Глава 10. Замкнутая система с раздельными очередями и приоритетами
to_full[i]=-l;
serving[i]->state=3:
serving[1]->to_pfull=servi ng[ 1 ]->di rect:
serving[i]=NULL;
if (qLengthd )==0) return:
//Очередь не пуста, ставим на освободившийся экскаватор новый самосвал
if (queued*(N20+N50]->pr==l)] mu=perf20;
else mu=perf50:
to_fu11[1]-(1nt)(get exp(mu)*60);
serving[i]=queue[i*(N20+N50)]:
queue[i *(N20+N50)]->state=2:
//Сдвигаем очередь
for(j=0:j<(N20+N50-l):j++)
queue[i*(N20+N50)+j]=queue[1*(N20+N50)+j+l]:
queue[i *(N20+N50)+N20+N50-1]=NULL:
}
void Fuller::run()
{
int i;
ford=0:i<volume;i++)
{
if (to_full[i]>0) to_full[i]-;
if (to_full[i]==0) Completed):
}
for(i =0:i <volume:i ++)
{
fprintf(qul."2d ". qLengthd)):
quel_ave[i]-quel_ave[i]*(l-1.0/(total+l))+((float)qLengthd))/(total+l):
ro_fuller[i]=ro_fuller[i ]+Stated):
} "
fprintf(qul, "\n“);
}
Emptier::Emptier(Fuller *f)
{
int 1;
//Вот для чего поля volume. N20 и N50 были объявлены открытыми
S=(f->volume)*(f->N20+f->N50):
to_empty=new int[volume]:
serving=new HeavyCar ‘[volume]:
queue=new HeavyCar *[S]:
fоrd=0:i<volume:i++)
{
to_empty[i]=-l:
serving[i]=NULL:
}
for(i=0;i<S:i++)
queued ]=NULL:
}
Emptier::-Emptier()
{
delete !] to_empty:
delete [] serving;
Листинг 10.1. Файл ClasseslO.h, содержащий протоколы классов 217
delete [] queue:
1
void Emptier::Print()
{
int 1:
printfCB очереди к измельчителю - W самосвалов\п". qLengthO):
for(i=0:i<S:i++)
{
if (queued ]==NULL) break:
printf("2d-fi в очереди - самосвал Ik £d\n". i+1. queued ]->id):
}
for(1=0:i<volume;i++)
{
if (to_empty[i]>0)
printf("£d-ft измельчитель работает. Он обслуживает самосвал Ik fcd До окончания
погрузки £d MMHyT\n".i+l.serving[i]->id.to_empty[i]/60):
else printf(”2d-ft измельчитель простаивает\п”.1+1):
}
return:
}
int Emptier::qLengthO
{
int i:
for(1=0:1<S:1++)
if (queued]==NULL) return(i):
return(S):
}
int Emptier::Busy()
{
int k=0:
for(int i=0;1<volume:i++)
if (to_empty[i]>0) k++:
return(k):
}
int Emptier::FirstAvailО
{
for(int i=0:i<volume:i++)
if (to_empty[i]==-l) return(i):
return(-l):
}
void Emptier::Arrival(HeavyCar *h)
{
int k. p: float mu:
if (h->pr--l) mu=perf20:
else mu=perf50:
k=FirstAvail():
//Ставим прибывший самосвал в очередь
if (k—1)
{
p=qLength():
218 Глава 10. Замкнутая система с раздельными очередями и приоритетами
queue[p]-h:
queue[p]->state=4:
}
//Ставим прибывший самосвал на обслуживание
else
{
t o_empty[к]-(i nt)(get_exp(mu)*60):
serving[k]-h:
se rv1ng[k]->state-5:
}
}
void Emptier:: Completed nt i)
{
int j. who;
float mu:
serving[i]->state=6;
servi ng L i]->to_pempty=servi ng[i]->back;
if (qLengthO—0)
{
to_empty[i]=-l:
serving[i]=NULL;
}
//Очередь не пуста
else
{
//Выбираем из очереди самосвал и ставим на разгрузку
who-choice():
if (queue[who]->pr—1) mu-perf20:
else mu-perf50;
to_empty[i]-(1nt)(get_exp(mu)*60):
serving[i]=queue[who];
serving[i]->state=5;
//Сдвигаем очередь
for(j=who:j<(S-l);j++)
queue[j]-queue[j+l]:
queue[S-l]=NULL:
}
}
//Реализация приоритета
int Emptier::choice()
{
int i:
//Приоритетов нет. Первый в очереди - на разгрузку
if (order—0) return(O):
if (order—2) //приоритет 50-тонным самосвалам
{
ford-0: i<S; 1++)
{
if (queue[i]==NULL) return(O);
if (queued]->pr—2) returnd):
}
return(O);
}
10.4. События и методы 219
else //приоритет 20-тонным самосвалам
{
for(i=0:1<S:1++)
{
if (queue[i]“NULL) return(O):
If (queued]->pr-=l) return(i):
}
return(O):
}
}
void Emptier::run()
{
for(int i-0:i<volume:i++)
{
if (to_empty[i]>0) to_empty[i]--:
if (to_empty[i]-=0) {
//Разгружен 20-тонный самосвал
if (serving[i]->pr--1) weight+=0.4:
else weight+-l: //разгружен 50-тонный самосвал
Completed):
completed++:
}
}
fprintf(qu2."*d\n". qLengthO):
que2_ave=que2_ave*(l-1.0/(total+l))+((float)qLength())/(total+l):
ro_emptier-ro_emptier*(1-1.0/(total+l))+(((fl oat)Busy())/volume)/(total+1):
}
Прежде чем рассмотреть листинг функции mainO (листинг 10.2), коротко оста-
новимся на порядке создания объектов и их инициализации, который в данной
задаче является отнюдь не произвольным. В первую очередь должен быть создан
объект класса Fuller, так как именно он владеет информацией о количестве экс-
каваторов и самосвалов, необходимой конструкторам других объектов. Эта ин-
формация может быть представлена либо в виде констант, как в приведенном
варианте программы, либо в виде значений полей данных, передаваемых в каче-
стве аргументов конструктору класса Fuller. Затем создаем объект класса Emptier,
передавая конструктору указатель на уже имеющийся объект Fuller. Далее за-
полняем массив указателей на объекты HeavyCar, зная их количество и типы
опять-таки из существующего объекта Fuller. Для каждого созданного объекта
HeavyCar устанавливаем связи с имеющимися объектами Fuller и Emptier. В за-
вершение для объекта Fuller вызываем метод PutCarsO, передавая в качестве па-
раметра сформированный массив указателей на HeavyCar. Только после этого все
объекты можно считать полностью проинициализированными и готовыми для
запуска основного моделирующего цикла.
Листинг 10.2. Функция main()
#define N 2В800
#include "classeslO.h"
int mainO
{
int 1. a. b. c. j:
220 Глава 10. Замкнутая система с раздельными очередями и приоритетами
HeavyCar **masCar=NULL;
srandC(unsigned)time(O)):
Fuller fu:
Emptier emC&fu):
a=fu.N20: b=fu.N50; c=fu.volume;
quel_ave=new float[c]:
ro_fuller=new int[c]:
for(i=0:i<c:i++)
{
quel_ave[i]=O;
ro_fuller[i]=0:
}
//Выделение памяти под массив указателей на HeavyCar
masCar=new HeavyCar *[(a+b)*c)]:
for(i=0:i<c:i++)
{
for(j=0:j<a:j++) //создание 20-тонных самосвалов для 1-го экскаватора
masCar[i*(a+b)+j]=new HeavyCar(i*(a+b)+j+l.l.i):
for(j=0:j<b:j++) //создание 50-тонных самосвалов для 1-го экскаватора
masCar[i*(a+b)+a+j]=new HeavyCar(i*(a+b)+a+j+1.2.i):
//Установление связей между объектами
forCj=0:j<Ca+b):j++)
{
masCar[i *(a+b)+j]->putFul1erC&fu):
masCa r[i *(a+b)+j]->putEmpt1er(&em):
}
}
//Заполнение пункта погрузки самосвалами
fu.PutCars(masCar):
qul=fopen("quel''. "wt”):
qu2=fopen(''que2". "wt"):
for(total=OL:total<N;total++) //основной моделирующий цикл
{
for(i=0;i<C;i++)
for(j=O:j<(a+b):j++)
masCar[i*Ca+b)+j]->runC);
fu.runO:
em.run():
}
//Освобождение памяти, выделенной под объекты и массивы
delete masCar:
delete[] quel_ave: deleted ro_fuller:
fcloseCqul):
fclose(qu2);
//Вывод результатов моделирования
printfC"Всего разгружено самосвалов 2d\n". completed):
printfC"Производительность !L3f\n". weight):
printfC"Средняя длина очереди к измельчителю ГЗЛп”. que2_ave);
printfC“Коэффициент загрузки измельчителя £.3f\n". ro_emptier):
for(i=0;i<c:i++)
{
ргШГС'Коэффициент загрузки экскаватора fcd - ГЗАп". i.
((f1оаt)ro_fu11er[i])/tota1):
10.5. Анализ результатов 221
printfC"Средняя длина очереди к экскаватору fcd - fc.3f\n". i. quel_ave[1]):
}
printfCfloPK времени, проведенного в пути порожняком ГЗЛп".
((float)path_empty)/(c*(a+b)*total)):
printfCflonK времени, проведенного в пути гружеными 1.3f\n".
((float)path_full)/(c*(a+b)*total)):
printfCflonfl времени, проведенного в пункте погрузки *.3f\n",
((float)take)/(c*(a+b)*total)):
printfC'floHfl времени, проведенного в пункте разгрузки r3f\n”,
((f1oat)gi ve)/(c*(a+b)*tota1)):
}
10.5. Анализ результатов
Несмотря на то что системы, описанные в главах 9 и 10, имеют много общего,
для рассматриваемой системы можно поставить ряд принципиально новых за-
дач, обусловленных именно теми ее особенностями, которые отсутствуют в си-
стеме, рассмотренной в главе 9. Прежде всего выясним влияние приоритета
обслуживания на показатели работы системы. Результаты моделирования пред-
ставлены в табл. 10.1.
Таблица 10.1. Сравнительный анализ дисциплин обслуживания
Показатель 50-тонным самосвалам Приоритет
20-тонным самосвалам Нет
Всего разгружено самосвалов 149-150 172 158-159
Производительность 93-94 91 92-93
Средняя длина очереди к измельчителю 2.53 2,34 2,43
Коэффициент загрузки измельчителя 0,86 0,88 0,86
Коффициент загрузки экскаватора 1 0,74 0,75 0,73
Коэффициент загрузки экскаватора 2 0,74 0,75 0,73
Коэффициент загрузки экскаватора 3 0,74 0,75 0,73
Средняя длина очереди к экскаватору 1 0,67 0,68 0.68
Средняя длина очереди к экскаватору 2 0,67 0,68 0,68
Средняя длина очереди к экскаватору 3 0,68 0,67 0,69
Доля времени в пути порожняком 0,06 0,06 0,06
Доля времени в пути груженым 0,1 0,1 0,1
Доля времени на пункте погрузки 0,47 0,48 0,47
Доля времени, проведенного на пункте разгрузки 0,37 0,36 0,37
222 Глава 10. Замкнутая система с раздельными очередями и приоритетами
Из этих результатов можно сделать следующие выводы:
О наибольшее количество разгруженных самосвалов получается при приорите-
те 20-тонных самосвалов. При этом же приоритете длина очереди к измель-
чителю является наименьшей. Этот результат хорошо согласуется с извест-
ным фактом из теории очередей, согласно которому приоритет коротких
заявок позволяет увеличить интенсивность выходного потока и уменьшить
среднюю длину очереди. Обычно реализация такого приоритета осложняется
тем, что заранее нужно знать длину заявки, однако в данном случае это про-
исходит естественным образом;
О наибольшая производительность, то есть наибольшая масса разгруженной
руды, достигается при приоритете 50-тонных самосвалов. Так как этот пока-
затель мы приняли за основной, во всех дальнейших экспериментах будем по
умолчанию задавать именно такой приоритет;
О нетрудно видеть, что наиболее узким местом системы является измельчи-
тель — и по коэффициенту загрузки, и по средней длине очереди.
На рис. 10.2-10.4 изображены зависимости некоторых параметров системы от
количества измельчителей. Из них видно, что, увеличив количество измельчите-
лей до четырех, можно поднять производительность системы до 117 50-тонных
самосвалов, то есть до 117 • 50 = 5850 т, а количество разгруженных самосва-
лов — до 206. Дальнейший рост количества измельчителей нецелесообразен.
Количество измельчителей
Рис. 10.2. Зависимость числа разгруженных самосвалов
от количества измельчителей
10.5. Анализ результатов 223
Количество измельчителей
Рис. 10.3. Зависимость производительности от количества измельчителей
Количество измельчителей
Рис. 10.4. Зависимость коэффициента загрузки пункта разгрузки от количества измельчителей
224 Глава 10. Замкнутая система с раздельными очередями и приоритетами
Не исключено, что читателю уже порядком набили оскомину кочующие из гла-
вы в главу и похожие друг на друга статистические зависимости, графики и зада-
чи, решения которых достаточно очевидны и интуитивно ясны, так что вопрос
остается только в получении конкретной цифры. Настало время несколько скра-
сить такое однообразие новой интересной задачей, решение которой не сможет
предугадать человек даже с самой развитой интуицией.
Итак, пункт погрузки состоит из 3 однотипных экскаваторов. Каждому из них
приписано М = 3 самосвала — два 20-тонных и один 50-тонный. Следовательно,
всего имеется в распоряжении шесть 20-тонных и три 50-тонных самосвалов.
Возникает вопрос: а нельзя ли достичь большей производительности, по-друго-
му распределив эти самосвалы между экскаваторами?
Ограничимся рассмотрением только таких вариантов распределения, в которых
экскаваторам достается одинаковое число самосвалов. Тогда вариантов остается
всего три. Результаты моделирования представим в виде код варианта — произ-
водительность (в 50-тонных самосвалах):
1 • (1 + 2 + 2) + 1 • (1 + 1 + 2) + 1 • (1 + 1 + 1) - 92,7.
2 • (1 + 1 + 1) + 1 • (2 + 2 + 2) — 90,4.
3 • (1 + 1 + 2) - 94,3.
Поясним, как мы кодируем варианты. Запись k (mt + т2 + т3), где k = 1, 2, 3,
//г,- = 1 или 2, означает, что k экскаваторам из трех приписано по столько 20-тон-
ных самосвалов, сколько имеется единиц в наборе (ти,, т2, т3), и по столько
50-тонных самосвалов, сколько в этом наборе двоек. Таким образом, исходной
формулировке нашей задачи соответствует третий вариант, который, как видим,
дает наилучшую производительность. Но почему именно этот вариант получает-
ся лучше других, какая тут закономерность? В теории очередей ответа на такой
вопрос мы не найдем. Приведенных статистических данных тоже явно недоста-
точно, чтобы уловить какую-либо закономерность. Попробуем провести анало-
гичные расчеты для других входных данных.
Если в нотации варианта знаки умножения и сложения понимать буквально, то
значения выражений равны 12, так как для каждого из них набор самосвалов в
общей совокупности остается одним и тем же. Назовем это значение весом зада-
чи и попробуем получить решение для других весов.
Семь 20-тонных самосвалов и два 50-тонных, вес И:
2 (1 + 1 + 1) + 1 • (1 + 2 + 2) — 89,4.
2 • (1 + 1 + 2) + 1 • (1 + 1 + 1) — 90,5.
Пять 20-тонных самосвалов и четыре 50-тонных, вес 13:
. 1 • (2 + 2 + 2) + 1 • (1 + 1 + 2) + 1 • (1 + 1 + 1) - 94,5.
2 • (1 + 1 + 2) + 1 • (1 + 2 + 2) — 97.
2 • (1 + 2 + 2) + 1 • (1 + 1 + 1) — 95,2.
Четыре 20-тонных самосвала и пять 50-тонных, вес 14:
1 • (2 + 2 + 2) + 1 • (1 + 2 + 2) + 1 • (1 + 1 + 1) - 96,5.
2 (1 + 1 + 2) + 1 • (2 + 2 + 2) — 97,8.
2 • (1 + 2 + 2) + 1 • (1 + 1 + 2) — 99,6.
10.5. Анализ результатов 225
Три 20-тонных самосвала и шесть 50-тонных, вес 15:
1 • (2 + 2 + 2) + 1 • (1 + 2 + 2) + 1 • (1 + 1 + 2) - 100,6.
2 • (2 + 2 + 2) + 1 • (1 + 1 + 1) - 98,1.
3(1 + 2 + 2)- 101,1.
Два 20-тонных самосвала и семь 50-тонных, вес 16:
2 - (1 + 2 + 2) + 1 - (2 + 2 + 2) — 103,7.
2 • (2 + 2 + 2) + 1 • (1 + 1 + 2) — 102,1.
На основании этих данных уже можно выдвинуть довольно правдоподобную ги-
потезу, почему оптимален именно тот, а не иной вариант распределения. Назо-
вем разбросом варианта kt mtj ,р<М, следующую величину:
1=1 7=1
М
м
ГДе?”-р =~V7
М
Рассмотрим вычисление разброса для первого варианта веса И — 2 (1 + 1 + 1) +
+ 1 (1 + 2 + 2). Имеем Л, = 2, k2 = 1, р = 2, М = 3, тп1ср = (1 +1 +1)/3 = 1,
ти2ф = (1 + 2 + 2)/3 = 5/3. Тогда г= 2 • (0 + 0 + 0) + 1 • (2/9 + 1/9 + 1/9) = 4/9.
Приведем результаты вычисления разброса для всех рассмотренных ранее вари-
антов.
1. Вес И: 4/9, 8/9.
2. Вес 12: 8/9, 0, 4/3.
3. Вес 13: 4/9, 4/3, 8/9.
4. Вес 14: 4/9, 8/9, 4/3.
5. Вес 15: 8/9, 0, 4/3.
6. Вес 16: 8/9, 4/9.
Теперь закономерность увидеть легко. Наилучшим является вариант с наиболь-
шим разбросом, и это правило не нарушилось ни разу. Проведем еще одну про-
верку для случая двух самосвалов, приписанных к каждому экскаватору.
Четыре 20-тонных самосвала, два 50-тонных, вес 8:
2 • (1 + 1) + 1 • (2 + 2) — 80,5, разброс 0.
2 • (1 + 2) + 1 • (1 + 1) — 81, разброс 1.
Три 20-тонных самосвала, три 50-тонных, вес 9:
1 - (1 + 1) + 1 (1 + 2) + 1 - (2 + 2) — 84,3, разброс 0,5.
3 • (1 + 2) — 85,6, разброс 1,5.
Два 20-тонных самосвала, четыре 50-тонных, вес 10: ,
2 • (1 + 2) + 1 (2 + 2) — 89, разброс 1.
2 • (2 + 2) + 1 • (1 + 1) — 87,7, разброс 0.
226 Глава 10. Замкнутая система с раздельными очередями и приоритетами
Как видим, и здесь наша гипотеза нигде не нарушается. Можно ли дать для нее
какое-либо теоретическое обоснование? Вопрос пока остается открытым.
Интересно отметить еще одну особенность — не всегда вариант с большим весом
дает большую производительность. Еще раз внимательно рассмотрим случай
М — 3. Вариант 11-2(11 — вес, 2 — номер варианта) лучше, чем 12-2; 13-2 лучше,
чем 14-1; 14-3 лучше, чем 15-2. Наилучшим при М= 3 все-таки является вариант
3 (2 + 2 + 2) с максимальным весом 18, дающий результат 106,5, но не исключе-
но, что при других соотношениях интенсивностей загрузки, разгрузки и времен
следования в пути оптимальным при фиксированном М может оказаться вари-
ант немаксимального веса, то есть когда не все самосвалы являются 50-тонными.
Все сказанное оставляет большой простор для теоретических исследований и воз-
можных правил и обобщений. Мы же на этом завершим главу, так как постанов-
ка и теоретическое исследование задач комбинаторной оптимизации не является
целью книги.
Выводы
1. Напрашивающееся применение наследования может не иметь достаточной
мотивации и оказаться неоправданным.
2. Для представления раздельных очередей в многоканальном узле необходим
двумерный массив указателей на объекты.
3. При наличии разнотипных заявок К типов и узла обслуживания, использу-
ющего приоритеты, в класс для этого узла вводится поле данных, являющее-
ся в общем случае целочисленным массивом размера К - 1, для задания при-
оритетов. Среди заявок с одинаковым приоритетом должно сохраниться
обслуживание в порядке поступления.
4. Порядок инициализации объектов в функции mainO не является произволь-
ным и должен учитывать информационные зависимости между классами.
5. Результаты моделирования находятся в соответствии с положением теории
массового обслуживания о приоритете коротких заявок.
6. Использование разработанной программы позволило выдвинуть и экспери-
ментально обосновать гипотезу о решении задачи комбинаторной оптимиза-
ции, для которой отсутствуют апробированные теоретические методы.
Задания для самостоятельной работы
1. Докажите, что в случае, если каждому из трех однотипных экскаваторов при-
писано по М самосвалов, общее число вариантов их распределения (по всем
значениям весов) равно (Л/ + 1) • (М + 2) • (М + 3)/6. Можно ли вывести об-
щую формулу для произвольного количества экскаваторов?
2. Попробуйте проверить гипотезу о максимальном разбросе для случая М = 4.
Программно реализовать варианты легко. Например, для варианта 3-(1 + 1 + 1)
Задания для самостоятельной работы 227
нужно в функции mainO заменить «циклическое» создание самосвалов сле-
дующим кодом:
masCar[O]*=new HeavyCard.1.0):
masCar[l]-=new HeavyCar(2.1.0):
masCar[2]-=new HeavyCar(3.1.0):
masCar[3]*=new HeavyCar(4.1.1):
masCar[4]-=new HeavyCar(5.1.1):
masCar[5]*=new HeavyCar(6.1.1);
masCar[6]=new HeavyCard, 1.2);
masCar[7]=new HeavyCar(8.1.2):
masCar[8]*=new HeavyCar(9.1.2):
Для M = 4 таких строк будет не 9, а 12. Для задания того или иного варианта
следует расставить нужным образом значения вторых аргументов в вызовах
конструктора. Следует также избавиться от полей данных N20 и N50, которые
при переборе вариантов распределения теряют смысл. Все вхождения в код
их суммы следует заменить значением М.
3. Для исходной формулировки задачи (шесть 20-тонных самосвалов, три 50-
тонных) рассмотрите варианты, в которых экскаваторам приписано неравное
количество самосвалов, например 1(2 + 2) + 1(1 + 1 + 2) +
+ 1(1+ 1 + 1 + 1) или даже 1-(1 + 1 + 14-1 + 1 + 1 + 2 + 2 +2) (все самосва-
лы приписаны одному экскаватору!). Проверьте, можно ли таким способом
увеличить разброс или общую производительность.
4. Попробуйте «поиграть» распределениями вероятностей. Подтвердится ли ги-
потеза о максимальном разбросе, если в условии задачи заменить одно или
оба экспоненциальных распределения нормальным или равномерным?
Глава 11
Система управления
запасами
с неудовлетворенным
спросом
Основные вопросы, рассматриваемые
в данной главе:
□ Особенности генерации
целочисленной случайной величины,
равномерно распределенной
на заданном отрезке
□ Проектирование классов для системы,
манипулирующей запасами
и заказами
□ Сохранение целостности данных при
реализации предварительного
«чернового» прогона
□ Влияние периодичности проверки
запасов. Оптимизация системы
11.2. Модельное время 229
11.1. Описание системы
В большом универмаге планируется ввести систему управления запасами радио-
приемников. Время между поступлениями запросов покупателей на покупку ра-
диоприемника распределено экспоненциально с математическим ожиданием
0,2 недели. Если покупателю потребовался радиоприемник, а его нет в запасе,
покупатель в 80 % случаев отправляется в ближайший магазин, представляя со-
бой тем самым несостоявшуюся для данного универмага продажу. В 20 % таких
случаев делается повторный заказ, и покупатели ждут поступления следующей
партии товара. Магазин использует периодическую систему просмотра состоя-
ния запасов, в которой запас просматривается каждые 4 недели и принимается
решение о необходимости осуществления (размещения) заказа на новую партию
товара. Стратегия принятия решения состоит в размещении заказа, доводящего
запас до контрольного уровня — 72 радиоприемников. Текущее состояние запаса
определяется как наличный запас плюс заказанные ранее универмагом радио-
приемники минус неудовлетворенный спрос, под которым понимаются упомя-
нутые ранее 20 % покупателей. Если текущее состояние запаса меньше или равно
18 радиоприемникам (точка заказа), заказ размещается. Время доставки заказа
(между его размещением и получением) постоянно и составляет 3 недели.
Необходимо смоделировать систему управления запасами за 6 лет (312 недель)
для получения статистических данных о следующих величинах:
О числе радиоприемников в запасе;
О неудовлетворенном спросе;
О количестве несостоявшихся продаж и времени между ними.
Начальные условия для имитации: состояние запаса — 72 радиоприемника, не-
удовлетворенного спроса нет. Чтобы уменьшить смещение статистических дан-
ных из-за начальных условий, все статистические данные, накопленные к концу
первого года шестилетнего периода имитации, должны очищаться (обнуляться).
11.2. Модельное время
В качестве единицы модельного времени принимаем 1 ч. В самом деле, если пе-
ресчитать 0,2 недели в днях, получим среднее время между поступлениями за-
просов на покупку радиоприемника, равное 1,4. Следовательно, день — слишком
грубая единица времени, потому что при округлении экспоненциальной случай-
ной величины со средним 1,4 до ближайшего целого нам придется отбрасывать
дробные части, сравнимые с самой случайной величиной. 1,4 дня составляет
33,6 ч, поэтому интенсивность входного потока равна 1/33,6 » 0,03 — ее мы и
подставим в качестве параметра экспоненциального распределения. Тогда общая
продолжительность моделирования составит 312 • 7 • 24 = 52 416 ч — тактов мо-
дельного времени. Значения же постоянных промежутков времени (4 недели и
3 недели) представим в программе в днях (соответственно, 28 и 21). Для чего это
сделано?
230 Глава 11. Система управления запасами с неудовлетворенным спросом
При инициализации объекта Супермаркет (SuperMarket) разыгрываем время, остав-
шееся до очередного просмотра состояния запасов, как целочисленную случай-
ную величину, равномерно распределенную на временном отрезке 4 недели.
Если этот отрезок выразить в часах, то выражение, вычисляющее случайную ве-
личину, должно быть таким: rand()*672+l; если в днях — (гand0*28+1)*24. Второе
выражение реализует равномерное распределение точнее. Дело в том, что функ-
ция rand О возвращает целочисленную случайную величину, равномерно распре-
деленную на отрезке от 0 до 32 767. Если необходимо снизить верхнюю границу
интервала до некоторого числа К, то конструкция rand()*K обеспечит равномер-
ное распределение только в том случае, если 32 768 делится на К без остатка.
В противном случае некоторые значения будут более вероятны, другие — менее.
Нетрудно заметить, что нарушение равномерности распределения будет тем
больше, чем больше К. Например, для К = 3 различиями можно пренебречь,
а для К = 32 767 значение 0 будет в два раза вероятнее всех остальных. Поэтому
при генерации равномерной случайной величины мы снижаем значение делите-
ля, а затем результат умножаем на 24. Этим мы нисколько не нарушаем логику
моделирования. Да и с точки зрения здравого смысла, любой человек в описан-
ной в задаче ситуации на вопрос: «Сколько времени осталось до проверки?» —
даст ответ в днях, а не в часах.
11.3. Классы и объекты
Данная задача по сути представляет собой усложненный вариант процесса слу-
чайного блуждания [4, гл. 3], где в качестве случайной величины выступает теку-
щий запас товара. Система является открытой, а количество заявок в ней — пе-
ременной величиной без фиксированной верхней границы. Определяющую роль
в системе играет объект Супермаркет, для которого нужно, разумеется, создать
класс. Чтобы накапливать статистику о среднем времени ожидания товара теми
клиентами, которые при первом обращении не получили его и дали повторный
запрос, необходимо создать класс Клиент (Client) и хранить информацию о них в
объектах этого класса. В самом деле, клиенты, сразу получившие товар, в систе-
ме не задерживаются, и объекты для них создавать не нужно. То же самое отно-
сится и к клиентам, которые, не получив товар, не пожелали ждать. Те же клиен-
ты, которые согласились ждать прибытия очередной партии товара, остаются в
системе, и вплоть до момента получения ими товара информацию о них нужно
отслеживать, а именно: вести учет времени ожидания. Для объекта Радиоприемник,
тоже участвующего в работе системы, класс создавать нет необходимости, по-
скольку на всем протяжении моделирования мы работаем только с количества-
ми радиоприемников и никакая статистика по отдельно взятым единицам этого
товара не требуется.
Итак, с классом Клиент все ясно — его полями данных являются уникальный
идентификатор и время, которое он к данному моменту провел в системе, ожи-
дая исполнения заказа. Опишем, какие поля данных должен иметь класс Супер-
маркет (SuperMarket).
11.4. События и методы 231
Неизменяемые поля:
О интенсивность поступления клиентских запросов (0,03 заявок в час);
О периодичность проверок состояния запаса (28 дней);
О время исполнения заказа (21 день);
О нижний предел количества товара, при выходе за который делается заказ (18);
О уровень наличия товара, исходя из которого рассчитывается объем заказа (72);
О процент заявок, покинувших систему, из числа тех, которые не застали товар
в наличии (80).
Изменяемые поля:
О время, оставшееся до прибытия следующего покупательского запроса на ра
диоприемник;
О время, оставшееся до получения заказа; в случае, если в данный момент мы
не ждем заказа, значение равно -1; •
О время, оставшееся до начала следующей проверки;
О объем заказа, получение которого ожидается: в случае отсутствия заказа ра-
вен 0;
О текущее количество товара;
О список указателей на объекты класса Client, ожидающие получения товара.
В данном случае именно список, а не массив, так как*длина очереди не имеет
верхней границы;
О текущая длина очереди, может быть вычислена по списку указателей.
11.4. События и методы
Событий, меняющих состояние объекта Супермаркет, всего три: поступление кли-
ентского запроса, проверка состояния запаса и прибытие заказа. Оформление за-
каза не является отдельным событием, а входит в алгоритм проверки состояния
запаса. Каждому из этих трех событий соответствуют методы, алгоритмическая
реализация которых совершенно очевидна, не содержит никаких программных
ухищрений и очевидным образом кодирует словесное описание задачи. Объекты
класса Client не являются активными участниками процесса моделирования,
так как все действия по их созданию, обработке и удалению происходят внутри
методов класса SuperMarket. Поэтому метода run() у класса Client нет.
С какой целью задано условие сброса накопленной статистики по истечении
первого года? Дело в том, что если существует стационарный режим, случайный
процесс достигает его вне зависимости от начальных условий. Но за какое время
это произойдет, заранее ответить невозможно. Поэтому, если целью моделирова-
ния является получение стационарных характеристик, влияние начальных усло-
вий на результаты надо каким-то образом нивелировать. Сделать это можно двумя
способами: моделировать в течение длительного времени, так что соотношение
времен, проведенных в стационарном и переходном режимах, будет таким боль-
шим, что влиянием переходного режима можно пренебречь; в течение некоторого
232 Глава 11. Система управления запасами с неудовлетворенным спросом
времени моделировать «вхолостую» и только затем, считая, что стационарный
режим уже достигнут, включать режим сбора статистики. Ни один из способов,
разумеется, не дает стопроцентной гарантии, поскольку заранее ничего нельзя
сказать о длительности схождения процесса к стационарному режиму. До сих
пор мы пользовались первым способом, но здесь применим второй, заодно рас-
смотрев некоторые нюансы сброса статистики, так как этот сброс нужно выпол-
нить аккуратно (листинг 11.1).
Условие «чернового» прогона в течение одного года с промежуточным сбросом
статистики реализовано в функции mainO (листинг 11.2). Здесь важно учесть
следующее принципиальное обстоятельство. После сброса статистики возобнов-
ление моделирования происходит уже не с того состояния объекта, с которого
оно начиналось. Поэтому очень важно, чтобы корректность работы методов не
зависела от начального состояния объекта. Чтобы проиллюстрировать эту
мысль, мы специально не стали вводить в число глобальных статистических пе-
ременных счетчик числа заявок, покидающих очередь, который необходим для
подсчета среднего времени ожидания. Опасность подстерегает нас в случае, если
значение этого счетчика подсчитывать косвенно:
entered-rejected-sati sf 1ed-q_length+l.
где entered — счетчик всех запросов; rejected — счетчик потерянных запросов;
satisfied — счетчик немедленно обслуженных запросов; qlength — текущая дли-
на очереди. Казалось бы, все логично: после всех вычитаний остаются только те
заявки, которые побывали в очереди и уже покинули ее. Здесь необходимо, од-
нако, соблюдение одного условия: в момент начала сбора статистики очередь
должна быть пуста. Иначе мы получим абсурдный результат в виде отрицатель-
ного значения счетчика (например, в начальный момент времени -q length), что
приведет к некорректному подсчету среднего времени ожидания. Это затрудне-
ние преодолено следующим образом. Для класса SuperMarket вводится дополни-
тельное поле данных q_extra, которое инициализируется текущей длиной очереди
в момент завершения «чернового» прогона. Тогда в методе SuperMarket::complete!),
имитирующем поступление заказа и удовлетворение за счет этого заявок, ожи-
дающих в очереди, порядковый номер удовлетворенной заявки в выходном по-
токе можно рассчитать по формуле
c~entered-rejected-satisfied-q_length+q_extra+l;
Величина 1/с затем используется в качестве усредняющего множителя для рас-
чета среднего времени пребывания заявки в системе.
Листинг 11.1. Файл Classesll.h с описанием протоколов классов
#include<cstdio>
#include<cstdlib>
#include<ctime>
#1nclude<cmath>
using namespace std;
#include "List.ii"
FILE *sojourn; //файл для сбора статистики о времени ожидания
//товара
FILE *que: //файл для сбора статистики о длине очереди;
//пополняется один раз в неделю
Листинг 11.1. Файл CLassesll.h с описанием протоколов классов 233
long int entered-OL: //счетчик общего числа заявок на товар
long int rejected-OL; //счетчик числа заявок, сразу покинувших систему
long int satisfied-OL; //счетчик числа заявок, немедленно удовлетворенных
int ntim_orders“0: //счетчик числа сделанных заказов
float soj_ave4): //переменная для подсчета среднего времени ожидания
float que_ave-0; //переменная для подсчета средней длины очереди
long int total: //счетчик тактов модельного времени (количество
//часов)
//Протокол класса Client
class Client
{
long int id: //уникальный идентификатор клиента
int hours: //время, проведенное клиентом в системе
public:
friend class SuperMarket:
ClientO //метод-конструктор
{
//Вычисляем, какая это по счету заявка, поставленная в очередь.
//от момента начала моделирования, и назначаем ей идентификатор
i d-entered-rejected-satisfi ed+1:
hours=0;
}
void PrintO:
long int getldO;
int getTimeO:
}:
//Вывод содержимого объекта
void Client::Print()
{
//168 - количество часов в неделе
printf("id~Sld\n ждет исполнения заказа Sd недель\п". id. hours/168):
}
//Чтение идентификатора заявки
long int Client::getld()
{
return(id):
)
//Чтение проведенного в системе времени
int Client::getTime()
{
return(hours);
}
//Протокол класса Супермаркет
class SuperMarket
{
int to_arrival; int to_order; //время до прибытия следующей заявки //время до исполнения заказа
int order: //объем ожидаемого заказа
int to_check: //время до следующей проверки
int q_length; //текущая длина очереди
int exist: //текущее количество товара
234 Глава 11. Система управления запасами с неудовлетворенным спросом
ListNode<Cl1ent* *queue; //очередь ожидающих заявок
//Описание неизменяемых полей данных
const static float mu-0.03:
const static int checking-2B:
const static int ordering-21:
const static int level 1-18:
const static int level2-72;
const static int percentage-80:
public:
SuperMarket(int 1):
void run();
void arrival0:
void completed:
void checkO:
void PrintO:
int getLengthO:
int q_extra: //переменная для хранения начальной длины очереди
}:
//Метод-конструктор. Параметр - исходное количество товара
SuperMarket::SuperMarket(int i)
{
q_length=0:
q_extra-0;
queue-NULL:
to_a r r1va1-(i nt)(get_exp(mu));
//«Насильственно» устанавливаем экспоненциальную случайную величину
//в единицу, если после округления до целого она обратилась в ноль.
//Вероятность такой ситуации тем меньше, чем с большим коэффициентом
//промасштабировано время. В данном случае она равна
//1-ехр(-0.03 • 0.5) - 0,015.
if (to_arrival“0) to_arrival-l:
to_order=-l:
order-0:
exi st-i:
//Время до ближайшей проверки устанавливается случайным образом
to_check=(rand()fcchecking+l)*24:
)
int SuperMarket::getLength()
{
return(q_length);
)
void SuperMarket::PrintO
{
printfC"Следующая заявка поступит через fcd часов\п". to_arrival);
if (to_order>0) {
printfC"Заказ прибудет через W дней, он составляет £d единиц товара\п".
to_order/24. order);
)
else
printfC“Заказа нет\п"):
printfC“Следующая проверка запасов состоится через Sd дней\п", to_check/24):
printfC'WflyT удовлетворения запроса И клиентов\п“. q_length):
Листинг 11.1. Файл Classesll.h с описанием протоколов классов 235
printf("Имеется fcd единиц товара\п“, exist):
}
//Моделирование прибытия нового запроса
void SuperMarket::arrival О
{
int i:
Client *p-NULL:
//Разыгрываем новый интервал между прибытиями
to_a г гi val-(i nt)(get_exp(mu));
if (to_arrival=0) to_arrival~l:
//инкремент общего счетчика запросов
//товар есть
//декремент количества товара
//инкремент счетчика сразу удовлетворенных
//запросов
entered++;
if (exist>0)
{
exist--;
satisfied**:
)
else //товара нет
{
if (rand(WOO<percentage) //клиент не стал ждать и ушел
{
rejected++: //инкремент счетчика потерянных клиентов
return:
)
//Создаем новый обьект класса Client и новый элемент списка
p-new Client();
ListNode<Client> *ptr=new ListNode<Client>(p.NULL):
//Очереди нет. Новый элемент становится головой списка
if (q_length=“0) queue-ptr:
//Добавляем новый элемент в хвост списка
else ListAdd<Client>(queue.ptr):
q_length++: //инкремент длины очереди
}
return:
}
//Имитация прибытия заказа
void SuperMarket::completed
{
int mi. i. b. c:
to_order*-l:
exist+-order;
order-0:
if (q_length=*0) return:
//Определяем, сколько единиц товара будет продано немедленно
if (exist<q_length) mi=exist: else mi~q_length:
for(i-0:i<mi:i++)
{
//Отпускаем товар клиенту, находящемуся в голове списка
b-queue->Data()->getTime();
//Время ожидания записываем в файл (в днях)
fprintf(sojourn.'T2f\n". ((float)b)/24);
//Определяем, каким по счету из покинувших очередь с момента начала
//моделирования является этот клиент. Учитывается начальная длина очереди
236 Глава 11. Система управления запасами с неудовлетворенным спросом
c=entered-rejected-sati.sfied-q_length+q_extra+l;
soj_ave-soj_ave*(l-1.0/c)+(float)b/c: //пересчет среднего времени
//ожидания
//Удаляем элемент из головы списка
ListNode<Client> *ptr-queue:
queue=queue->Next():
delete ptг;
q_length--: //декремент длины очереди
exist--: //декремент количества товара
}
}
//Имитация проверки состояния запаса
void SuperMarket::check()
{
int а:
to_check-checki ng*24:
//Вычисление текущего состояния запаса
a-exi st+order-q_length:
if (a>=levell) return; //заказ делать не нужно
//Заказ делать нужно
to_order-ordering*24:
//вычисление обьема заказа
order-1evel2-a:
num_orders++: //инкремент количества заказов
}
//Метод-диспетчер
void SuperMarket::run()
{
int 1:
float a;
ListNode<Client> *ptr;
to_arrival--;
if (to_arrival--0) arrivalO:
if (to_order>0) to_order--:
if (to_order“0) completed:
to_check--:
if (to_check—0) check О:
//Инкремент текущего времени пребывания для всех клиентов ожидающих исполнения заказа
if (queue!-NULL)
{
ptr-queue:
while(ptrl-NULL)
{
((pt r - >Data ()) - >hou r s) ++:
ptr-ptr->Next():
)
)
//Еженедельная запись в файл текущей длины очереди
if (totaW==0) fprintf(que."fcd\n". q_length):
//Пересчет средней длины очереди
que_ave=que_ave*(1-1.О/(total+1))+((f1oat)q_length)/(total+1):
return;
)
11.5. Анализ результатов 237
Листинг 11.2. Функция main()
#define N 52416 // общее время моделирования
#include "classesll.h"
int mainO
{
//Открытие файлов для сбора статистики
que=fopen("quel". “wt");
sojourn-fopenC"sojourn". “wt"):
//Инициализация генератора случайных чисел
srandC(unsigned)time(O));
SuperMarket s(72):
//«Черновой» прогон в течение года. В760 - количество часов в году
for(total“OL:total<8760;total++)
{
s.runO:
)
fclose(que):
fclose(sojourn):
//сброс статистики
que=fopen(“quel". "wt");
sojourn-fopenC“sojourn". "wt"):
entered=0L:
rejected=0L;
satisfied-OL;
num_orders“0:
soj_ave=0:
que_ave=0;
//Запись текущей длины очереди в поле данных q_extra перед началом сбора
//статистики
s.q_extra~s.getLength():
//Основной моделирующий цикл
for(total-0L:total<N:tota1++)
{
s.runO:
)
fclose(sojourn):
fclose(que):
//Вывод на печать результатов имитационного эксперимента
printfC“Всего поступило заявок fcld\n", entered);
printfC’flonK потерянных заявок $.3f\n", ((float)rejected)/entered):
printfC'flonK немедленно обслуженных заявок r3f\n". ((ftoat)satisfied)/entered);
printfC Количество заказов W\n". num_orders);
printfC“Среднее время ожидания И дней\п", soj_ave/24);
printfC"Средняя длина очереди Шп". que_ave);
)
11.5. Анализ результатов
Многократный прогон модели в течение 6 лет дал следующие результаты:
О количество заявок — 1594;
О доля потерянных заявок — 0,1;
238 Глава 11. Система управления запасами с неудовлетворенным спросом
О доля немедленно удовлетворенных заявок — 0,88;
О количество заказов — 22-23;
О среднее время ожидания — 9,3 дней;
О средняя длина очереди — 0,17.
Более интересны другие зависимости. Предположим, что мы не можем ни увели-
чить скорость выполнения заказа, ни снизить требовательность клиентов, ни
расширить складские площади для хранения более 72 единиц товара. Тогда по-
лучается, что мы можем управлять только периодичностью проверки запасов
(она не может быть менее трех недель — срока исполнения заказа) и нижним по-
рогом заказа (он не может превышать 72). В числе показателей эффективности
функционирования оставим три: долю потерянных заявок, количество заказов,
среднее время ожидания.
На рис. 11.1-11.3 приведены зависимости этих показателей от периодичности
проверки, все времена измерены в днях. Мы видим, что зависимости эти отнюдь
не монотонны, хотя общие тенденции к росту или уменьшению все же сохраня-
ются. Как объяснить такое явление?
Интервал между проверками
Рис. 11.1. Зависимость доли потерянных заявок от периодичности
проверки запасов
11.5. Анализ результатов 239
Интервал между проверками
Рис. 11.2. Зависимость количества заказов от периодичности проверки запасов
Интервал между проверками
Рис. 11.3. Зависимость среднего времени ожидания от периодичности проверки запасов
240 Глава 11. Система управления запасами с неудовлетворенным спросом
Суть его в том, что для данной задачи небольшое увеличение периодичности
проверки запасов совсем не обязательно, как это ни странно, приводит к ухудше-
нию показателей, а может, наоборот, значительно их улучшить. Все зависит от
того, в какой момент мы «поймаем» проверкой запасов ситуацию, когда текущий
запас уменьшился до 18. Предположим, что в момент времени Т дней он стал
равен 17. Если периодичность проверки такова, что проверка произошла на
(Т - 1)-й день, то эта ситуация очень нехороша, так как то, что запас меньше
критического значения, будет обнаружено очень нескоро, соответственно, неско-
ро произойдет и пополнение. Если же увеличить периодичность таким образом,
что проверка произойдет на несколько дней позже, чем Т, ситуация значительно
улучшится, так как время от уменьшения запаса до его пополнения сократится.
Таким образом, если мы, к примеру, хотим обеспечить долю потерянных заявок
не более 0,2, периодичность проверки либо должна быть менее 62 дней, либо ле-
жать в интервале приблизительно от 92 до 122 дней (условие задачи этому огра-
ничению как раз удовлетворяет — проверка запасов выполняется раз в 28 дней).
Этой же цели, как следует из графика, изображенного на рис. 11.4, можно до-
стичь, установив нижний уровень заказа не более 5.
Монотонный характер зависимостей, изображенных на рис. 11.4-11.6, легко объ-
ясним. В процессе работы супермаркета запас товара неуклонно уменьшается,
и чем раньше мы начнем «бить тревогу» (то есть сделаем заказ на его пополне-
ние), тем качественнее обслужим своих клиентов.
Рис. 11.4. Зависимость доли потерянных заявок от нижнего уровня заказа
11.5. Анализ результатов 241
Нижний уровень заказа
Рис. 11.5. Зависимость количества заказов от нижнего уровня заказа
Рис. 11.6. Зависимость среднего времени ожидания от нижнего уровня заказа
242 Глава 11. Система управления запасами с неудовлетворенным спросом
Можно поставить еще одну интересную задачу. Как видно из приведенных на
рисунках графиков, уменьшение доли потерянных заявок и среднего времени
ожидания сопровождается увеличением количества сделанных заказов, каждый
из которых может сопровождаться определенными накладными расходами вне
зависимости от объема заказанного товара. Если степень «дискомфорта», кото-
рый доставляет нам единица каждой из этих величин, количественно выразить
весовыми коэффициентами с„ с2, с3, то возникает задача минимизации целевой
функции:
Е(Р) = ct Lost + с2 • Orders + с3 Тср
или
Е(Е) = Ct • Lost + с2 • Orders + с3 Тср,
где Р — периодичность проверки; Lost — доля потерянных заявок; Orders — коли-
чество заказов; 7^ — среднее время ожидания; L — нижний уровень заказа. Весо-
вым коэффициентам можно придать вполне реальный смысл, если предполо-
жить, что за каждую потерянную заявку, за каждый день ожидания клиента и за
каждый заказ мы платим определенную сумму и нужно минимизировать общие
расходы. Чтобы приблизительно выровнять значения слагаемых, примем с, = 15,
с2 = 1, с3 = 0,35.
Периодичность проверки
Рис. 11.7. Зависимость значения критерия от периодичности проверки
11.5. Анализ результатов 243
Все это реально, так как уход клиента без покупки, да еще с намерением больше
в этот супермаркет не возвращаться — тяжелая потеря для торговой организа-
ции.
Согласно графику, приведенному на рис. 11.7, минимум критерия достигается
при Р = 59, но значение минимума в этой точке — 26,3 — ненамного отличается
от значения в другой точке локального минимума (Р= 36), равного 26,6. Видимо,
лучше все-таки принять периодичность проверки, равную 36 дням.
Проверим, можно ли улучшить решение за счет варьирования нижнего уровня
заказа (сохраняя Р= 28 дней). График на рис. 11.8 показывает, что минимум кри-
терия достигается при L = 8 и равен приблизительно 25,8, то есть он немного
меньше минимума по параметру Р. Следовательно, формулируем вывод: умень-
шение запаса до значения 18 — еще не повод бить тревогу. Следует сохранять
терпение и выдержку и оформлять заказ на пополнение запаса только тогда, ко-
гда запас уменьшится до 8.
Нижняя граница заказа
Рис. 11.8. Зависимость значения критерия от нижнего уровня заказа
Разумеется, при иных значениях весовых коэффициентов оптимальное решение
тоже будет иным. Задание этих коэффициентов близкими к реальным зависит
исключительно от опыта и квалификации эксперта, например главного бухгал-
тера или коммерческого директора супермаркета.
244 Глава 11. Система управления запасами с неудовлетворенным спросом
Выводы
1. Точность генерации с помощью функции randO целочисленной случайной
величины, равномерно распределенной на некотором конечном множестве
значений, тем выше, чем меньше мощность этого множества и чем больше
значение константы RAND MAX, определенной в заголовочном файле cstdlib.
2. Класс для объектного представления единицы хранимого запаса создавать не
нужно.
3. Корректность работы методов не должна зависеть от начальных значений по-
лей данных объектов.
4. Зависимости показателей системы от периодичности проверки запаса носят
не вполне простой, но объяснимый характер.
5. Удачно сформировать критерий оптимизации и подобрать весовые коэффи-
циенты — сложная задача, требующая участия квалифицированного эксперта.
Задания для самостоятельной работы
Попробуйте экспериментально решить задачу двумерной оптимизации по пара-
метрам Р и L, например методом покоординатного спуска [9]. Одну из точек (36; 18)
или (28; 8) можно принять в качестве довольно хорошего начального приближе-
ния. В какой точке (Р; 1) достигается абсолютный минимум критерия?
Глава 12
Очереди с разнотипными
заявками:работа порта
Основные вопросы, рассматриваемые
в данной главе:
□ Открыто-замкнутая система.
Неименованные и именованные
заявки
□ Применение наследования для
объектного представления заявок
в системе
□ Полиморфизм методов
□ Учет сопутствующих факторов
(штормы)
□ Расчет оптимального количества
именованных заявок и выявление
узкого места в системе
246 Глава 12. Очереди с разнотипными заявками: работа порта
12.1. Описание системы
В африканском порту танкеры загружаются сырой нефтью, которую затем мор-
ским путем доставляют по назначению. Мощности порта позволяют загружать
на более трех танкеров одновременно. Танкеры, прибывающие в порт через каж-
дые 11 ± 7 ч, относятся к трем различным типам. Относительная частота появ-
ления танкеров данного типа и время, требуемое на их погрузку, приведены в
табл. 12.1. Относительную частоту следует понимать как вероятность того, что
прибывший танкер относится к данному типу.
Таблица 12.1. Характеристики типов танкеров
Тип Относительная частота Время погрузки, ч
1 0,25 18 ±2
2 0,55 24 ±3
3 0,20 36 ±4
В порту имеется один буксир, услугами которого пользуются все танкеры при
причаливании и отчаливании. Причаливание и отчаливание занимает по одному
часу, причем, если в услугах буксира нуждаются сразу несколько танкеров, при-
оритет отдается операции причаливания.
Судовладелец предлагает дирекции порта заключить контракт на перевозку неф-
ти в Великобританию и обеспечить выполнение условий контракта с помощью
5 танкеров особого, четвертого типа, для погрузки которых требуется 21 ± 3 ч.
После погрузки танкер отчаливает и следует в Великобританию, там разгружает-
ся и затем снова возвращается в африканский порт для погрузки. Время цикла
обращения танкера, включая время разгрузки, составляет 240 ± 24 ч.
Фактором, осложняющим перевозку нефти, являются штормы, которым подвер-
гается порт. Интервал времени между штормами распределен экспоненциально
с математическим ожиданием 48 ч, причем шторм продолжается 4 ± 2 ч. Во вре-
мя шторма буксир не работает.
Перед заключением контракта руководство порта решило определить влияние,
которое окажут пять дополнительных танкеров на функционирование порта.
Выводы предлагается сделать по результатам имитации работы порта в течение
одного года (8760 ч) при условии заключения предлагаемого контракта. Оцени-
ваемые величины — время пребывания в порту дополнительных танкеров и уже
работающих танкеров трех типов.
12.2. Модельное время
За единицу модельного времени примем 1 мин, чтобы не связывать себя мало-
вероятным предположением, что все события занимают промежутки времени,
кратные одному часу. Интервал времени между штормами будем генерировать
12.3. Классы и объекты 247
так: (int) (get_exp(mu)*60), где mu = 1/48 = 0,021. Для генерации равномерного рас-
пределения (для интервалов между прибытиями танкеров, времени погрузки и
цикла обращения) будем использовать функцию get uniformO, которая разыгры-
вает абсолютное значение отклонения от среднего, а затем с вероятностью 0,5
прибавляет его к среднему либо вычитает из него. Этот способ позволяет умень-
шить в два раза значение делителя при взятии остатка, а значит, снизить ошиб-
ку, возникающую из-за того, что 32 768 не делится нацело на этот делитель (лис-
тинг 12.1).
Листинг 12.1. Генератор равномерного распределения
Int get_uniform(int a. int b)
{ //Генерация равномерно распределенной величины а±Ь
int х. у:
x=rand()X(b+l);
y=rand()X2:
if (у==0) return(a-x):
return(а+х):
}
12.3. Классы и объекты
Продолжаем наращивать арсенал приемов, используемых для объектного пред-
ставления динамических систем. Рассмотрим особенности поставленной задачи,
с которыми ранее мы не встречались:
О Буксир имеет две разных очереди на обслуживание — на причаливание и на
отчаливание. Заявки из очереди на отчаливание обслуживаются только в том
случае, если очередь на причаливание пуста.
О Система соединяет в себе свойства как открытой, так и замкнутой. Количест-
во заявок первых трех типов является переменной неограниченной величи-
ной, так как они поступают из внешнего входного потока и, будучи обслужен-
ными, покидают систему, после чего их дальнейшая судьба не отслеживается.
Количество же заявок дополнительного, четвертого типа является постоян-
ным, каждая из них периодически возвращается на обслуживание в систему,
и за ними нужно продолжать следить в промежутках между периодами об-
служивания (путешествие из Африки в Великобританию на разгрузку нефти
и обратно). В связи с этим заявки первых трех типов будем называть неиме-
нованными, а заявки четвертого типа — именованными.
Три типа неименованных заявок, разумеется, можно описать одним классом,
так как они различаются только значениями своих неизменяемых полей дан-
ных — частотой встречаемости и временем обслуживания. Так как неименован-
ные заявки постоянно находятся под контролем некоторого обслуживающего
устройства — буксира или порта, — метод run() для них не нужен, их постоянно
будут «вести» другие объекты, а после выхода из порта они как объекты переста-
ют существовать. В противоположность этому именованные заявки после выхо-
да из порта пускаются «в самостоятельное плавание», продолжая существовать
248 Глава 12. Очереди с разнотипными заявками: работа порта
в качестве полноправных объектов системы. В это время они сами должны сле-
дить за собой и в конце концов зафиксировать момент следующего прибытия на
погрузку. Очень показательна разница между механизмами фиксации прибытия
неименованных и именованных заявок. Неименованные заявки поступают из
случайного входного потока, поэтому время их прибытия разыгрывается с помо-
щью ГСЧ, а само событие инициируется принимающей стороной — буксиром.
Прибытие же именованной заявки буксиром не разыгрывается — она сама дает
знать о прибытии в порт, посылая буксиру соответствующее сообщение.
Указанные различия в поведении говорят о том, что неименованные и именован-
ные заявки одним классом представлять нельзя, так как эти различия являются
существенными. Для отображения поведения именованной заявки нам понадо-
бятся и дополнительные поля данных, и дополнительные методы. Но и описы-
вать их совершенно разными классами тоже нехорошо. Почему? Дело в том, что
после причаливания как те, так и другие заявки будут находиться в общей очере-
ди на погрузку. Так как длина этой очереди не ограничена, то по традиции жела-
тельно описывать ее связным списком с помощью уже готового и неоднократно
примененного шаблона. Но если в очереди будут заявки разных классов, какой
же тогда класс подставлять в шаблон в качестве значения параметра, коим как
раз и является имя класса? Получается, что и один класс, и два разных класса —
неудовлетворительные решения.
Ответ приходит сам собой — конечно же, следует применить наследование. Вни-
мательное изучение того, что происходит в системе и что нужно отразить в про-
грамме, позволяет сделать вывод, что все поля данных и методы неименованных
заявок покрываются именованными, последние же расширяются некоторым
множеством дополнительных полей и методов, например run(), а некоторые ме-
тоды переопределяются (например, PrintO). Поэтому базовым классом будет
класс неименованных заявок, а производным от него — расширенный класс име-
нованных заявок. Проблема с «разнотипностью элементов» списка тоже решает-
ся наилучшим образом. Напомним, что в качестве данных в элементе списка вы-
ступает не сам объект, а указатель на него, поэтому в качестве параметра
шаблона можно задать имя базового класса — неименованных заявок. По прин-
ципу подстановки (см. главу 2) указатель на объект производного класса являет-
ся и указателем на объект базового класса, поэтому указатель на именованную
заявку — объект производного класса — можно смело заносить в элемент списка.
Конкретизируем рассуждения, перечислив поля базового класса Tanker и произ-
водного класса Tanker4.
12.3.1. Класс Tanker
Неизменяемые поля данных:
О уникальный идентификатор объекта; можно назначить равным текущему
значению счетчика прибытий;
О тип танкера (1, 2 или 3);
О среднее значение времени обслуживания на погрузке;
О максимальное отклонение от среднего значения.
12.3. Классы и объекты 249
Изменяемые поля данных:
О время, проведенное в системе на текущий момент, начиная от постановки
в очередь к буксиру на причаливание;
О код текущего состояния (1 — в очереди на причаливание, штормит; 2 — в оче-
реди на причаливание, шторма нет; 3 — причаливание; 4 — в очереди на по-
грузку; 5 — погрузка; 6 — в очереди на отчаливание, штормит; 7 — в очереди
на отчаливание, шторма нет; 8 — отчаливание).
12.3.2. Производный класс Tanker4
Неизменяемые поля данных:
О среднее значение времени в пути на разгрузку и обратно (14 400 мин);
О максимальное отклонение от среднего значения (1440 мин);
О связь с объектом Буксир для посылки ему сообщения о своем прибытии.
Изменяемые поля данных:
О добавляется еще одно возможное значение кода текущего состояния: 9 — в пути
на разгрузку или обратно;
О время до прибытия на причаливание; поле данных имеет смысл лишь для со-
стояния 9.
По аналогии с предыдущими задачами буксир и порт должны быть объявлены
«друзьями» танкера. Интересный нюанс заключается в том, что дружествен-
ность нужно отдельно объявить и в производном классе Tanker4, так как по пра-
вилам C++ она не наследуется.
Довольно много полей данных приходится вводить для класса Буксир (Tug). Это
связано с тем, что буксир является связующим звеном между танкерами и пор-
том, а кроме того следует учитывать еще и влияние штормов.
12.3.3. Класс Tug
Неизменяемые поля данных:
О среднее значение интервала времени между прибытиями танкеров первых трех
типов (660 мин);
О максимальное отклонение от среднего значения (420 мин);
О длительность причаливания и отчаливания (60 мин);
О средняя продолжительность шторма (240 мин);
О максимальное отклонение от среднего значения (120 мин);
О параметр экспоненциального распределения для интервала времени между штор-
мами (0,021);
О указатель на объект класса Port для взаимодействия с ним.
Изменяемые поля данных:
О время до следующего прибытия танкера одного из трех типов;
О время до окончания причаливания;
250 Глава 12. Очереди с разнотипными заявками: работа порта
О время до окончания отчаливания;
О очередь танкеров на причаливание;
О очередь танкеров на отчаливание;
О причаливающий (отчаливающий) танкер;
О текущая длина очереди на причаливание (вычисляемое поле);
О текущая длина очереди на отчаливание (вычисляемое поле);
О время до начала следующего шторма;
О время до окончания шторма.
12.3.4. Класс Port
Класс Port моделируется как обычный многоканальный узел обслуживания
с общей очередью.
Неизменяемые поля:
О количество терминалов для погрузки (3);
О указатель на объект класса Tug.
Изменяемые поля:
О очередь танкеров на погрузку;
О массив указателей на обслуживаемые в данный момент танкеры;
О массив значений времени, оставшегося до окончания погрузки на каждом из
терминалов;
О текущая длина очереди (вычисляемое поле).
12.4. События и методы
Танкеры выполняют в системе роль заявок, поэтому они не имеют моделирую-
щих методов — все события, происходящие с ними, принимаются и обрабаты-
ваются объектами-серверами (листинги 12.2, 12.3). Для буксира можно выде-
лить следующие события и связанные с ними методы:
1. Начало шторма. Метод не имеет параметров.
2. Окончание шторма. Метод не имеет параметров.
3. Прибытие танкера четвертого типа на причаливание. Метод имеет параметр —
указатель на прибывший танкер.
4. Прибытие танкера одного из первых трех типов на причаливание. Метод не
имеет параметров.
5. Один из танкеров закончил погрузку и требует отчаливания. Метод имеет па
раметр — указатель на танкер.
6. Окончание отчаливания. Метод не имеет параметров, так как отчаливший
танкер доступен через поле данных самого буксира.
12.4. События и методы 251
7. Окончание причаливания. Метод не имеет параметров, так как причаливший
танкер доступен через поле данных самого буксира.
Коротко остановимся на особенностях некоторых методов. Методы 3 и 4 описы-
вают одно и то же событие, но их алгоритмические реализации различаются по
причине уже упоминавшегося существования различий между именованными и
неименованными заявками. В методе 4 необходимо создать новый временный
объект базового класса Tanker и разыграть время до прибытия следующего танке-
ра. В методе 3 этого делать не нужно, так как прибывший объект уже существует
в системе и доступ к нему мы получаем через передаваемый параметр. Эти два
метода могут иметь одно название, что допускается правилами C++, так как их
сигнатуры различаются. Конечно, методы 3 и 4 можно было бы объединить и в
один, передавая в одном из случаев NULL-указатель и осуществляя внутри соот-
ветствующую проверку параметра. Но такой подход скрывал бы принципиаль-
ные различия между обработкой двух вариантов прибытия танкеров, которые
здесь, наоборот, хотелось бы подчеркнуть.
В методе 5 в качестве параметра может быть передан указатель на танкер любого
типа — как указатель на объект базового класса.
Отметим, что финальной частью методов 2, 6 и 7 является одно и то же дейст-
вие — выбор в одной из очередей первого танкера и постановка его на обслужи-
вание. Этот общий фрагмент кода для исключения повторений удобно выделить
в отдельный метод, который мы назвали choiceO.
Для объекта Port событий всего два:
О прибытие очередного танкера. Метод имеет один параметр — указатель на
прибывший танкер — и вызывается буксиром из метода 7;
О завершение погрузки. Метод имеет один параметр — номер терминала, кото-
рый завершил погрузку, — и вызывает метод 5 для буксира.
Листинг 12.2. Реализация классов
#include<cstdio>
#include<cstdlib>
#include<ctime>
#include<cmath>
using namespace std;
#include "List.h” FILE *q_tugln; //файл для сбора статистики о длине очереди //на причаливание
FILE *q_tugOut: //файл для сбора статистики о длине очереди //на отчаливание
FILE *q_loading: //файл для сбора статистики о длине очереди //на погрузку
FILE *sojourn; //файл для сбора статистики о времени пребывания //в порту
float q_tug!nAve=O: //переменная для подсчета средней длины очереди //на причаливание
float q_tugOutAve=0: //переменная для подсчета средней длины очереди //на отчаливание
float q_loadAve-0: //переменная для подсчета средней длины очереди //на погрузку
252 Глава 12. Очереди с разнотипными заявками: работа порта
float soj_Ave=0: //переменная для подсчета среднего времени //пребывания на погрузке
float sojl_Ave-0: //переменная для подсчета среднего времени //пребывания на погрузке для танкеров первых //трех типов
float soj2_Ave=0; //переменная для подсчета среднего времени //пребывания на погрузке для танкеров //четвертого типа
long int ro_tug-0L; //переменная для подсчета загрузки буксира
float ro_port=0: //переменная для подсчета загрузки порта
long int entered=OL: //счетчик общего числа поступлений
long int completed=OL: //счетчик отчаливших танкеров
long int completed1-OL: //счетчик отчаливших танкеров первых трех типов
long int completed2=0L: //счетчик отчаливших танкеров четвертого типа
long int total: //базовый класс class Tanker //счетчик тактов модельного времени
{ public:
long int id: //идентификатор танкера
int type: //номер типа
int median: //среднее время погрузки
int offset: //максимальное отклонение
int minutes: //текущее время пребывания на погрузке
int state: friend class Tug: friend class Port: //текущее состояние
TankerO: //конструктор
//Метод PrintO удобно объявить виртуальным, например, для обхода любой из очередей //и распечатки ее содержимого, так как в очереди могут находиться танкеры любого типа
virtual void PrintO: ): //Производный класс
class Tanker4: public Tanker { const static int median_path-14400: //14400 минут=240 часов - среднее
//время обращения танкера четвертого //типа
const static int offset_path=1440; //24 часа - максимальное отклонение
//от среднего для времени обращения //танкера четвертого типа
int to_arrival; //время до прибытия пустого танкера на причаливание
void *t; public: friend class Tug: friend class Port: Tanker4(int i): void putTug(Tug *a): void run()://диспетчер virtual void PrintO: //связь с буксиром
}: //Класс Буксир class Tug {
Листинг 12.2. Реализация классов 253
const static Int arr_median=660; //660 минут=11 часов - среднее время между
//прибытиями танкеров первых трех типов
const static int arr_offset=420: //7 часов - максимальное отклонение //от среднего для интервалов между //прибытиями танкеров первых трех типов
const static int time_path=60; //1 час - длительность причаливания //и отчаливания
const static int storm_median=240: //4 часа - средняя длительность шторма
const static int storm_offset-120; //2 часа - максимальное отклонение //от среднего для длительности шторма
const static float storm_mu=0.021: //1/48. где 48 часов - средняя
//длительность «бесштормового» //интервала времени
int to_arrival: //время до прибытия танкера типов '
int to_in: //время до окончания причаливания
int to_out: //время до окончания отчаливания
ListNode<Tanker> *queue_in: //очередь на причаливание
ListNode<Tanker> *queue_out: //очередь на отчаливание
Tanker ‘serving: //обслуживаемый танкер
int que_inLength: //длина очереди на причаливание
int que_outLength; //длина очереди на отчаливание
int to_sStart; //время до начала шторма
int to_sEnd; //время до окончания шторма
void *p: //указатель на порт
public:
TugO;
void stormStartO; //начало шторма
void stormEndO: //окончание шторма
void Arrival_Sea(Tanker4 *t): //прибытие танкера четвертого типа
void Arrival_Sea(); //прибытие танкера типов 1,2.3
void Arrival_coast(Tanker *t): //танкер требует отчаливания
void DepartureO: //окончание отчаливания
void ArrivalO: //окончание причаливания
void run(): //диспетчер
void putPort(Port *a):
void choiceO; //выбор танкера для обслуживания
void PrintO:
):
//Класс Порт
class Port
{
const static int volume=3:
ListNode<Tanker> *queue;
Tanker **serving;
int *to_serve;
int q_length:
void *t;
public:
PortO:
-PortO:
void Arrival(Tanker *a);
void Completednt 1);
void putTug(Tug *a):
void PrintO:
//очередь на погрузку
//загружаемые танкеры
//время до окончания погрузки
//длина очереди
//указатель на буксир
//прибытие танкера
//завершение погрузки
int FirstAvailO:
254 Глава 12. Очереди с разнотипными заявками: работа порта
int BusyO:
void run(): //диспетчер
};
Tanker::Tanker()
{
int r:
id=entered:
mi nutes-0;
//Разыгрывание типа танкера
r=rand()fclOO+l:
if (r>-25) type-1:
else if (r<-55) type-2:
else type-3:
switch(type)
{
case 1: median-1080: offset-120: break;
case 2: median-1440: offset-180: break:
case 3: median-2160: offset-240: break:
}
}
void Tanker:-.PrintO
{
switch(state)
{
case 1: printf("Танкер № fcld типа fcd находится в очереди на причаливание. 1Итормит\п".
id. type): break:
case 2: printf("Танкер № Xld типа fcd находится в очереди на причаливание. Шторма
нет\п". id. type); break:
case 3: printf("Танкер ft Xld типа fcd причаливает\п". id. type): break:
case 4: printf("Танкер ft fcld типа W находится в порту в очереди на погрузку\п". id.
type); break:
case 5: printf("Танкер ft fcld типа W грузится в порту\п". id. type): break:
case 6: printfCTaHKep ft Xld типа Xd находится в очереди на отчаливание. Штормит\п".
id. type): break:
case 7: printfCTaHKep Ik fcld типа fcd находится в очереди на отчаливание. Шторма
нет\п”. id. type): break;
case 8: printfCTaHKep ft fcld типа fcd отчаливает\п". id. type): break:
)
)
//Начальное состояние танкера четвертого типа - 9
Tanker4::Tanker4(int i)
{ i
id=i:
minutes=0:
state-9:
type-4:
to_arrival-get_uniform(median_path. offset_path):
median-1260;
offset=180:
)
void Tanker4::putTug(Tug *a)
{
Листинг 12.2. Реализация классов 255
t=a;
void Tanker4::Print()
{
switch(state)
{
case 1: printfC’TaHKep ft fcld типа Xd находится в очереди на причаливание. Штормит\п“
id. type): break:
case 2: printf("Танкер № Xld типа W находится в очереди на причаливание. Шторма
нет\п”. id. type): break;
case 3: printfC’TaHKep ft fcld типа Xd причаливает\п“. id. type): break:
case 4: printfC’TaHKep ft fcld типа Xd находится в порту в очереди на погрузку\п”. id.
type): break:
case 5: printfC’TaHKep № fcld типа Xd грузится в порту\п". id. type): break:
case 6: printfC’TaHKep ft fcid типа fcd находится в очереди на отчаливание. Штормит\п".
id. type): break:
case 7: printfC’TaHKep № fcld типа Xd находится в очереди на отчаливание. Шторма
нет\п“. id. type): break:
case 8: printfC’TaHKep ft fcld типа Xd отчаливает\п’’. id. type): break;
case 9: printfC’TaHKep ft fcld типа Xd находится в пути\п”. id. type): break:
)
)
void Tanker4::run()
4
if (state==9) to_arriva1--:
//Танкер прибыл из Великобритании и сообщает об этом буксиру
if (to_arriva1—0) ((Tug*)t)->Arrival_Sea(this):
}
//В начальном состоянии буксир свободен, очереди пусты
Tug::Tug()
{
to_arrival=get_uniform(arr_median. arr_offset):
serving-NULL:
to_in--l:
to_out=-l;
queue_i n-NULL;
qlieue_out=NULL:
que_1nLength-0:
que_outLength-0:
to_sSta rt-(i nt)(get_exp(stormjnu)*60):
if (to_sStart-=0) to_sStart-l:
to_sEnd--l:
)
void Tug::choice()
{
//Очередь на причаливание не пуста, ей - приоритет
if (que_1nLength>0)
{
to_in-time_path:
que_inLength--:
serving-queue_in->Data(): //голову очереди - на обслуживание
queue_in’=queue_in-<@062>Next(): //продвижение очереди
256 Глава 12. Очереди с разнотипными заявками: работа порта
//Заявок на причаливание нет, а на отчаливание - есть
else if (que_outLength>0)
{
to_out=time_path:
que_outLength--:
serving=queue_out->Data():
queue_out=queue_out->Next():
)
return:
}
void Tug::stormStart()
{
to_sStart--l:
to_sEnd=get_uniform(storni_niedian. storm_offset):
)
void Tug::stormEnd()
{
to_sEnd=-l;
to_sStart-(i nt)(get_exp(storm_mu)*60):
if (to_sStart“0) to_sStart=l:
choiceO:
)
void Tug::Arrival_Sea() //прибытие танкера типов 1.2.3
{
to_arrival=get_uniform(arr_median. arr_offset);
entered++:
Tanker *ptr-new TankerO: //создаем новый танкер
//Шторма нет. буксир свободен, танкер сразу идет на причаливание
1f ((to_sEnd=~-1)&&(servi ng-=NULL))
{
serving=ptr:
to_in=time_path:
serving->state=3:
return:
)
//Танкер ставится в очередь
’ que_inLength++:
ListNode<Tanker> *ptrl=new ListNode<Tanker>(ptr. NULL):
if (queuejn) queue_in=ptrl:
else ListAdd<Tanker>(queue_in. ptrl):
//Назначение танкеру номера состояния в зависимости от наличия шторма
if (to_sEnd>0) ptr->state=l:
else ptr->state=2;
return:
}
void Tug::Arrival_Sea(Tanker4 *t)
{
entered++:
t->to_arriva>-l;
i f ((to_sEnd-“-1)&&(servi ng—NULL))
{
Листинг 12.2. Реализация классов 257
serving=t:
to_in=time_path:
serving->state-3;
return:
}
que_inLength++:
ListNode<Tanker> *ptrl-new ListNode<Tanker>(t. NULL);
if (queue_in==NULL) queue_in-ptrl:
else ListAdd<Tanker>(queue_in. ptrl);
if (to_sEnd>0) t->state=l:
else t->state=2:
return:
)
void Tug::Arrival_coast(Tanker *t) //прибытие танкера на отчаливание
{
if ((to_sEnd—1)&&(serving==NULL)) //шторма нет. буксир свободен
{
servi ng<;
to_out=time_path:
serving->state=8;
return:
)
que_outLength++:
ListNode<Tanker> *ptrl=new ListNode<Tanker>(t. NULL):
if (queue_out==NULL) queue_out=ptrl:
else ListAdd<Tanker>(queue_out. ptrl):
if (to_sEnd>0) t->state-6:
else t->state=7;
return:
)
void Tug::Departure!)
{
to_out=-l:
//Фиксация времени пребывания в порту отбывающего танкера
fprintf(sojourn.'T3f\n". (float)serving->minutes/60);
comp!eted++:
//Пересчет среднего времени пребывания
soj_Ave=soj_Ave*(1-1.0/completed)+(f1oat)(servi ng->mi nutes)/completed;
//Отбывает танкер типов 1.2.3
if (serving->type<4)
{
completedl++:
sojl_Ave-sojl_Ave*(1-1.0/completedl)+(f1oat)(servi ng->minutes)/completedl:
//Объект для неименованной заявки удаляется из системы
delete serving:
)
else //отбывает танкер четвертого типа
{
completed2++:
soj 2_Ave=soj 2_Ave*(1-1.0/completed2)+(f1 oat)(serv i ng->mi nutes)/completed2;
serving->state=9:
//Отправляем танкер четвертого типа в Великобританию
258 Глава 12. Очереди с разнотипными заявками: работа порта
((Tanker4*)serving)->to_arrival-get_uniform(((Tanker4*)serving)->median_path.
((Tanker4*)serving)->offset_path):
//Сброс времени пребывания в порту
serving->minutes=O:
)
serving=NULL:
//Пока отчаливали, начался шторм. Буксир отдыхает
if (to_sEnd>0) return:
//Шторма нет. Выбираем следующий танкер на обслуживание
choiceO:
)
void Tug::Arrival()
{
to_in—1:
//Сообщаем в порт о прибытии танкера на погрузку
((Port*)p)->Arrival(serving);
serving=NULL:
//Пока причаливали, начался шторм. Буксир отдыхает
if (to_sEnd>0) return:
choiceO:
)
void Tug::run()
{
int k:
if (to_sStart>0) to_sStart--:
if (to_sStart“0) stormStartO:
if (to_sEnd>0) to_sEnd--:
if (to_sEnd==0) stormEndO:
if (to_arrival>0) to_arrival--:
if (to_arrival—0) Arrival_Sea():
if (to_in>0) to_in--:
if (to_in—0) ArrivalO:
if (to_out>0) to_out--:
if (to_out==0) DepartureO:
ListNode<Tanker> *ptr-queue_in:
//Инкремент времени пребывания для всех танкеров которые в данный момент
//контролирует буксир
while(ptrI-NULL)
{
ptr->Data()->minutes++:
ptr-ptr->Next():
}
ptr=queue_out:
while(ptr!=NULL)
{
ptr->Data()->mi nutes++;
ptr-ptr->Next():
}
if (serving!=NULL) serving->minutes++:
//Запись статистики - один раз в час
if (<total+l)X60“0)
{
k“(tota1+1)/60:
Листинг 12.2. Реализация классов 259
fprintf(q_tugln. "W\n”. que_inLength):
q_tugInAve-q_tugItiAve*(l-1.0/k)+( (float )que_inLength)/k;
fprintf(q_tugOut."£d\n". que_outLength);
q_tug0utAve-q_tug0utAve*(l-1.0/k)+((f1oat)que_outLength)/k:
)
if (serving!-NULL) ro_tug++:
}
void Tug::putPort(Port *a)
{
p-a:
)
void Tug:-.PrintO
{
if (to_sStart—1)
printf("Буксир не работает из-за шторна\п");
else if (to_1n>0)
printf("Буксир помогает причалить танкеру типа !6d\n". serving->type):
else if (to_out>0)
printf("Буксир помогает отчалить танкеру типа М\п". serving->type):
else
printf("Буксир простаивает, так как некого обслуживать\п"):
}
//Конструктор для класса Port
Port::PortO
{
int i;
queue=NULL;
serving=new Tanker *[volume]:
to_serve-new 1nt[volume];
for(i-0;i<volume:i++)
{
serving[i]-NULL:
to_serve[i]--l:
)
q_length-0:
)
//Деструктор для класса Port
Port::-PortО
{
delete[] to_serve:
delete [] serving;
)
void Port::Arrival(Tanker *t)
{
int 1:
//Проверяем, есть ли свободный терминал
i-FirstAvai1():
if (i!--l) //есть, сразу ставим танкер на погрузку
{
serving[i]-t:
to_serve[1]=get_uniform(t->median. t->offset):
serving[i]->state=5:
260 Глава 12. Очереди с разнотипными заявками: работа порта
)
else //нет. ставим танкер в очередь
{
q_length++:
ListNode<Tanker> *ptr=new L1stNode<Tanker>(t. NULL);
if (queue==NULL) queue=ptr;
else ListAdd<Tanker>(queue. ptr);
t->state=4;
)
)
void Port:: Compl eted nt 1)
{
//Отправляем загруженный танкер к буксиру
((Tug*)t)->Arrival_coast(serving[i]);
to_serve[i]=-l:
serving[i]-NULL;
if (queue-=NULL) return;
//Очередь не пуста, ставим на освободившийся терминал новый танкер
serving[i]=queue->Data();
to_serve[i]=get_uni form(serving[1]->medlan. servi ng[i]->offset);
serving[i]->state-5:
queue-queue->Next();
q_length--:
}
void Port::run()
{
int k:
//Проверка завершения обслуживания и инкремент времени пребывания для всех
//танкеров, находящихся под контролем порта
fordnt i-0;i<volume:i++)
{
if (to_serve[i]>0) { serving[i]->minutes++: to_serve[i]--; }
if (to_serve[i]“0) Completed):
)
ListNode<Tanker> *ptr=queue:
while(ptr!=NULL)
{
ptr->Data()->m1nutes++:
ptr=ptr->Next();
}
//Запись статистики - 1 раз в час
if ((total+l)«60“0)
{
k-(total+l)/60:
fprintf(q_loading."fcd\n". q_length);
q_loadAve=q_loadAve*(1-1.0/k)+((f1oat)q_length)/k:
ro_port-ro_port*(1-1.0/k)+((f1oat)Busy())/(k*volume);
)
}
void Port::PrintO
{
printfC'B очереди на погрузку находятся Id танкеров\п". q_length):
12.4. События и методы 261
printf("Заняты погрузкой £d терминалов\п". BusyO):
for(int i=0:i<volume:i++)
printf("fcd-й терминал обслуживает танкер типа $d\n". 1+1. serving[i]->type):
}
Int Port::FirstAvail()
{
for(1nt i=0;i<volume:1++)
If (serving[i]==NULL) return(i):
return(-l):
}
v
int Port::Busy() //вычисление текущего количества занятых терминалов
{
int k=0:
fordnt i=0:i<volume:i++)
if (serving[i]•-NULL) k++:
return(k):
}
void Port::putTug(Tug *a)
{
t-a:
}
Листинг 12.3. Функция main()
#define N 525600 //количество минут в году
#define М 5 //количество танкеров четвертого типа
^include "classesl2.h"
int main()
{
int i;
Tanker4 **mas:
//Создание объектов Буксир и Порт
Port port:
Tug tug:
//Настройка их взаимодействия
port.putTug(&tug):
tug.putPort(&port):
mas=new Tanker4 *[M]::
q_tugln=fopen("q_tugln". "wt"):
q_tugOut=fopen("q_tugOut”. "wt"):
q_loadi ng-fopen("q_loading". ”wt“):
sojourn=fopen("sojourn". "wt"):
srand((unsigned)time(O)):
//Инициализация танкеров четвертого типа и настройка их взаимодействия
//с буксиром
for(i=0:i<M;i++)
{
mas[i]=new Tanker4(i+1):
mas[i]->putTug(&tug):
}
//Основной цикл моделирования
f or(total=0L;total<N:total++)
262 Глава 12. Очереди с разнотипными заявками: работа порта
{
tug.runO:
port.runO:
for(i-0:i<M:i++)
mas[i]->run():
}
delete [] mas:
//Закрытие файлов сбора статистики
fclose(sojourn):
fclose(q_tugln): fclose(q_tugOut): fclose(q_loading):
//Вывод на печать результатов эксперимента
printfC Всего поступлений танкеров Hld\n". entered):
printf("Завершили цикп обслуживания в порту lld\n”. completed):
printf("H3 них танкеров типов 1,2.3 - lld\n". completedl):
printf("Из них танкеров четвертого типа £ld\n". completed2):
printf("Средняя длина очереди на причаливание £.3f\n". q_tug!nAve):
printf("Средняя длина очереди на отчаливание 1.3f\n". q_tugOutAve):
printf("Средняя длина очереди на погрузку 1.3f\n". q_loadAve):
printf("Среднее время пребывания на погрузке £.3f\n". soj_Ave/60):
printf("Среднее время пребывания на погрузке для танкеров типов 1.2,3 - £.3f\n".
sojl_Ave/60):
printf("Среднее время пребывания на погрузке для танкеров четвертого типа - 1.3f\n"
soj2_Ave/60);
printf("Коэффициент загрузки буксира - 1.3f\n". ((float)ro_tug)/total):
printf("Коэффициент загрузки порта - £.3f\n". ro_port):
}
12.5. Анализ результатов
При отсутствии танкеров четвертого типа моделирование дает следующие ре-
зультаты:
О всего поступлений в систему — 786 танкеров;
О из них обслужено — 784 танкера;
О средняя длина очереди на причаливание — 0,019 танкера;
О средняя длина очереди на отчаливание — 0,024 танкера;
О средняя длина очереди на погрузку — 0,01 танкера;
О среднее время пребывания на погрузке — 22 часа;
О коэффициент загрузки буксира — 0,18;
О коэффициент загрузки порта — 0,59.
Мы видим, что неиспользованные ресурсы системы довольно велики, и, видимо,
контракт заключить стоит. Убедимся в этом. При добавлении пяти танкеров чет-
вертого типа получаем:
О всего поступлений в систему — 946;
О из них завершили обслуживание — 944;
О из них танкеров типов 1, 2, 3 — 784;
О танкеров типа 4 — 160;
12.5. Анализ результатов 263
О средняя длина очереди на причаливание — 0,027;
О средняя длина очереди на отчаливание — 0,035;
О средняя длина очереди на погрузку — 0,16;
О среднее время пребывания на погрузке (для всех танкеров) — 23,68 ч;
О среднее время пребывания на погрузке для танкеров типов 1, 2, 3 — 23,23 ч;
О среднее время пребывания на погрузке для танкеров типа 4 — 25,88 ч;
О коэффициент загрузки буксира — 0,22;
О коэффициент загрузки порта — 0,72.
Показатели выросли, но остались в пределах нормы, перегрузок нигде не воз-
никло.
Интересно проследить, как меняются показатели работы системы при дальней-
шем увеличении количества М танкеров четвертого типа. Из графика, приведен-
ного на рис. 12.1, видно, что среднее время пребывания танкеров в порту 7^ уве-
личивается довольно медленно, пока М не превосходит 15. Далее 7^ начинает
увеличиваться стремительно и быстро выходит за разумные пределы. Следо-
вательно, заключать контракт на обслуживание более 15 танкеров нецелесо-
образно.
Количество танкеров четвертого типа
Рис. 12.1. Зависимость среднего времени пребывания в порту
от количества танкеров четвертого типа
264 Глава 12. Очереди с разнотипными заявками: работа порта
Количество танкеров четвертого типа
Рис. 12.2. Зависимость коэффициента загрузки буксира от количества танкеров четвертого типа
Количество танкеров четвертого типа
Рис. 12.3. Зависимость коэффициента загрузки порта от количества танкеров четвертого типа
Задания для самостоятельной работы 265
Из этого графика можно сделать еще один вывод. Время обслуживания танкера
буксиром фиксировано и равно 2 ч, что составляет в общем-то небольшую долю
для значения Гср. Значит, потенциальным узким местом системы является по-
грузка в порту, где танкеры проводят все оставшееся время. Этот вывод подтвер-
ждают графики, изображенные на рис. 12.2 и 12.3.
Так, из графика, приведенного на рис. 12.2, видно, что загрузка буксира при уве-
личении М стремится асимптотически к некоторой не очень большой величине,
равной приблизительно 0,3. А вот порт с ростом М быстро оказывается перегру-
женным, его загрузка стремится к единице.
Выводы
1. Различия в /Поведении именованных и неименованных заявок настолько
существенны, что их нужно представлять разными классами.
2. Для моделирования общей очереди разнотипных заявок удобно использовать
наследование и принцип подстановки.
3. Отношение дружественности классов не наследуется, поэтому его нужно явно
описывать для всех производных классов.
4. Наличие сопутствующих факторов, осложняющих моделирование, требует
ответа на вопрос — в полях данных каких классов эти факторы следует
учесть. В данной задаче это был класс, описывающий буксир.
5. Имитационное моделирование позволило ответить на поставленный в усло-
вии задачи вопрос и определить оптимальное количество танкеров четверто-
го типа.
Задания для самостоятельной работы
1. Постройте зависимости среднего времени пребывания танкера в порту и за-
грузки порта от количества погрузочных терминалов при М= 15. Сколько до-
полнительных терминалов придется построить, чтобы среднее время пребы-
вания в порту не превышало 22 ч?
2. Предположим, что за одну погрузку танкера четвертого типа дирекция порта
получает от судовладельца плату в размере С денежных единиц. За пребыва-
ние танкера в порту в течение времени Т > 22 дирекция платит штраф в раз-
мере К(Т — 22), где К — некоторый коэффициент пропорциональности. Рас-
считать средствами имитационного моделирования, на обслуживание какого
количества танкеров четвертого типа должен быть заключен контракт, чтобы
за 1 год получить максимальную прибыль.
Глава 13
Прерывание обслуживания
с возвратом в очередь:
моделирование работы
станка с поломками
Основные вопросы, рассматриваемые
в данной главе:
□ Гибкий подход к генерации
случайных чисел
□ Поломки и ремонт обслуживающих
устройств. Диаграмма переходов.
Соответствие между переходами
и событиями
□ Как учесть ранее накопленное время
обслуживания при возвращении
на дообслуживание?
□ Нужен ли еще один станок?
13.1. Описание системы 267
13.1. Описание системы
Схема процесса выполнения заданий на станке и поломок станка показана на
рис. 13.1. Задания поступают на станок в среднем 1 раз в час. Распределение вели-
чины интервала между ними экспоненциально. При нормальном режиме работы
задания выполняются в порядке их поступления. Время выполнения задания
нормально распределено с математическим ожиданием 0,5 ч и среднеквадратич-
ным отклонением 0,1. Перед выполнением задания производится наладка станка,
время которой распределено равномерно на интервале 0,2-0,5 ч. Задания, вы-
полненные на станке, направляются в другие отделы цеха и считаются покинув-
шими рассматриваемую систему. Станок время от времени ломается. Интервалы
между поломками распределены нормально с математическим ожиданием 20 ч и
среднеквадратичным отклонением 2 ч. При поломке выполняемое задание уда-
ляется со станка и помещается в начало очереди заданий к станку. Выполнение
задания возобновляется с того места, на котором оно было прервано. Когда ста-
нок ломается, начинается процесс устранения неисправности, который состоит
из трех фаз. Продолжительность каждой фазы распределена экспоненциально с
математическим ожиданием, равным 45 мин. Поскольку общая продолжитель-
ность устранения поломки является суммой независимых и одинаково распреде-
ленных случайных величин с одинаковыми параметрами, она имеет эрлангов-
ское распределение.
Поступление
задания
Ожидание обработки
Станок
Удаление
выполненного
задания
Поломка
станка
Возвращение
на обслуживание
задания,
прерванного
поломкой станка
Рис. 13.1. Схема работы станка с поломками
Работа станка анализируется в течение 500 ч для получения информации о за-
грузке станка и времени выполнения задания.
268 Глава 13. Прерывание обслуживания с возвратом в очередь
13.2. Модельное время
В качестве единицы модельного времени примем одну минуту. К генерации мно-
гочисленных случайных величин, входящих в описание системы, будем подхо-
дить избирательно. Чтобы генераторы случайных чисел не работали со слишком
большими или слишком маленькими параметрами и вследствие этого не теряли
точность при численном интегрировании и решении уравнений, будем в одних
случаях задавать параметры распределений в минутах, а в других — в часах, ум-
ножая во втором случае результат на 60 и округляя до ближайшего целого числа,
а именно:
О интенсивность входного потока — 1 заявка в час;
О время обслуживания: среднее — 30 мин, отклонение — 6 мин;
О время наладки станка: среднее — 21 мин, отклонение — 9 мин;
О время безаварийной работы станка после ремонта: среднее — 20 ч, отклоне-
ние — 2 ч. Уточним, что в него не входит то время, когда станок не занят об-
служиванием;
О время ремонта: средняя продолжительность фазы — 0,75 ч, отсюда параметр
распределения Эрланга равен 1,33.
13.3. Классы и объекты
Обобщим первоначальную задачу, предположив, что вместо одного станка у нас
есть несколько станков (многоканальный узел). Это обобщение необходимо для
того, чтобы выяснить в дальнейшем, как влияет количество станков на произво-
дительность системы. Предположим, что каждый станок обслуживается своими
собственными наладчиком и ремонтником, так что ни одному из станков не при-
ходится ожидать ремонта, когда он ему потребуется. Очередь единственная, и за-
явка попадает на первый свободный станок. Если после поломки станка сущест-
вует хотя бы один свободный и готовый к обслуживанию станок, заявка сразу же
переходит к нему, и обслуживание считается непрерывным. Тогда сформулиро-
ванная задача есть частный случай, когда число каналов (станков) равно едини-
це. Система является открытой, число заявок — переменное. Для моделирования
системы необходимо описать класс Станок (Machine), представленный одним по-
стоянным объектом, и класс Заявка (Client), представленный переменным чис-
лом объектов, которые по ходу моделирования порождаются в системе и уда-
ляются из нее. Поведение заявок в системе полностью моделируется станком,
поэтому класс Client собственного метода run() не имеет. Отметим существен-
ные особенности системы:
О прерывание обслуживания заявки и возвращение ее на дообслуживание осу-
ществляются с учетом полученного ранее обслуживания;
О усложненное расписание работы сервера (станка), необходимость учета нала-
док, поломок и ремонтов.
13.3. Классы и объекты 269
Упорядочим имеющиеся у нас сведения о работе станка. На рис. 13.2 приведена
диаграмма его состояний и возможные переходы. Каждый переход обозначен на-
званием метода класса Machine, который этот переход обрабатывает (см. 13.4).
Repair_End
Рис. 13.2. Диаграмма состояний и переходов станка
Изменяемые поля данных класса Machine должны каким-то образом обеспечи-
вать информацию о текущем состоянии объекта и о том, сколько времени оста-
лось до перехода в то или иное состояние. Объект класса Client при возвраще-
нии в очередь должен наряду со временем пребывания в системе сохранять
информацию об оставшемся времени дообслуживания, а также — в целях сбора
статистики — изменять значение признака, свидетельствующего о том, что в его
обслуживании был перерыв. Таким образом, класс Client имеет следующие поля
данных:
О уникальный идентификатор объекта, равный порядковому номеру поступив-
шей заявки;
О текущее время пребывания в системе;
О оставшееся до окончания обслуживания время. Поле данных имеет смысл
только для заявок, вернувшихся в очередь, и только на время их повторного
нахождения в ней. Для всех остальных заявок полагаем его равным -1;
О признак прерывания (false — непрерывное обслуживание, true — обслужива-
ние прерывалось). Заметим, что значение признака не переключается в еди-
ницу, если после поломки станка заявке сразу же удалось перейти на другой
свободный и готовый к обслуживанию станок.
Неизменяемые поля данных класса Machine:
О количество станков (1);
О интенсивность входного потока (1 заявка в час);
О среднее значение времени обслуживания (30 мин);
О среднеквадратичное отклонение времени обслуживания (6 мин);
270 Глава 13. Прерывание обслуживания с возвратом в очередь
О среднее значение времени наладки (21 мин);
О максимальное отклонение времени наладки от среднего (9 мин);
О среднее время безаварийной работы (20 ч);
О среднеквадратичное отклонение времени безаварийной работы (2 ч);
О интенсивность одного этапа ремонта (1,33 заявок в час);
О количество этапов ремонта (3);
Изменяемые поля данных класса Machine:
О очередь заявок на обслуживание (список указателей);
О массив указателей на обслуживаемые в текущий момент заявки;
О массив времен, остающихся до окончания обслуживания;
О массив времен, остающихся до окончания наладки;
О массив времен, остающихся до окончания безаварийной работы;
О массив времен, остающихся до окончания ремонта;
О время до прибытия следующей заявки из входного потока;
О текущая длина очереди (вычисляемое поле).
Отметим, что отличными от -1 в некоторых состояниях могут быть сразу два из-
меняемых поля данных станка — время до окончания обслуживания и время до
окончания безаварийной работы.
13.4. События и методы
В системе с каждым из станков могут происходить следующие события:
О поломка станка (Breakage);
О завершение ремонта (Repalr End);
О завершение наладки (Set End);
О завершение обслуживания (Complete);
О прибытие новой заявки (Arrival).
Снова обратившись к рис. 13.2, отметим интересную особенность: между множе-
ствами переходов и событий существует связь «многие к одному» — одному пе-
реходу соответствует одно событие, но одному событию могут соответствовать
несколько переходов (листинг 13.1). Перечисленные пять методов программиру-
ются по текстовому описанию системы. Остановимся на некоторых нюансах:
О декремент оставшегося времени безаварийной работы следует производить
только в том случае, если станок занят обслуживанием. Разыгрывать это время
следует при обработке события «Завершение ремонта» (в методе Repair EndO);
О при выборке заявки из очереди время ее обслуживания следует разыграть,
если это первый сеанс обслуживания, и взять из поля данных заявки — в про-
тивном случае (если это поле отлично от -1);
13.4. События и методы 271
О необходимо предусмотреть возможность наложения событий. В частности,
как поступить, если время безаварийной работы и время обслуживания исте-
кут одновременно? С точки зрения логики, станок не может сломаться после
того, как он закончил работать, поломка должна произойти раньше. Поэтому
в методе гип() (листинг 13.2) мы сначала фиксируем события поломок (тогда
заявка уходит в очередь с временем дообслуживания, равным единице), а за-
тем уже для остальных станков производим декременты остаточного времени
пребывания в текущем состоянии.
Листинг 13.1. Реализация классов
#include<cstdio>
#1nclude<cstdlib>
#include<ctime> #include<cmath> using namespace std: #include "List.h" #include "erlang.h" #include "normal.h“ FILE *que; //файл для сбора статистики о длине очереди
FILE *sojourn; //файл для сбора статистики о времени пребывания
int entered-0; //в системе //счетчик поступлений
int completed-0: //счетчик обслуженных заявок
int completedl-0: //счетчик заявок, не возвращавшихся в очередь
int completed2-0: //счетчик заявок, возвращавшихся в очередь
float ro_ave-0; //переменная для подсчета коэффициента загрузки
float que_ave=0: //станков //переменная для подсчета средней длины очереди
float soj_ave=0: //переменная для подсчета среднего времени пребывания
long int total; //в системе //счетчик модельного времени
class Client { int id: //уникальный идентификатор
int time: //текущее время пребывания в системе
int to_serve: //остаточное время обслуживания
int interrupt; //признак возврата в очередь
protected: static int counter: //счетчик заявок
public: friend class Machine; Client!); }: int Client::counter-0: //инициализация статического поля данных вне класса
Client::C1ient() //конструктор
counter++:
id-counter;
tlme-0:
interrupt-0:
to_serve—1:
}
272 Глава 13. Прерывание обслуживания с возвратом в очередь
class Machine
static
static
static
static
static
static
static
static
static
static
const
const
const
const
const
const
const
const
const
const
ListNode<C11ent> *queue;
Client **serving;
int
int
int
int
int
int
public:
Machined:
-Machined:
void Arrivald:
void
void
void
void
int Busyd:
int FirstAvaild;
void rund:
*to_serve:
*to_setting;
*to_break:
*to_repair:
toarrival:
q_length:
int volume=l:
float input_rate=l;
float serve_median=30:
float serve_offset=6;
int set_median=21:
int set_offset=9:
float break_median=20:
float break_offset=2:
float repair_rate=l.33;
int repair_stages-3:
//очередь
//обслуживаемые заявки
//текущее время до окончания обслуживания
//текущее время до окончания наладки
//оставшееся время безаварийной работы
//текущее время до окончания ремонта
//время до прибытия новой заявки
//текущая длина очереди
Completed nt i);
Breakaged nt i):
Repair_End(int 1):
Set Enddnt i):
//Поиск доступного станка.
//в состоянии простоя
int Machine::FirstAvai1()
Доступным считается станок, находящийся
fordnt i=0;i<volume:i++)
if ((serving[i]==NULL)&&(to_repair[1]==-l)&&(to_setting[1]==-l))
returnd);
return(-l):
int Machine::Busy()
//подсчет количества занятых станков
int k=0:
ford nt i-0:i<volume:i++)
if (serving[i]!=NULL) k++:
return(k):
//Конструктор. Исходное состояние - простой
Machine:: Machined
queue=NULL:
//Выделение памяти под массивы
Листинг 13.1. Реализация классов 273
serving=new Client *[volume]:
to_serve=new intEvolume];
to_setti ng=new i nt[volume]:
to_break- new intEvolume];
to_repair= new intEvolume]:
to_a rri va1-(i nt)(get_exp(i nput_rate)*60):
If (to_arr1val“0) to_arrival=l:
//Инициализация массивов
ford nt i=0:i<volume:i++)
{
to_serveEi]=-l:
to_settingEi]“-l;
to_breakE1]=(1 nt)(get_normal(breakjnedian. break_offset. 0.001)*60):
if (to_breakEi]“0) to_breakEi]=l;
to_repair[i]=-l:
servingE1]“NULL:
}
q_length-0:
}
Machine::-Machined
{
fordnt i-0:i<volume:i++)
if (servingEi]!=NULL) delete servIngEI]:
delete E] serving:
deleteE] to_serve;
deleteE] to_setting:
deleteE] to_break:
deleteE] to_repa1r:
whi1e(queue) queue=ListDelete<Client>(queue.queue):
}
void Machine::Arrival() //поступление новой заявки
{
int k:
Client *ptr=NULL:
L1stNode<C11ent> *ptrl=NULL:
//Разыгрываем новую длительность интервала между поступлениями
to_arri va1-(1 nt)(get_exp(i nput_rate)*60):
if (to_arrival==0) to_arrival=l;
entered++: //инкремент счетчика поступлений
ptr-new ClientO: //создание новой заявки
k=FirstAvail():
if (k==-l) //поставить поступившую заявку на обслуживание
//невозможно
{
ptrl=new ListNode<Client>(ptr. NULL):
if (queue==NULL) queue-ptrl:
else ListAdd<Client>(queue, ptrl);
q_length++:
}
else //новую заявку сразу ставим на обслуживание
//на k-й станок
{
servingEk]=ptr:
to_serveEk]-(int)get_normal(servejnedian. serve_offset. 0.001);
if (to_serveEk]==0) to_serveEk]“l;
274 Глава 13. Прерывание обслуживания с возвратом в очередь
}
return:
}
void Machine:Complete(int i) //завершение обслуживания на i-м станке
{
completed++:
to_serve[1]“-l;
if (serving[i]->interrupt”O) completedl++:
else completed2++:
//запись статистики
fprintf(sojourn. 'X3f\n". ((float)serving[1 ]->time)/60):
soj_ave-soj_ave*(1-1.0/completed)+(float)(serving[i]->time)/completed:
delete serving[i];
serving[i]=NULL;
//Станок переходит в состояние наладки
to_setti ng[i]=get_uniform(setjnedi an. set_offset):
return;
}
void Machine:: Breakages nt i) //поломка i-го станка
{
int k:
k-FirstAvailO:
if (k“-l) //свободного станка нет. заявка
//переходит в очередь
{
serving[i]->to_serve=to_serve[i]: //сохраняем время дообслуживания
//в поле данных заявки
servi ng[i]->interrupt-1:
//Заявка «от станка» заносится в голову очереди
queue=new ListNode<Client>(serving[i].queue):
q_length++:
}
else //свободный станок найден
{
serving[k]-serving[i]:
to_serve[k]-to_serve[i]:
}
//Вышедший из строя станок переходит в состояние ремонта
serving[i]-NULL;
to_serve[i]--l:
to_repair[i]=(int)(get_erlang(repair_rate. repair_stages,0.001)*60):
if (to_repair[i]--0) to_repair[i]-l:
to_break[i]--l:
return:
}
void Machine:;Repair_End(int i) //завершение ремонта i-го станка
{
to_repair[i]--l:
if (q_length--0) return: //очередь пуста, ставить на обслуживание
//нечего
//Очередь не пуста, заявку из головы очереди ставим на отремонтированный станок
servi ng[i]-queue->Data():
13.4. События и методы 275
if (serving[i]->to_serve>0) //эта заявка ранее уже обслуживалась
{
to_serve[i]-servi ng[i]->to_serve:
serving[1]->to_serve*-l:
}
else to_serve[i]“(int)get_normal(serve_median. serve_offset. 0.001):
if (to_serve[1]—0) to_serve[i]-l:
queue=queue->Next(): //сдвиг очереди
qjength--;
//Разыгрываем новое время безаварийной работы
to_break[i]-(i nt)(get_normal(breakjnedi an. break_offset. 0.001)*60);
if (to_break[i]“0) to_break[i]-l:
}
void Machine::Set_End(int i) //завершение наладки i-го станка
//Код этого метода такой же. как и код предыдущего, только время
//безаварийной работы не разыгрывается
{
to_setting[i]—1:
if (q_length--0) return:
servi ng[i]-queue->Data():
if (serving[i]->to_serve>0)
{
to_serve[i]-serving[1]->to_serve; serving[i]-> to_serve--l;
}
else to_serve[i]-(int)get_normal(servejnedian. serve_offset.0.001):
if (to_serve[i]=-0) to_serve[i]-=l;
qjength--:
queue=queue->Next():
}
void Machine::run() //диспетчер
{
int i:
ListNode<Client> *ptr=NULL;
//Фиксируем сломавшиеся станки
for(i=0;i<volume: 1++)
{
if (serving[i]!-NULL) to_break[i]--;
if (to_break[i]“0) Breakage(i):
}
if (to_arrival>0) to_arrival--:
if (to_arrival-=0) ArrivalO:
//Осуществляем, если нужно, переходы станков в новые состояния
for(i-0:i<volume: i++)
{
if (serv1ng[i]!-NULL) to_serve[i]--:
if (to_serve[i]—0) Completed);
if (to_setting[i]>0) to_setting[i]--:
if (to_setting[i]--0) Set_End(i):
if (to_repair[i]>0) to_repair[i]--:
if (to_repair[i]—0) Repair_End(i);
}
//Инкремент времени пребывания у всех заявок, находящихся в системе
//...в очереди
276 Глава 13. Прерывание обслуживания с возвратом в очередь
ptr=queue:
while(ptr)
{
ptr->Data()->time++:
ptr“ptr->Next():
}
//...и обслуживающихся
for(i=0;1<volume:1++)
If (serving[1]!=NULL) serv1ng[1]->time++:
//Запись статистики
fprintf(que. "fcd\n". qjength);
que_ave=que_ave*(1-1.0/(total+l))+((fl oat)q_length)/(total+l);
ro_ave=ro_ave*(l-1.0/(total+l))+((float)Busy())/(volume*(total+l));
}
Листинг 13.2. Функция main()
#define N 30000 //время моделирования
#include "classesl3.h"
Int malnO
{
int i;
Machine m:
que=fopen("que". "wt”);
sojourn-fopen("sojourn”. “wt”):
srand((unsigned)time(O)):
for(total=0L:total<N;total++)
m.runO:
fclose(sojourn):
fclose(que):
printfC Всего поступило заявок fcd\n”. entered):
printf("Всего завершило обслуживание £d заявок\п". completed):
printfC'flonB заявок, не прерывавших обслуживания fc.3f\n".
((float)completedl)/completed);
printfCflonB заявок, прервавших обслуживание fc.3f\n". ((float)completed2)/completed):
printf("Средняя длина очереди fc.3f\n". que_ave):
printft"Среднее время пребывания 8.3f\n". soj_ave/60):
printf("Коэффициент загрузки станков ГЗАп". ro_ave):
I
13.5. Анализ результатов
Моделирование системы при заданных условиях (1 станок, 500 ч) дало следу-
ющие результаты:
О количество выполненных заданий — 500;
О доля заданий, не возвращавшихся в очередь, — 0,977;
О доля заданий, возвращавшихся в очередь, — 0,023;
О средняя длина очереди — 3,654;
О среднее время пребывания в системе — 4 ч:
О коэффициент загрузки станка — 0,5.
13.5. Анализ результатов 2.11
На рис. 13.3 и 13.4 приведены примеры реализаций случайной величины и слу-
чайной функции — соответственно, времени пребывания заявок в системе и длины
очереди к станку. Периодические всплески на этих графиках связаны с поломка-
ми станка. В самом деле, если обратиться к рис. 13.4 и провести горизонтальную
линию приблизительно на уровне шести (напомним, что среднее значение — 3.65),
то зафиксируем приблизительно 11-12 всплесков, доходящих до этого уровня.
Количество поломок на протяжении 500 часов также устойчиво показывает при
имитационных экспериментах значение 11-12.
Мы видим, что при среднем времени обслуживания 30 мин заявка проводит
в системе в среднем довольно много времени — 4 ч. Насколько улучшится ситуа-
ция, если добавить второй станок? Вот результаты:
О средняя длина очереди — 0,115;
О среднее время пребывания заявки в системе — 0,6 ч (36 мин);
О коэффициент загрузки станков — 0,24.
Таким образом, добавление второго станка полностью решает проблемы — сред-
нее время пребывания заявки в системе становится практически равным средне-
му времени обслуживания. Поэтому добавлять третий станок не имеет смысла.
Номер обслуженной заявки
Рис. 13.3. Пример реализации случайной величины — среднего времени пребывания
заявки в системе
278 Глава 13. Прерывание обслуживания с возвратом в очередь
Выводы
1. При генерации нескольких последовательностей случайных величин полезно
для повышения точности использовать гибкое масштабирование времени, за-
висящее от функции распределения и диапазона возможных значений. После
генерации все величины приводятся к единице времени, принятой в модели-
рующей программе.
2. При наличии прерываний необходимы дополнительные поля данных в клас-
се, представляющем заявки.
3. Объект, представляющий обслуживающее устройство, принимает в рабочем
состоянии два потока — заявок и поломок. При одновременном наложении
событий из обоих потоков порядок их обработки определяется из логики ра-
боты системы.
4. Соответствие между множествами событий и переходов может не быть вза-
имно-однозначным.
5. Имитационное моделирование позволило установить целесообразность вве-
дения в действие второго станка. Добавление третьего и последующих стан-
ков уже ничего не дает.
Задания для самостоятельной работы 279
Задания для самостоятельной работы
1. Измените программу таким образом, чтобы в случае поломки станка обслу-
живание заявки однозначно прерывалось и могло быть продолжено только на
этом же станке после его ремонта. Как изменятся результаты, если будут ра-
ботать два станка?
2. Измените программу таким образом, чтобы учесть разную производитель-
ность станков. Проблема заключается в том, что в случае перехода с одного
станка на другой длительность дообслуживания заявки следует пересчитать
в соответствии с производительностью этого станка.
3. Измените программу, предположив, что накопленное время обслуживания
заявки не сохраняется, то есть в случае поломки станка ее обслуживание на
другом или на том же самом станке не возобновляется с прерванного места,
а начинается заново. Как изменятся результаты для одного и двух станков?
4. Предположите, что все станки обслуживает одна только бригада наладчиков
и одна бригада ремонтников, то есть два станка не могут одновременно нала-
живаться или ремонтироваться. Это означает, что ремонт станков выполня-
ется последовательно: ремонт следующего сломавшегося станка начинается
только после окончания ремонта предыдущего. Как изменятся результаты
моделирования для двух станков?
5. Предположим, что установить второй станок нет возможности. Постройте
зависимости среднего времени пребывания заявки в системе от различных
входных параметров (среднее время обслуживания, наладки, ремонта, безава-
рийной работы) и выясните, улучшение какого из них скажется на этом пока-
зателе наиболее эффективно. Этот результат покажет, в каком направлении
инженерам следует совершенствовать технологический процесс обработки
деталей на станке.
Глава 14
Сетевое планирование
и анализ проектов
Основные вопросы, рассматриваемые
в данной главе:
□ Треугольное распределение
и генератор случайных чисел для него
□ Как моделировать задачу сетевого
планирования
□ Что такое стохастический критический
путь
□ Что такое значимость работы
□ Решение эталонной задачи
и сравнение результатов
14.1. Описание системы 281
14.1. Описание системы
Сетевая модель проекта замены и наладки оборудования представлена на рис. 14.1,
а в табл. 14.1 приведены описания работ проекта. Предполагается, что продол-
жительности всех работ имеют треугольное распределение (см. 14.2). Термин
«мода», являющийся заголовком одного из столбцов табл. 14.1, означает точку
максимума функции, задающей плотность распределения случайной величины.
Эти работы относятся к силовому оборудованию, инструменту или вновь уста-
навливаемому оборудованию и состоят из стандартных операций.
Рис. 14.1. Сетевая модель проекта замены и наладки оборудования
На рисунке в квадратах приведены номера работ. В начале проекта могут парал-
лельно осуществляться следующие три вида работ:
О разборка силового оборудования и оснащение инструментами ( /);
О освоение нового монтажа (2);
О подготовка к проверке качества наладки (3).
Чистка, проверка и ремонт силового оборудования (4) и калибровка инструмен-
та (5) выполняются только после разборки силового оборудования и инструмента.
Таким образом, работы 4 и 5 в сети должны следовать после работы 1. После освое-
ния нового монтажа (2) и калибровки инструмента (5) можно проверять контакты
(6) и правильность сборки (7). Проверить наладку {9) можно только после про-
верки правильности сборки (7) и завершения подготовки к проверке наладки (3).
Сборка и проверка силового оборудования (S) может быть проведена после чист-
ки, проверки и ремонта силового оборудования (4).
Проект считается завершенным, когда все девять работ завершены. Поскольку
работы 6, 8 и 9 могут быть выполнены только после выполнения всех остальных,
их завершение означает окончание выполнения проекта. В сети (см. рис. 14.1)
это условие отображается с помощью узла 6, в котором сходятся работы 6, 8 и 9.
282 Глава 14. Сетевое планирование и анализ проектов
Таблица 14.1. Перечень и продолжительность работ (в условных единицах времени)
Номер работы Описание Мода Минимум Максимум Среднее
1 Разборка силового оборудования и оснащение инструментами 3 1 5 3
2 Освоение нового монтажа 6 3 9 6
3 Подготовка к проверке 13 10 19 14
4 Чистка, проверка и ремонт силового оборудования 9 3 12 8
5 Калибровка инструмента 3 1 8 4
6 Проверка контактов 9 8 16 И
7 Проверка правильности сборки 7 4 13 8
8 Сборка и проверка силового оборудования 6 3 9 6
9 Проверка наладки 3 1 8 4
14.2. Модельное время
В этой задаче мы впервые столкнулись с треугольным распределением длитель-
ности случайного интервала времени. Разберем его несколько подробнее. Функ-
ция распределения имеет вид
F(x) =
(х-Л)2
(в-лхс-л)’ х< ’
Л(В-0+В(С-2х) + х2
(Л-ВХВ-С)
где А <х< В, А < С < В. Математическое ожидание равно (Л + В + 0/3. Пара-
метр Л соответствует столбцу «Минимум» из табл. 14.1 характеристик работ, па-
раметр В — столбцу «Максимум», параметр С — столбцу «Мода». График функ-
ции плотности распределения /(х) при А = -4, В = 4, С = 2 приведен на рис. 14.2.
Интервал (Л; В) является областью определения случайной величины, описы-
ваемой треугольным распределением, С — точкой максимума функции /(х).
Уравнение В(х) =р имеет простое аналитическое решение, поэтому треугольное
распределение относится к первой группе распределений, согласно классифика-
ции, приведенной в главе 3. Это решение имеет вид
л+7р(В-лхс-л),
В-А
в-7(1-р)(в-л)(в-о,
В-А
14.2. Модельное время 283
Рис. 14.2. Функция/(х) треугольного распределения, А - -4, В - 4, С - 2
Подставляем его в код ГСЧ для треугольного распределения (листинг 14.1).
Листинг 14.1. Генератор случайных чисел для треугольного распределения
float getJriangleCfloat A. float В. float С)
{
int r_num: float root, right:
r_num-rand(): //получение случайного целого
//числа
right-((float)r_num)/(RAND_MAX+l); //проекция на интервал (0:1).
//Константа RAND_MAX-32767 (215-1) определена в cstdlib
if (right<(C-A)/(B-A)) root-A+sqrt(right*(B-A)*(C-A)):
else root-B-sqrt((l-right)*(B-A)*(B-C));
return(root):
}
Единица измерения времени в условии задачи не указана, поэтому мы можем
применить масштабирование времени с произвольным коэффициентом (так как,
согласно табл. 14.1, минимальное время равно единице, достаточно будет при-
нять его, например, равным 100). Параметры ГСЧ оставляем такими же, как
в условии задачи, результат умножаем на коэффициент и округляем до целого
числа. В конце эксперимента при выводе результатов все величины, относящие-
ся ко времени, делим на коэффициент масштабирования, чтобы представить ре-
зультаты в исходных единицах времени.
284 Глава 14. Сетевое планирование и анализ проектов
14.3. Классы и объекты
Данная задача относится не к теории очередей, а к сетевому планированию.
В ней отсутствуют понятия заявки, сервера, очереди и все связанные с ними.
Если во всех предыдущих задачах общая длительность эксперимента выбира-
лась заранее, то здесь она является вычисляемой величиной, получение которой
как раз и составляет основную цель моделирования, — это время завершения вы-
полнения всех работ. Каждая работа выполняется однократно, и условием нача-
ла ее выполнения является завершение некоторой совокупности других работ,
различной для каждой работы.
Рассмотрим следующую логику моделирования. Введем класс Работа (Work), ко-
торый будет представлен в программе девятью объектами. Объект класса Work
может находиться в трех состояниях: до выполнения, в процессе выполнения,
после выполнения. Если объект находится в процессе выполнения, необходимо
вести учет времени, оставшегося до завершения работы. Если объект находится
в состоянии до начала выполнения, для него необходимо поддерживать перечень
номеров работ, завершения которых он должен дождаться, чтобы начать выпол-
няться самому. Объекту также нужно знать, какие работы ожидают завершения
его выполнения, для того чтобы разослать им соответствующее сообщение, как
только это произойдет. В состоянии после выполнения никакой информации для
объекта поддерживать уже не требуется. Итак, выпишем поля данных объекта
Work.
Неизменяемые поля данных:
О уникальный номер объекта (работы);
О минимум, максимум и мода треугольного распределения длительности выпол-
нения объекта (работы);
О наименование объекта (работы);
О выходной список — перечень номеров объектов (работ), которым объект рас-
сылает сообщения о том, что завершил выполнение. Соответствует меткам
стрелок, выходящим из той вершины, в которую входит стрелка, помеченная
номером объекта (работы);
О некоторый указатель (адрес массива объектов), по которому можно вычислить
адреса объектов — получателей сообщения о завершении выполнения работы.
Изменяемые поля данных:
О входной список — перечень номеров объектов (работ), завершения которых
объекту осталось дождаться, чтобы перейти в состояние выполнения. Когда
список становится пустым, объект начинает выполняться. Входной список
инициализируется метками стрелок, входящими в ту вершину, из которой
выходит стрелка, помеченная номером объекта (работы);
О время, оставшееся до завершения выполнения. В других состояниях значение
этого поля данных равно -1.
Выпишем начальные входные списки и выходные списки для всех девяти работ
(табл. 14.2).
14.4. События и методы 285
Таблица 14.2. Начальные входные и выходные списки работ
Номер работы Входной список Выходной список
1 NULL 4,5
2 NULL 6,7
3 NULL 9
4 1 8
5 1 6,7
6 2.5 NULL
7 2,5 9
8 4 NULL
9 3,7 NULL
Хранение списков реализуем в виде массивов, длина которых равна количеству
работ. Признаком вхождения числа i (1 < i < 9) в список является равенство еди-
нице i-го элемента массива.
14.4. События и методы
С каждым из объектов-работ могут произойти два события, приводящие к изме-
нению его состояния, а именно:
1. Поступление сообщения о завершении выполнения от объекта из входного
списка.
2. Завершение выполнения самого объекта.
На первое событие объект реагирует так: исключает выполнившийся объект из
своего текущего входного списка и проверяет, не стал ли он после этого пустым.
Если стал, разыгрывает время своего выполнения и начинает выполняться. Ре-
акция на второе событие состоит в рассылке сообщения всем объектам, чьи но-
мера входят в выходной список. Рассылка — это вызов для данных объектов-ра-
бот метода, соответствующего первому событию, с параметром, равным номеру
объекта-отправителя.
Далее приведен листинг программы с комментариями (листинги 14.2, 14.3). При
его анализе следует помнить, что массивы нумеруются с нуля, поэтому номера
объектов (работ) при индексации массивов смещены на единицу вниз — от нуля
до восьми.
Листинг 14.2. Файл classesl4.h с реализацией класса Work
#1nclude<cstd1o>
#1nclude<cstdl1b>
#lnclude<ctime>
#include<cmath>
#1nclude<cstrlng>
286 Глава 14. Сетевое планирование и анализ проектов
using namespace std:
#define N 9
#define RATIO 100
float timeBegin[N];
float timeEnd[N]:
long int total-OL:
Int completed-0:
//количество объектов
//коэффициент масштабирования времени
//массив для хранения времен начала работ
//массив для хранения времен окончаний работ
//счетчик текущего времени моделирования
//счетчик количества выполненных работ
class Work
{
Int Id: //номер работы
//Параметры треугольного распределения времени выполнения
float Minimum:
float Maximum:
float Moda:
char *name: //название работы
int *input; //входной список
int *output: //выходной список
int time: //время до завершения работы
Work **ptr: //массив указателей на объекты
public:
//Конструктор
Workdnt a. char *b. int *с. int *d. float fl. float f2. float f3. Work **p):
-WorkO: //деструктор
void WorkFinishdnt а): //объект с номером а завершил выполнение
void Completed: //данный объект завершил выполнение
void rund: //диспетчер
void PrintO:
}:
//Метод-конструктор
Work::Work(int a. char *b. int *c. int *d. float fl. float f2. float f3. Work **p)
{
int i:
id=a:
name=new char[strlen(b)+l]:
strcpy(name.b):
if (c!-NULL)
{
input-new int[NJ:
for(1-0;i<N:i++)
1nput[i]=c[i];
time--l:
else
//входной список не пуст
//выделение памяти
//инициализация входного
//списка
//объект не выполняется
//входной список пуст.
//объект сразу начинает
//выполняться
{
time=(int)(get_triangle(fl. f2. f3)*RATI0): //разыгрываем время
if (t1me--0) time-1:
i nput-NULL:
timeBegin[id-l]=O: //фиксируем время начала
//выполнения
Листинг 14.2. Файл classesl4.h с реализацией класса Work 287
if (d!-NULL) //заполнение выходного
//списка, если он не пуст
{
output-new int[N],
for(i-0;i<N:i++)
Output[i]-d[i]:
}
else output=NULL:
Minimum-fl:
Maximum-f2:
Moda-f3;
ptr-p:
}
//Метод-деструктор
Work::-Work()
{
delete [] name:
if (input) delete [] input:
if (output) delete [] output:
}
//Сообщение о завершении работы из входного списка
void Work::WorkFinish(int а)
{
int 1:
1nput[a-l]=0: //исключаем работу из входного списка
for(i-0:i<N;i++) //проверяем, стал ли он после этого пустым
if (input[i]!-0) break:
if (1—N) //да. начинаем выполнение
{
time-(int)(get_triangle(Minimum. Maximum. Moda)*RATI0):
if (time—0) time-1:
timeBegin[id-l]-total:
}
}
//Завершение выполнения
void Work::Complete()
{
int 1:
time--l:
timeEnd[id-l]-total: //фиксируем время завершения
completed++: //инкремент счетчика выполненных работ
if (output!-NULL) //выходной список не пуст, делаем рассылку
{
for(i-0; 1<N: i++)
if (outputs]!-О) ptr[i]->WorkFinish(id): //объект j+1 получает
//сообщение о том. что
//выполнение объекта id
//завершено
}
//Метод-диспетчер
288 Глава 14. Сетевое планирование и анализ проектов
void Work::run()
{
if (time>0) time--:
if (time=“0) CompleteO:
}
//Печать состояния объекта
void Work::PrintO
{
Int 1:
if (time=-l) if (input==NULL)
{
printf("Работа Xd (Xs) уже завершена\п". id. name):
}
else
{
for(i=0:i<N:1++)
if (input[i]!“0) break;
if (i==N) printf(“Работа Xd (Xs) уже завершена\п". id. name);
else printf("Работа Xd (Xs) еще не начиналась\п". id. name):
}
else printfCРабота Xd (Xs) выполняется\п”. id. name):
}
Листинг 14.3. Функция main()
#include “classesl4.h"
Int mainO
{
Int 1;
void zero(int *nl. int *n2):
//Подготовка данных для конструктора
Work **mas-new work *[N]:
int *ml-new int[N]:
int *m2-new int[N]:
for (i-0;i<N:i++)
{
ml[i]=0:
m2[i]=0:
}
srand((unsigned)time(O)):
char s[100]:
//Создание девяти объектов, каждого - со своей «начинкой»
т2[3]-1; т2[4]-1:
strcpy(s."Разборка силового оборудования и оснащение инструментами"):
mas[0]-new Work(l.s.NULL.m2. 1.5.3.mas):
zero(ml. m2):
m2[5]=l: m2[6]-l:
strcpy(s."Освоение нового монтажа"):
mas[l]=new Work(2.s.NULL.m2. 3.9.6.mas):
zero(ml. m2):
m2[8]=l;
strcpy(s."Подготовка к проверке наладки");
mas[2]=new Work(3.s.NULL.m2. 10.19.13.mas):
zero(ml. m2):
Листинг 14.3. Функция main() 289
m2[7]=l: ; mlEOJ-1:
strcpyls."Чистка. проверка и ремонт силового оборудования”):
mas[3]-new Work(4.s.ml.m2. 3.12.9.mas):
zero(ml. m2):
m2[5]-l: m2[6]-l: ml[0]=l:
strcpy(s.“Калибровка инструмента"):
mas[4]-new Work(5.s.ml.m2. 1.8.3.mas):
zero(ml. m2):
ml[l]=l; ml[4]-l:
strcpyls."Проверка контактов"):
mas[5]=new Work(6.s.ml.NULL. 8.16.9.mas):
zero(ml. m2):
ml[l]-l: ml[4]-l; m2[8]-l:
strcpy(s."Проверка правильности сборки"):
mas[6]=new Work(7.s.ml.m2. 4.13.7.mas):
zero(ml. m2):
ml[3]-l;
strcpyls."Сборка и проверка силового оборудования"):
mas[7]-new Work(8.s.ml.NULL. 3.9.6.mas):
zero(ml. m2):
ml[2]=l: ml[6]-l;
strcpy(s,“Проверка наладки");
mas[8]-new Work(9,s.ml.NULL. 1.8.3.mas):
zero(ml. m2);
//Условие завершения основного цикла - завершение всех работ
wh11e(completed!“N)
{
for(1=0;1<N:1++)
{
mas[i]->run():
}
total++:
}
//Освобождение выделенной памяти
for(i=0;i<N:i++) delete mas[1]:
delete [] mas:
delete [] ml: delete □ m2:
//Вывод результатов моделирования
printfCTotal tlme“X.3f\n". (float)Itotal-D/RATIO):
printf!"TimeBegi n:\n");
for(1-0:1<N;1++)
printf(“1.3f ". ((float)timeBeg1n[i])/RATI0);
printf(”\n“):
pr1ntf("TimeEnd:\n");
for(1“0;1<N;i++)
printf(“1.3f ". ((float)timeEnd[i])/RATIO):
printf("\n");
}
void zerolint *nl. int *n2)
{
for (int 1=0;1<N:1++)
{
п1[1]“П2[1]=0:
}
}
290 Глава 14. Сетевое планирование и анализ проектов
14.5. Анализ результатов
По результатам 10 000 прогонов модели мы можем вывести следующие усреднен-
ные показатели. Общая длительность составила 20,7 единиц времени. В табл. 14.3
представлены времена начала и завершения работ.
Таблица 14.3. Моменты начала и окончания работ
Фаза работы 1 2 3 4 5 6 7 8 9
Начало 0 0 0 2,98 2,98 7.39 7,39 10,93 15,96
Конец 2,98 6,00 13,97 10,93 6,96 18,42 15,34 16,92 19,94
Моделирование заканчивается завершением шестой, восьмой или девятой работы.
Интересно проследить статистику, показывающую, какая из этих работ в сколь-
ких случаях завершается последней. Данные таковы:
О вероятность того, что последней завершается работа 6, — 0,25;
О вероятность того, что последней завершается работа 8, — 0,13;
О вероятность того, что последней завершается работа 9, — 0,62.
В теории сетевого планирования хорошо известно [7, гл. 6], что такое критиче-
ский путь на графе работ. Так называется такая последовательность работ, в ко-
торой завершение предыдущей входит в условие начала последующей (то есть
связная линия на графе, соединяющая начальное и конечное состояния) и изме-
нение длительности любой из них приводит к изменению общего времени завер-
шения всех работ. Критический путь определяется четко и однозначно в случае
детерминированной длительности всех работ. Если же длительности работ явля-
ются случайными величинами, говорить о критическом пути можно лишь в ве-
роятностном смысле, так как математическое ожидание общего времени завер-
шения изменится при изменении параметров вероятностного распределения
длительности любой работы. Вопрос в том, насколько будут различаться эти из-
менения при «отключении» той или иной работы?
Спланируем эксперимент следующим образом. Зададим параметры треугольно-
го распределения первой работы равными 0, 1 и 0,5, то есть практически исклю-
чим ее влияние на конечный результат, если таковое имеется. После получения
результата восстановим параметры для первой работы, а параметры 0,1, 0,5 зада-
дим для второй работы, проделаем то же для всех девяти работ. Осуществление
этого плана позволило получить следующие времена завершения всего комплек-
са работ:
1. Исключение первой работы — 19,7.
2. Исключение второй работы — 20,46.
3. Исключение третьей работы — 20,36.
4. Исключение четвертой работы — 20,5.
5. Исключение пятой работы — 19,91.
6. Исключение шестой работы — 20,28.
14.5. Анализ результатов 291
7. Исключение седьмой работы — 19,8.
8. Исключение восьмой работы — 20,54.
9. Исключение девятой работы — 19,23.
Если упорядочить работы по убыванию эффективности влияния их исключения
на время выполнения проекта, получим последовательность 9-1-7-5-6-3-2-4-8.
Подойдем теперь к изучению вопроса с другой стороны. Снова рассмотрим
табл. 14.3. Мы уже знаем, что значительно чаще других последней завершается
работа 9. В ее входном списке две работы — 3 и 7. Среднее время завершения
седьмой работы (15,34) значительно ближе к среднему времени начала девятой
(15,96), чем среднее время завершения работы 3. Поэтому предположим, что оп-
ределяющее влияние на момент начала работы 9 оказывает работа 7. В свою оче-
редь, во входной список работы 7 входят две работы — 2 и 5. Используя ту же
логику, получим, что следующей в этом списке должна идти работа 5, так как
6,96 > 6,00. Ну а входной список работы 5 состоит только из одной работы —
первой. Таким образом, мы получили список из четырех работ: 1,5, 7, 9. Но это в
точности те самые работы, которые занимают первые четыре места в ранее полу-
ченной упорядоченной последовательности!
Таким образом, факты свидетельствуют о том, что выделенную последователь-
ность работ 1, 5, 7, 9 можно считать так называемым стохастическим крити-
ческим путем для рассмотренной задачи. Для сокращения времени выполнения
всего проекта следует пытаться сократить длительность именно этих четырех ра-
бот или, по крайней мере, некоторых из них. Сокращение длительности других
работ будет менее эффективно.
У не слишком искушенного в теории вероятностей читателя при анализе табл. 14.3
может возникнуть вопрос: почему времена завершения «заключительных» работ
равны в среднем 16,92, 18,42 и 19,94, а среднее время завершения всего проекта
20,7? На самом деле ничего необычного в этом нет, наоборот, полученные дан-
ные говорят в пользу правдоподобности модели. Дело в том, что математическое
ожидание максимума совокупности независимых случайных величин не равно
максимуму их математических ожиданий, а превосходит его, то есть
Е = М(тах(Х1, Х2...Х„)) > шах(М(Х1), М(Х2)...М(Х„)).
Исходя из вероятностных рассуждений для величины Е, стоящей в левой части
неравенства, можно записать точную формулу
+“ п п
Fj{x)dx, (14.1)
0 i=l >’1
где Е,(х) и/(х) — соответственно, функции распределения и плотности распре-
деления случайной величины Xit значение п для нашей задачи равно 9.
Для случая двух независимых одинаково распределенных экспоненциальных ве-
К» j 5 j
личин формула (14.1) принимает вид Е =21 xpe-,u (1 -е~^ )dx = — > М(Х) = —
о НН
В случае равномерного распределения на отрезке [a; fc] имеем
292 Глава 14. Сетевое планирование и анализ проектов
Е =---[ х(х-a)dx = > М(Х) =
(b-a)2ja 3 2
Таким образом, возникает любопытная ситуация. Если комплекс работ состоит
из двух работ и каждую из них выполняет рабочий, время работы которого рав-
номерно распределено на промежутке от часа до двух, то каждый завершит работу
в среднем через 1,5 ч, а обе работы в целом завершатся в среднем через 1 ч 40 мин.
Поэтому нанять рабочих, выполняющих работу за фиксированное время
1 ч 35 мин, оказывается выгоднее.
14.6. Criticality и cruciality
Как говорилось ранее, критический путь однозначно определяется в случае де-
терминированных длительностей всех работ, так как порядок и точное время их
завершения могут быть предсказаны заранее. Если же длительности работ —
случайные величины, однозначность теряется, поскольку при различных реали-
зациях случайного процесса порядок завершения работ, а значит, и критический
путь могут меняться. В предыдущем разделе этой главы мы воспользовались
правдоподобными рассуждениями, чтобы внести некоторую ясность в получен-
ные результаты. А существует ли какая-либо признанная теория? -
Наиболее удачное и доступное из знакомых автору изложений данного вопроса
приведено в уже цитировавшейся книге Терри Уильямса [90], где подобные за-
дачи исследуются с помощью двух вычисляемых для каждой работы мер —
criticality и cruciality1. Поскольку автору неизвестны общепризнанные переводы
этих терминов в русскоязычной научной литературе, в дальнейшем будут ис-
пользованы термины «критичность» и «значимость».
Итак, критичностью работы называется вероятность того, что она лежит на кри-
тическом пути. Из этого определения следует, что дать количественную оценку
критичности можно лишь путем имитационного моделирования с последующим
подсчетом того, в скольких экспериментах тот или иной путь оказался критиче-
ским. Однако, как показано в [90], знания только этого показателя подчас недо-
статочно для того, чтобы проектировщик мог считать себя обладающим всей не-
обходимой для принятия решения информацией. Представим себе систему из
двух параллельных работ А и В. Длительность А составляет 0,1 и 0,2 с вероятно-
стью 50%, длительность В — 0 и 100 с вероятностью, соответственно, 0,99 и 0,01.
Критичность работ А и В легко подсчитать и без моделирования. Из условия
следует, что с вероятностью 0,99 последней завершится работа А (в момент вре-
мени 0,1 или 0,2), с вероятностью 0,01 последней завершится работа В (в момент
времени 100). Поэтому для А критичность равна 0,99, для В — 0,01, так как толь-
ко в одном случае из ста путь, состоящий из работы В, окажется критическим.
Но следует ли из этого, что руководителя проекта, отвечающего за сроки его вы-
полнения, больше всего волнует именно работа А? Нет. Одну десятую или две
1 Понятия взяты из источника [90], они играют основную роль в материале этого раздела.
14.6. Criticality и cruciality 293
десятых единиц времени займет проект — не так уж и важно, а вот если он затя-
нется на 100 — это будет провал, и причина его лежит в работе В, а не А.
Изменим описание работы А. Пусть теперь ее длина равна 2 и 99 с вероятностью
опять-таки 50%. Тогда риск, связанный с А, многократно возрастает — руково-
дитель, вероятно, сосредоточит все внимание именно на ней. Но значения кри-
тичности остались теми же — 0,99 и 0,01. Таким образом, критичность не чувст-
вительна к различиям между очень и очень разными ситуациями. Чтобы учесть
это различие, вводится еще один показатель — значимость, определяемый как
коэффициент корреляции между двумя случайными величинами — длительно-
стью работы и суммарной продолжительностью всего проекта. Смысл показате-
ля можно объяснить так: если проект длится долго, когда долго длится работа X,
и недолго, когда работа X длится недолго, то X имеет высокую значимость,
так как ее длина является хорошим «зеркалом» длительности всего проекта и в
значительной степени задает ее неопределенность. С помощью этого показателя
можно выявить работы, требующие особого внимания проектировщика.
Вычислим значимость работы А для первого примера. Коэффициент корреля-
ции между двумя случайными величинами х и у вычисляется по формуле
(14.2)
5А
где символы М и 6 обозначают, соответственно, математическое ожидание и сред-
неквадратическое отклонение случайной величины. Обозначим а — длитель-
ность работы А, с — длительность всего проекта. Тогда случайная величина а
принимает значения 0,1 и 0,2 с вероятностью 0,5, а случайная величина с — зна-
чения 0,1, 0,2 и 100 с вероятностями, соответственно, 0,495, 0,495 и 0,01. Легко
подсчитать, что М(а) = 0,15, 6О = 0,05, М(с) = 1,1485, 6С = 9,935. Возможные пары
значений а и с таковы: (0,1; 0,1) с вероятностью 0,495, (0,2; 0,2) с вероятностью
0,495, (0,1; 100) с вероятностью 0,005 и (0,2; 100) с вероятностью 0,005. Тогда
числитель в формуле (14.2) равен
(0,1 - М(а))(0,1 - М(с)) • 0,495 + (0,2 - М(а))(0,2 - М(с)) 0,495 +
+ (0,1 - М(а))(100 - М(с)) 0,005 + (0,2 - М(а))(100 - М(с)) • 0,005 = 0,002475.
Отсюда значимость работы А равна 0,002475/(0,05 9,935) = 0,005.
Аналогичные подсчеты для значимости работы В дают результат 0,999. Во вто-
ром примере результаты несколько иные — 0,99 для А и 0,1 для В. Отметим, что
для вычисления коэффициента корреляции тоже требуются имитационные экс-
перименты, так как необходимо получить достаточно длинные последовательно-
сти двух случайных величин (один прогон модели дает ровно по одному элемен-
ту последовательностей). Для оценки проекта в целом полезно знать значения
обоих показателей для каждой работы.
Вернемся теперь к нашей задаче. Стократный прогон дал следующие результаты
частоты встречаемости критических путей:
О 1, 5, 7, 9 — 23 раза;
О 2, 7, 9 — 23 раза;
О 1, 5, 6 — 18 раз;
294 Глава 14. Сетевое планирование и анализ проектов
О 3, 9 — 16 раз;
О 1, 4, 8 — 10 раз;
О 2, 6 — 10 раз.
Результаты упорядочения частоты встречаемости работ на критическом пути,
следующие из приведенных данных, представлены в табл. 14.4.
Таблица 14.4. Частота появления работ на критическом пути
Номер работы Частота появления на критическом пути, раз
9 62
1 51
7 46
5 41
2 33
6 28
3 16
4 10
8 10
В начале списка видим все те же четыре работы — 9, 1, 7, 5.
Теперь приведем результаты расчетов значимости работ (табл. 14.5).
Таблица 14.5. Значимости работ
Номер работы Значимость работы
9 0,493
7 0,473
5 0,356
6 0,217
1 0,206
3 0,203
2 0,078
8 0,048
4 0,046
Здесь между работами 5 и 1 «вклинилась» работа б, потеснив работу 1 на пятое
место. Этот результат есть отражение того факта, что средняя длительность ра-
боты б (11) значительно превышает среднюю длительность работы 1 (3). Пере-
веса работы 1 в критичности оказалось недостаточно для того, чтобы компенси-
14.7. Обойдемся без @ RISK? 295
ровать разность продолжительностей работ для показателя значимости. Так,
работа 3 по значимости приблизительно равна работам 1 и б, хотя по критично-
сти намного от них отстает. Это и неудивительно, так как работа 3 — самая про-
должительная из всех девяти работ.
Таким образом, можно сделать вывод, что наши правдоподобные рассуждения
о стохастическом критическом пути полностью согласуются с теорией, разрабо-
танной Т. Уильямсом.
14.7. Обойдемся без ©RISK?
Вооружившись отлаженной методикой моделирования сетевых задач, рассмот-
рим задачу из [90], упомянутую в конце первой главы. В этой книге она была ре-
шена с помощью специального пакета ©RISK for Projects. Не имея такого паке-
та, решим ее с помощью C++ и сравним результаты. Упрощенная постановка
задачи такова.
Рис. 14.3. Проект установки нефтяной платформы в Северном море
Изучается проект установки нефтедобывающей платформы в Северном море
(рис. 14.3). На этот проект ярко выраженное влияние оказывает погода. Первые
пять работ — от документирования до сборки — выполняются на берегу, а ос-
тальные четыре — в море, поэтому зависят от погодных условий (в частности,
высоты волн). Каждая из «морских» работ должна быть завершена до начала сле-
дующей. Выполнение этих работ должно начаться не позднее начала лета, чтобы
не захватить зимний период, когда погода, скорее всего, испортится. В связи
296 Глава 14. Сетевое планирование и анализ проектов
с этим моделирование проекта требует информации о погодных явлениях, в дан-
ном случае о высоте волн. Эта информация включает не только распределение
высоты волны во времени, но и статистические данные, например автокорреля-
цию (иными словами, можно ли, зная высоту волны сегодня, определить, какой
она будет завтра).
По результатам статистической обработки метеонаблюдений максимальная вы-
сота волны в течение дня t удовлетворяет следующему уравнению регрессии:
Xt = 0.8Х(_, + 0,2р + £t, где ес (t = 0, 1, 2, 3...) — последовательность независимых
нормально распределенных случайных величин со средним, равным 0, и сред-
неквадратичным отклонением 0,5; р — среднемесячная высота волны, заданная
в табл. 14.6 в футах (1 фут ® 30,48 см).
Таблица 14.6. Среднемесячная высота волны
Янв Фев Март Апр Май Июн Июл Авг Сен Окт Ноя Дек
6,5 6,5 5,5 4,5 3,5 3,5 3,5 4,5 5,0 5,5 6,5 7,0
При задании продолжительности каждой работы пойдем на еще одно упрощение
и объединим пять «сухопутных» работ в одну (начало проекта).
1. Начало проекта (разработка документации, установка остовной решетки, па-
лубы и платформы, сборка) — равномерное распределение, от 3 до 4 месяцев.
2. Буксировка — 6 дней, если максимальная высота волны не превышает 4
(в день, когда это условие не выполняется, работа не ведется).
3. Установка подъемной системы — 6 дней, если максимальная высота волны
не превышает 3 (в день, когда это условие не выполняется, работа не ве-
дется).
4. Установка кабельной и трубопроводной систем — 12 дней, если высота вол-
ны не превышает 3,5 (в день, когда это условие не выполняется, работа не ве-
дется).
5. Комплексные испытания, наладка, сдача в эксплуатацию — равномерное
распределение, от 1 до 2 месяцев.
Проект начинается в начале года. Листинг моделирующей программы для этой
задачи (листинг 14.4) выглядит следующим образом (измененные по сравнению
с предыдущей задачей фрагменты кода выделены). Единица модельного време-
ни — один день.
Класс Work, его методы и функция main() (листинг 14.5) для этой задачи принци-
пиально ничем не отличаются от их аналогов, использованных в листингах 14.2
и 14.3. Отличия связаны лишь с новым распределением длительностей работ, не-
обходимостью вычисления и учета высоты волны, а также отслеживанием теку-
щей календарной даты.
Листинг 14.4. Моделирование проекта установки нефтяной платформы
#include "Data.h" //заголовочный файл с классом Data, описанным в главе 2
//и представляющим календарную дату
#include "normal.h"
14.7. Обойдемся без @ RISK? 297
#define N 5 //количество работ
float wh=6.5; //начальная высота волны 1 января
Data total(2004.0.1); //переменная для хранения текущей даты.
//Инициализируется датой начала проекта - 1 января 2004 года
int completed;
float whs[12]={6.5.6.5.5.5.4.5.3.5.3.5.3.5.4.5.5.0.5.5.6.5.7.0}: //данные
//о среднемесячной высоте волны
class Work
{
Int id:
char *name;
int *input:
int *output:
int time;
Work **ptr;
public;
Workdnt a. char *b. int *c. int *d. Work **p):
-WorkO: //деструктор
void WorkFinishdnt a);
void CompleteO:
void run():
}:
Work::Work(int a. char *b. int *c. int *d. Work **p)
{
int i:
id=a;
name=new char[strlen(b)+l]:
strcpy(name.b):
if (id!=l)
{
input=new int[N];
for(i=0:i<N;i++)
inputEi]=c[i]:
time=-l;
}
else
{
time=get_uniform(105.15): //разыгрываем время сухопутных работ
i nput=NULL;
}
if (id!-N)
{
output=new int[N]:
for(i=0;i<N;i++)
output[i]=d[i]:
}
else output=NULL:
ptr=p;
}
//Метод-деструктор
Work::-WorkO
{
delete [] name;
if (input) delete [] input:
if (output) delete [] output;
298 Глава 14. Сетевое планирование и анализ проектов
)
void Work::WorkFinish(int а)
{
int i:
input[a-l]-0:
for(i-0:i<N:i++)
if (input[i]!-0) break:
if (i==N)
{
//Разыгрываем длительность начинающейся работы
switch(id)
{
case 2: time=6: break; /
case 3: time-6: break;
case 4; time-12; break;
case 5: time=get_uniform(45. 15): break:
}
}
}
void Work::Complete()
{
int i;
time--l:
completed++: //инкремент счетчика выполненных работ
if (output!-NULL) //выходной список не пуст, делаем рассылку
{
for(i=0: i<N; 1++)
if (output[1]!=0) ptr[i]->WorkFinish(id):
)
)
void Work::run()
{
if (time>0)
{
//Проверяем, продвинулась ли текущая работа или этому помешал шторм
if ( (id==l)||(id==5)) time--:
if ((id—2)&&(wh<4)) time--:
if ((id—3)&&(wh<3)) time--:
if ((id—4)&&(wh<3.5)) time--:
}
if (time—0) CompleteO:
}
Листинг 14.5. Функция main()
#include "classes.h“
#define UI 1000 //количество прогонов модели
int ma n()
{
int i. k. alas:
void zero(int *nl. int *n2);
Work **mas: FILE *stat;
int *ml=new int[N]:
int *m2=new int[NJ:
char s[100]:
Листинг 14.5. Функция main() 299
srand((unsigned)time(O)):
stat=fopen("stat". "wt");
for(k-0:k<UI;k++)
{
alas=O: //сброс признака того, что в течение года проект не завершен
mas=new Work *[N]:
completed=0:
for (i=0;i<N:i++)
{
ml[i]-0:
m2[i]-0:
}
m2[l]-l:
strcpyfs."Работы на берегу"):
mas[0]=new Work(l.s.NULL.m2.mas);
zero(ml. m2):
m2[2]-l: ml[0]-l:
strcpyfs."Буксировка"):
mas[l]=new Work(2.s.ml.m2.mas);
zerofml. m2):
m2[3]-l: ml[l]-l:
strcpyfs."Установка подъемной системы"):
mas[2]=new Workf3.s.ml.m2.mas):
zerofml. m2):
m2[4]-l: ml[2]-l:
strcpyfs."Монтаж кабельной и трубопроводной систем");
mas[3]=new Work(4.s.ml.m2. mas):
zerofml. m2):
ml[3]-l:
strcpyfs."Монтаж системы в целом"):
mas[4]=new Work(5.s.ml.NULL.mas):
zerofml. m2):
while(completed!=N) //моделирующий цикл
{
//Вычисляем текущую максимальную высоту волны. Метод Data:rgetMonthf)
//возвращает номер текущего месяца - от 1 до 12
wh=0.8*wh+ get_normal(0.0.5.0.001)+0.2*whs[total.getMonthf)]:
for(i=0:i<N;i++)
mas[1]->run(); //моделирование работ
++total: //наступление следующего дня
if ((total .getMonthf)-=ll) && (total .getDay()=-3D)
{
alas=l; //устанавливаем признак того, что за год
//завершить проект не удалось
break: //выходим из моделирующего цикла
)
} //прогон завершен
for(i=0:i<N:i++) delete mas[i]:
delete [] mas: delete [] ml: delete [] m2:
//Записываем в файл, в каком месяце завершился проект
if (alas==0) fprintf(stat. "fcd\n". total.getMonthf)+l):
else fprintffstat. "13\n"): //проект за год не завершился
} //заданное число прогонов сделано
fclosefstat):
)
void zero(int*nl. int*n2){for(int i=0. i<N; 1++){nl[i]=0; n2[i]}-0:}}
300 Глава 14. Сетевое планирование и анализ проектов
Сравним результаты. На рис. 14.4 изображена гистограмма распределения про-
должительности проекта, полученная по результатам 1000 итераций в [90]. Из
нее видно, что до наступления сентября проект завершится приблизительно в
83% случаев, в сентябре-декабре — в 12% (причем «львиная доля» завершений
этого периода приходится на сентябрь), и в 5% случаев из-за плохой погоды
проект так и не успевает завершиться в текущем году.
Рис. 14.4. Гистограмма распределения продолжительности проекта, полученная в [90]
с помощью пакета @RISK
Рис. 14.5. Гистограмма распределения продолжительности проекта, полученная на C++
Задания для самостоятельной работы 301
На рис. 14.5 изображены полученные нами результаты. Сравнивая рис. 14.4 и 14.5,
видим, что при одинаковой общей картине количественные показатели все же
несколько различаются, давая в нашей программе более благоприятный прогноз.
Так, показатель периода июнь—август равен 87 %, периода сентябрь-декабрь —
10%, вероятность того, что проект не будет завершен в текущем году, — 3%. Раз-
личаются также показатели для июля и августа. В оригинале больше заверше-
ний приходится на август, в нашей программе — на июль. Трудно сказать, в чем
причина различий. Это могут быть неточный перевод условия задачи с англий-
ского языка, незначительная логическая ошибка в программе или некоторые
различия в генераторах нормального распределения. Тем не менее, даже если
рис. 14.4 принять за эталон, можно сделать вывод, что общую картину распреде-
ления программа на C++ передает верно.
Выводы
1. Работа — это объект, поддерживающий в своих полях данных два списка —
входной и выходной. Первый динамически меняется в сторону уменьшения,
второй остается постоянным.
2. Для рассылки сообщений о завершении выполнения объекту-работе должен
быть доступен любой другой объект, поэтому одним из полей данных класса
Work должен быть массив указателей на все объекты-работы системы.
3. В задачах сетевого планирования длительность моделирования заранее не за-
дается, а является одним из основных результатов самого моделирования.
Поэтому в заголовке основного моделирующего цикла ставится условие не
ограничения длительности, а завершения всех работ проекта.
4. Среднее время завершения всего проекта превышает среднее время заверше-
ния любой из работ.
5. Для изучения влияния работы на длительность проекта важное значение
имеют две характеристики — критичность и значимость.
Задания для самостоятельной работы
1. Разработайте общий формат представления исходных данных для сетевых
моделей в виде матрицы инцидентности или матрицы смежности графа ра-
бот, вводимой из файла, а также алгоритм получения по матрице входных
и выходных списков для каждой работы.
2. Модифицируйте на этой основе процедуру инициализации объектов так, что-
бы она представляла собой общий цикл, а не инициализацию каждого объек-
та фактическими данными по отдельности.
Глава 15
Моделирование
расписаний:участок дороги
с односторонним
движением
Основные вопросы, рассматриваемые
в данной главе:
□ Нужна ли связь между объектами,
работающими по заданному
расписанию
□ Программная реализация выполнения
расписания
□ Переход от детерминированного
расписания к динамическому.
Адаптация программы
□ Поиск оптимального расписания.
Выбор метода многомерной
оптимизации. Выполнение итераций
15.2. Модельное время 303
15.1. Описание системы
Моделируемая система представляет собой поток транспорта, движущегося в двух
направлениях по дороге с двусторонним движением, одна сторона которой за-
крыта в связи с ремонтом на протяжении 500 м. На рис. 15.1 показана схема
управления движением транспорта на ремонтируемом участке дороги с односто-
ронним движением.
Движение в первом
направлении
Движение во втором
направлении
Обозначения:
Светофор i
Ожидающие автомобили
Автомобили в пути
Рис. 15.1. Схема работы светофоров
Светофоры, размещенные на обоих концах одностороннего участка, управляют
движением на нем. Светофоры открывают движение на участке в одном из на-
правлений в течение заданного промежутка времени. На рис. 15.1 показана схе-
ма управления движением транспорта на ремонтируемом участке дороги с одно-
сторонним движением. Когда загорается зеленый свет, машины следуют по
участку с интервалом 2 с. Подъезжающий к участку автомобиль едет по нему без
задержки, если горит зеленый свет и перед светофором нет машин. Автомобили
подъезжают к светофорам через интервалы времени, распределенные экспонен-
циально с математическим ожиданием, равным 12 с для первого направления и
9 с — для второго. Светофор имеет следующий цикл: зеленый в первом направ-
лении, красный в обоих направлениях, зеленый во втором направлении, красный
в обоих направлениях. Красный свет горит в обоих направлениях в течение 55 с
для того, чтобы автомобили, следующие через ремонтируемый участок дороги,
смогли покинуть его до переключения зеленого света на другое направление.
Цель моделирования — определение таких значений «зеленых» интервалов для
обоих направлений, при которых среднее время ожидания всех автомобилей бу-
дет минимальным.
15.2. Модельное время
Наличие экспоненциальных случайных величин со средними значениями 12 и 9
(и такими же среднеквадратичными отклонениями!) не позволяет использовать
в качестве единицы модельного времени непосредственно секунду, так как
304 Глава 15. Моделирование расписаний: участок дороги с односторонним движением
погрешность округления случайной величины до целого будет сравнима с самой
величиной. Поэтому необходимо использовать масштабирование. При реализа-
ции модели коэффициент масштабирования принят равным 10, то есть едини-
цей является одна десятая секунды.
15.3. Классы и объекты
По обыкновению создадим два класса: один моделирующий — Светофор (SignalLights)
и один статистический — Заявка (Client), предназначенный для сбора статистики
по среднему времени пребывания. Класс Светофор представлен двумя объектами —
первым и вторым. Алгоритмы функционирования светофоров абсолютно иден-
тичны, поэтому их можно считать принадлежащими одному классу. Выписать
неизменяемые и изменяемые поля данных класса Светофор не составляет труда.
Система является открытой, длина очереди не ограничена, поэтому по установив-
шейся традиции очереди к светофорам будем моделировать связными списками.
Теперь обсудим один интересный нюанс, ранее не встречавшийся. Речь идет
о взаимосвязи объектов.
Почти во всех ранее рассмотренных задачах при наличии в системе более одного
активного объекта возникала необходимость наладить взаимодействие между
некоторыми парами объектов, а значит, в число полей данных одного объекта
входили указатели на другие объекты. Такая необходимость диктовалась логи-
кой функционирования системы — событие на одном объекте инициировало со-
бытие на другом, поэтому первый объект должен был каким-то образом инфор-
мировать своего «коллегу» по работе, сказав ему: «Пора!» — или, выражаясь
строже, послав сообщение. Так как событие на первом объекте происходит в слу-
чайный момент времени, второй объект, разумеется, не может знать заранее,
когда оно произойдет, поэтому и нуждается в уведомлении.
Что мы видим в данной задаче? По условию как «зеленые», так и «красные» ин-
тервалы светофоров — детерминированные величины. Поэтому светофорам не
нужна информация друг от друга, достаточно лишь знать длину «зеленого» ин-
тервала другого светофора. Тогда длина «красного» интервала равна Gt + G2 + 27?,
где Gi 2 — длины «зеленых» интервалов, R — длина «красного» интервала.
ПРИМЕЧАНИЕ В дальнейшем, для исследования различных модификаций системы, «красные»
интервалы сделаем случайными, и поэтому связь между светофорами нам пона-
добится.
Выпишем поля данных класса Светофор.
Неизменяемые поля:
О номер светофора (1 или 2);
О продолжительность «красного» интервала (55);
О при зеленом свете — продолжительность интервала между последовательны-
ми вступлениями на участок машин, ожидающих в очереди (2);
15.4, События и методы 305
О средняя интенсивность входного потока (1/12 » 0,083 заявок в секунду для
первого светофора и 1/9 ® 0,11 — для второго);
О продолжительность «зеленого» интервала данного светофора;
О продолжительность «зеленого» интервала другого светофора.
Изменяемые поля:
О список указателей на заявки, находящиеся в очереди;
О время до начала «красного» интервала;
О время до начала «зеленого» интервала;
О время до вступления на участок следующей машины из очереди (параметр
имеет смысл только при зеленом свете светофора);
О время до прибытия очередной машины (ближайшего события внешнего потока);
О текущая длина очереди (вычисляемое поле).
15.4. События и методы
С объектом Светофор могут происходить четыре события, индуцируемые обраще-
нием в ноль четырех переменных, — прибытие извне нового автомобиля, вступ-
ление на участок автомобиля из очереди, включение зеленого света и включение
красного света. В результате наступления этих событий значения одного или
нескольких полей данных объекта изменяются. Реакция светофоров на событие
определяется описанием логики работы системы и прокомментирована в лис-
тингах 15.1, 15.2. Сделаем лишь одно довольно интересное замечание: в данной
системе с очередями отсутствует понятие обслуживания на сервере. Заявка, по-
кидающая очередь, тут же считается покинувшей и систему и в дальнейшем не
рассматривается. И еще. После того как зажегся зеленый свет, машине, стоящей
первой, нет надобности ждать две секунды — она поедет сразу, так как ей ничто не
мешает. Поэтому метод ForwardO вызывается в программе из двух мест: из мето-
да гип() по истечении двухсекундного интервала и из метода GreenO — этот вы-
зов соответствует проезду первой машины из очереди.
Листинг 15.1. Файл classesl5.h с описанием классов
#include<cstdio>
#1nclude<cstdlib>
#1nclude<ctime>
#1nclude<cmath>
using namespace std:
#1nclude "Ljst.h”
FILE *quel: //файл для сбора статистики по длине очереди к первому
//светофору
FILE *que2: //файл для сбора статистики по длине очереди ко второму
//светофору
const int К=10: //коэффициент масштабирования времени
FILE ‘sojourn; //файл для сбора статистики по времени пребывания
//в очереди
int entered=0: //счетчик поступивших заявок
306 Глава 15. Моделирование расписаний: участок дороги с односторонним движением
Int completed-O: //счетчик обслуженных заявок (машин, проехавших
//по участку)
float quel_ave-0: //переменная для вычисления средней длины очереди
//к первому светофору
float que2_ave-0: //переменная для вычисления средней длины очереди
//ко второму светофору
float soj_ave-0: //переменная для вычисления среднего времени ожидания
long int total: //счетчик модельного времени
class Client
{
int id: //уникальный идентификатор заявки
int time: //время, проведенное в очереди
public:
friend class SignalLights:
Clientdnt i);
void PrintO:
}:
Client::Client(int i)
{
id=i:
time=0:
}
void Client::PrintO
{
printfCid-fcd time-id\n". id. } class SignalLights { const static int both-55*K: const static int gap-2*K: float input_rate: int id: int green_I: int green_other: ListNode<Client> *queue: int to_red; int to_green: int to_forward: int to_arrival: int q_length: public: SignalLights(float a. int b. -SignalLightsO: void ArrivalO: void ForwardO: void RedO: void GreenO: void run(); time): //продолжительность «красного» интервала //интервал въезда на участок //интенсивность входного потока //номер светофора //продолжительность «зеленого» интервала //продолжительность «зеленого» интервала //другого светофора //список указателей, моделирующий очередь //к светофору //время до включения красного света //время до включения зеленого света //время до въезда на участок следующей //машины //время до прибытия машины извне //текущая длина очереди int с. int d):
SignalLights::SignalLights(float a. int b. int c. int pr) //Конструктор.
Листинг 15.1. Файл classesl5.h с описанием классов 307
//В качестве параметров передаются интенсивность входного потока, длины
//«зеленых» интервалов, а также номер светофора
{
id-pr:
queue-NULL:
i nput_rate-a:
green_I=b:
green_other-c:
//Начальное состояние первого светофора - он только что зажег зеленый свет
If (рг--1)
{
to_red-green_I;
to_green=-l:
}
else //соответственно, на втором светофоре горит красный свет
{
to_green-both+green_other:
to_red--l:
}
to_forward--l:
//Очереди пусты
q_length=O:
to_a ггi va1-(i nt)(get_exp(i nput_rate)*K);
if (to_arrival-=0) to_arnval-l;
}
//Деструктор. Возвращает память, выделенную под очередь
SignalLights::-S1gnalLights()
{
whi1е(queue) queue-ListDelete<Client>(queue.queue):
}
//Прибытие новой машины
void Signal Lights::Arrival()
{
Client *ptr-NULL:
Li stNode<Cl1ent> *ptrl-NULL;
//Разыгрываем длину нового интервала между прибытиями
to_arri va1-(1nt)(get_exp(1nput_rate)*K):
if (to_arrival--0) to_arrival=l:
entered++; //инкремент счетчика поступлений
ptr-new Cl lent(entered): //создаем для новой заявки объект. Память будет
//возвращена либо в методе Forward при проезде
//перекрестка, либо деструктором, если к моменту
//завершения моделирования заявка будет
//находиться в очереди
if (q_length>0) //очередь не пуста, заявка становится в нее
{
ptrl-new L1stNode<Client>(ptr, NULL):
ListAdd<Client>(queue. ptrl):
q length++:
} '
else if (to_green>0) //очередь пуста, но горит красный свет, заявка
//становится в очереди первой
308 Глава 15. Моделирование расписаний: участок дороги с односторонним движением
ptrl=new LIistNode<Cl1ent>(ptr. NULL):
queue=ptrl:
q_length=l;
}
else //сразу можно ехать
{
completed++: //инкремент счетчика обслуженных заявок
delete ptr: //дальнейшее отслеживание этой заявки
//нам не нужно
soj_ave=soj_ave*(l-l.O/completed): //пересчет среднего времени ожидания
fprintf(sojourn. "ОХп"): //время ожидания было равно нулю
}
return:
}
//Въезд на участок новой машины
void SignalLights::Forward()
{
Int p:
ListNode<Client> *ptr:
if (to_red-=-l) //уже зажегся красный свет, ехать нельзя
{
to_forward=-l: return:
}
completed++: //инкремент счетчика обслуженных заявок
q_length--; //декремент текущей длины очереди
//Запись в файл времени пребывания убывшей заявки и пересчет среднего
p-queue->Data()->time:
fprintf(sojourn. "i.3f\n". ((float)p)/K):
soj_ave=soj_ave*(1-1.0/completed)+((float)p)/completed:
//В очереди еще есть заявки
if (q_length>0)
{
ptr=queue:
to_forward=gap:
queue=queue->Next(): //продвижение очереди
delete ptr: //освобождение памяти, занимаемой покинувшей
//очередь заявкой
}
else //очередь стала пуста
{
to_forward=-l: //освобождение памяти, выделенной для очереди
delete queue:
queue=NULL;
}
return:
}
//Зажегся красный свет
void SignalLights::Red()
{
to_red=-l:
to_green=2*both+green_other:
to_forward=-l;
Листинг 15.2. Функция main() 309
//Зажегся зеленый свет
void SignalLights::GreenО
{
to_green--l:
to_red=green_I:
if (q_length>0) ForwardO; //Поехали!
}
//Диспетчер
void SignalLights::run()
{
ListNode<Client> *ptr=NULL:
if (to_forward>0) to_forward--:
if (to_forward==0) ForwardO:
if (to_red>0) to_red--;
if (to_red==0) Red();
if (to_green>0) to_green--;
if (to_green==0) GreenO:
if (to_arrival>0) to_arrival--:
if (to_arrival==0) ArrivalO:
//Инкремент счетчика времени пребывания для всех заявок, стоящих в очереди
if (q_length>0)
{
ptr=queue:
while(ptr)
{
(ptr->Data()->time)++:
ptr=ptr->Next();
}
}
//Запись в файлы сбора статистики и пересчет средней длины очереди раз в две //секунды
1 f ((i d=-l) && (tota U20==0) &&( tota 1 /20>0))
{
fprintf(quel. "8d\n". q_length):
quel_ave=quel_ave*(1-1.0/(total/20))+((f1oat)q_length)/(total/20);
}
i f ((i d==2) && (tot a U20==0) && (tota 1 / 20>0))
{
fprintf(que2. "!Sd\n". q_length):
que2_ave=que2_ave*(l-1.0/(total/20))+((float)q_length)/(total/20):
}
}
Листинг 15.2. Функция main()
#include "classesl5.h"
#define N 12*3600*K //общее время моделирования - 12 часов
int main()
{
int 1:
quel=fopen("quel". "wt"):
que2=fopen("que2". "wt"):
sojourn=fopen(“sojourn". "wt”):
srand((unsigned)time(0)):
310 Глава 15. Моделирование расписаний: участок дороги с односторонним движением
//Создаются два светофора с длиной зеленого интервала 1 мин
SignalLights sl(0.083, 600. 600. 1):
SignalLights s2(0.111. 600. 600. 2):
//Моделирующий цикл
for(tota1-0L;tota1<N:total++)
sl.runO:
s2.run():
}
//закрытие файлов сбора статистики
fclose(sojourn);
fclose(quel): fclose(que2):
// вывод результатов
printf("Всего поступлений ld\n". entered):
printf("Проехало по участку M\n". completed):
printf("Средняя длина очереди к первому светофору 1.3f\n", quel_ave):
printf("Средняя длина очереди ко второму светофору X.3f\n". que2_ave):
printf("Среднее время пребывания в очереди *.3f\n". soj_ave/K):
}
15.5. Немного фантазии
Из условия задачи можно сделать вывод, что за 55 с автомобиль проезжает ремон-
тируемый участок дороги от начала до конца. По окончании «зеленого» интерва-
ла светофор должен исходить из наихудшего предположения, заключающегося
в том, что последний автомобиль въехал на участок как раз перед включением
красного света, ведь у светофора нет автоматической системы видеонаблюдения
за дорогой и он не может отследить момент въезда последней машины. Но на са-
мом деле такое предположение является избыточным, ведь наверняка возникают
ситуации, когда в последние, скажем, 10 с «зеленого» интервала автомобили на
участок не въезжали. Значит, уже через 45 с на другом светофоре можно вклю-
чать зеленый свет. Сделаем фантастическое предположение: светофоры обо-
рудованы автоматической системой и могут динамически подстраивать длитель-
ности «красных» интервалов друг друга, фиксируя момент проезда последней
машины и одновременно с переключением на красный свет посылая сообщение
«коллеге», через сколько секунд ему включить зеленый. Соответственно, через
некоторое время отправитель и сам получит сообщение от коллеги, через какое
время ему включать зеленый. Если же такое сообщение по каким-то причинам
получено не будет, зеленый свет включится по детерминированному расписа-
нию. Интересно выяснить, как скажется такое дорогостоящее техническое нов-
шество на основном показателе — среднем времени ожидания.
Для сокращения дальнейшего сравнительного анализа назовем прежнюю модель
первой, а рассматриваемую в данном случае — второй. Для второй модели дли-
тельность «красного» интервала становится случайной величиной, поэтому меж-
ду двумя светофорами должна существовать связь для передачи сообщений. Вы-
пишем для второй модели фрагменты кода, которые претерпевают изменения по
сравнению с первой моделью (листинги 15.3, 15.4).
15.5. Немного фантазии 311
Листинг 15.3. Модифицированные фрагменты методов
class SignalLights
{
int from_last: //время, прошедшее от момента въезда
//на участок последнего автомобиля
//(при зеленом сигнале светофора)
SignalLights *other: //указатель на другой светофор
public:
void PutOtherCSignalLights *р); //метод для инициализации связи
}:
void SignalLights::PutOtherCSignalLights *р)
{
other=p:
}
SignalLights::SignalLights(float a. int b. int c. int pr)
{
from_last=-l;
)
void SignalLights::Arrival()
{
else //машина проезжает сразу, поскольку
//нет очереди
{
completed++;
delete ptr;
soj_ave=soj_ave*(1-1.0/completed);
fprintf(sojourn. ”l\n");
from_last=0: //начинается новый отсчет времени
//с момента проезда последней машины
)
return:
)
void SignalLights::Forward()
{
from_last-0: //начинается новый отсчет времени
//с момента проезда последней машины
if (q_length>0)
{
)
else
{
)
return:
)
312 Глава 15. Моделирование расписаний: участок дороги с односторонним движением
void Signal Lights::Red()
{
to_red=-l;
to_green=2*both+green_other: //назначение длины «красного» интервала
//по максимуму. В дальнейшем она будет
//скорректирована в результате получения
//сообщения от другого светофора
to_forward=-l:
other->to_green=both-froni_last: //сообщаем другому светофору, через какое
//время он может включить зеленый свет
fromjast—1; //при красном свете это поле данных
//не имеет смысла
)
void Signal Lights::run()
{
if ((from_last!=’-l)&&(froni_last<both)) from_last++; //если значение поля
//данных достигло максимального (зто может случиться при больших
//длительностях зеленого интервала), далее его не увеличиваем, так как
//остаточная длительность красного интервала другого светофора будет равна
//нулю
)
Листинг 15.4. Функция main()
//Инициализация связи между созданными обьектами
sl.Put0ther(&s2);
s2.Put0ther(&sl);
for(total-0L:total<N;total++)
{
sl.runO:
S2.run();
}
15.6. Как оптимизировать?
В условии задачи не заданы длины «зеленых» интервалов. Их требуется найти
опытным путем, чтобы минимизировать общее среднее время ожидания, то есть
численно решить задачу двумерной оптимизации функции Т - f(glt g2), где Т —
среднее время ожидания, g} и g2 — длительности «зеленых» интервалов. Особен-
ности этой функции таковы:
О ничего не известно о градиенте (частных производных) минимизируемой функ-
ции;
О для получения более или менее достоверного значения функции в одной точ-
ке (при фиксированных длительностях «зеленых» интервалов) необходимо
усреднить результаты нескольких экспериментов, проведенных при одних
15.6. Как оптимизировать? 313
и тех же исходных данных (в представленных далее расчетах — 1000 прого-
нов для каждой точки).
Содержательность задачи оптимизации не вызывает возражений. В самом деле,
длительность зеленого интервала не может быть ни слишком малой — автомоби-
ли, стоящие в очереди, не успеют проехать, ни слишком большой — будет накап-
ливаться очередь на другом светофоре, так как пока на одном горит зеленый
свет, на другом горит красный. Следовательно, логично ожидать, что некоторая
пара значений, доставляющая среднему времени ожидания минимальное значе-
ние, существует.
Важно ответить на вопрос: является ли наша задача задачей безусловной или ус-
ловной оптимизации, то есть следует ли наложить какие-то ограничения на об-
ласть, в которой ищется точка минимума? Всякую ли пару значений «зеленых»
интервалов имеет смысл рассматривать? Очевидно, что нет. Эти значения долж-
ны быть такими, чтобы обеспечить существование стационарного режима,
то есть чтобы очереди не росли до бесконечности. Для этого средняя интенсив-
ность поступления заявок не должна превышать средней интенсивности их убы-
тия для каждого светофора. Попробуем выразить эти условия аналитически.
Пусть g, и g2 — длительности «зеленых» интервалов для первого и второго све-
тофоров. В течение так называемого светофорного цикла (два «красных» и два
«зеленых» интервала) у первого светофора скопятся в среднем (55 • 2 + gt + g2)/12
машин, а у второго — (55 2 + g, + g2)/9. За это время на участок будут пропу-
щены, соответственно, gjl и g2/2 машин. Имеем систему неравенств
l(110 + g1+g2)<^-;
l(110 + g1+g2)<^-.
Решением этой системы является следующее двойное неравенство:
(220+g1)/7<g2<5gI -110. (15.1)
Левая и правая часть формулы (15.1) совпадают при gt = 29,1, g2 = 35,5. Поэтому
можно сказать, что длительность зеленого интервала на первом светофоре не мо-
жет быть менее 30 с, а на втором — менее 36 с. Область, задаваемую форму-
лой (15.1), легко изобразить графически на координатной плоскости (рис. 15.2).
Какой же метод выбрать для поиска точки минимума? Наиболее подходящим
представляется метод покоординатного спуска, который хоть и сходится доволь-
но медленно, однако не требует, в отличие от градиентных методов, вычисления
частных производных. Идея метода состоит в следующем. Имея какое-то теку-
щее приближение к точке минимума, мы вычисляем значения минимизируемой
функции в некоторой A-окрестности от него, поочередно увеличивая или умень-
шая одну из координат на величину шага А, а значения остальных координат
оставляя неизменными. Если в одной из точек A-окрестности удалось уменьшить
значение функции и эта точка входит в область, заданную ограничениями (в дан-
ном случае условием (15.1)), то эта точка и становится новым текущим прибли-
жением к минимуму. Далее мы проверяем A-окрестность уже для новой точки.
314 Глава 15. Моделирование расписаний: участок дороги с односторонним движением
Если же уменьшить значение целевой функции не удалось, для величины шага h
назначается меньшее (например, в два раза) значение и вся процедура повторя-
ется для той же самой точки. Вычисления заканчиваются, когда h станет меньше
некоторой заданной точности.
Рис. 15.2. Область допустимых значений длин «зеленых» интервалов
Приведем формальное описание метода покоординатного спуска, следуя кни-
ге [9]. В ней же приведено и доказательство сходимости метода.
15.7. Метод покоординатного спуска
Требуется решить задачу условной оптимизации: J(U) —> inf, и е U. Обозначим
е, = (0..О, 1, 0.0) единичный координатный (базисный) вектор, у которого
i-я координата равна 1, остальные равны нулю, г = 1,..., п. Пусть щ — некоторое
начальное приближение, а а0 — некоторое положительное число, являющееся
параметром метода. Допустим, что нам уже известны точка щ е U и число ак > 0
при каком-либо k > 0. Примем
Рк = \ -
ik -k-n — +1,
п
(15.2)
15.8. Анализ результатов 315
ГЯ
где -
п
означает целую часть числа k/n. Условие (15.2) обеспечивает цикличе-
ский перебор координатных векторов е„ е2..е„, то есть р0 = eit p„_t = е„,
р„ = е{, p2n-i = ет Pin = ei- Вычислим значение функции J(u) в точке
и = ик + акрк и проверим условие и е U. Если оно выполняется, проверяем вы-
полнение неравенства
Лщ + o-kPk) <J(uk)- (15.3)
Если выполняется и оно, то примем
uh+l~uli + akPk> a*+l = at-
В том случае, если (15.3) не выполняется либо точка и не входит в область, за-
данную ограничениями, вычисляем значение функции J(u) в точке и - ик - а.крк
и проверяем выполнение неравенства
J(w* - акрк) < J(uk). (15.4)
В случае выполнения формулы (15.4) и вхождения в область U примем
uk + i = uk-akpk,ak+l = aak.
Назовем (k + 1)-ю итерацию удачной, если справедливо хотя бы одно из нера-
венств (15.3) или (15.4) и соответствующая точка и = ик ± принадлежит об-
ласти U. Если (k + 1)-я итерация неудачная, то есть одновременно с условием
и е U не выполняется ни одно из неравенств (15.3) и (15.4), то полагаем
Ы*+1 ~uk< a4+l _
Xat при ik =n,uk = uk_n+i; 5)
at при ik * n, или uk * uk_n+i, или 0 < k < n -1.
Здесь X, 0 < X < 1, — фиксированное число, являющееся параметром метода. Ус-
ловия (15.5) означают, что если за один цикл из п итераций при переборе на-
правлений всех координатных осей е„ ..., е„ с шагом ак реализовалась хотя бы
одна удачная итерация, то длина шага ак не дробится и сохраняется на протяже-
нии по крайней мере следующего цикла из п итераций. Если же среди последних
п итераций не оказалось ни одной удачной, то шаг ак дробится. Описание метода
покоординатного спуска закончено.
15.8. Анализ результатов
Приведем результаты расчетов методом покоординатного спуска для первой мо-
дели. Значения функции (среднего времени ожидания) округлены до одного
знака после запятой. Значение шага не будем принимать меньшим одной секун-
ды. В качестве начального приближения примем значения «зеленых» интерва-
лов равными одной минуте, то есть (g,, g2) = (60, 60).
7X60, 60) = 81,7; a = 10.
316 Глава 15. Моделирование расписаний: участок дороги с односторонним движением
Результаты первой итерации:
7(50; 60) = 78,8; 7(70; 60) = 86,5; 7(60; 70) = 79,8; 7(60; 50) = 108,1.
Итерация удачна, так как 7(50; 60) < 7(60; 60). Значение шага сохраняется. Ре-
зультаты второй итерации:
7(40; 60) = 80,2; 7(50; 70) = 78,3; 7(50; 50) = 88,6.
Итерация удачна. Значение шага сохраняется. Результаты третьей итерации:
7(40; 70) = 83,9; 7(50; 80) = 79,4.
Итерация неудачна. Полагаем значение шага равным 5:
7(55; 70) = 78,9; 7(45; 70) = 79,8; 7(50; 75) = 78,7; 7(50; 65) = 78,4.
Итерация неудачна. Полагаем значение параметра равным 2:
7(52; 70) = 78,5; 7(48; 70) = 78,4; 7(50; 72) = 78,5; 7(50; 68) = 78,2.
Формально итерация удачна, хотя вычисления уже можно прекращать — с прак-
тической точки зрения точность на уровне десятых долей секунды нам в этой за-
даче не нужна. Сделаем еще одну итерацию:
7(52; 68) = 78,4; 7(48, 68) = 78,2; 7(50; 66) = 78,2.
Итак, делаем вывод: для достижения оптимального решения необходимо задать
длину «зеленого» интервала для первого светофора 50 с, для второго светофо-
ра — от 65 до 70 с. При этом среднее время ожидания автомобиля в очереди рав-
но около 78 с.
Добавим, что при оптимальных значениях «зеленых» интервалов, средняя длина
очереди к первому светофору равна около 7, ко второму — около 8. Это логично,
так как интенсивность входного потока ко второму светофору выше.
Проведем аналогичные расчеты для второй модели. На основе имеющихся дан-
ных мы можем выбрать для начала расчетов лучшее начальное приближение —
точку (50; 70):
7(50; 70) = 68,2.
Результаты первой итерации, а = 5:
7(55; 70) = 68,3; 7(45; 70) = 69,3; 7(50; 75) = 68,6; 7(50; 65) = 68.
Итерация удачна, так как 7(50; 65) < 7(50; 70). Значение шага сохраняется. Ре-
зультаты второй итерации:
7(55; 65) = 68,5; 7(45; 65) = 68,7; 7(50; 60) = 68,3.
Итерация неудачна, а = 2:
7(52; 65) = 68,1; 7(48; 65) = 68; 7(50; 67) = 68; 7(50; 63) = 68,1.
15.8. Анализ результатов 317
Дальнейшие вычисления излишни. Вывод об оптимальных значениях «зеленых»
интервалов такой же, как и для первой модели. А вот среднее время ожидания
сократилось за счет динамической подстройки «красных» интервалов на 10 с.
Средняя длина очереди тоже уменьшилась: б — для первого светофора и 7,5 —
для второго. На рис. 15.3 приведены графики зависимости среднего времени
ожидания от длины «зеленого» интервала для второго светофора при длине «зе-
леного» интервала первого светофора, равной 50. Область определения зависи-
мостей рассчитана с помощью выражения (15.1) при g( = 50 и представляет со-
бой интервал [46; 139]. Нетрудно заметить идентичность этих двух кривых.
«Зеленый» интервал для второго светофора
Рис. 15.3. Зависимости среднего времени ожидания от длины «зеленого» интервала
второго светофора
Средняя длительность «красного» интервала в обоих направлениях уменьшает-
ся во второй модели с 55 до 46-47 с при g, = 50, g2 = 66.
Таким образом, имитационное моделирование свое дело сделало. Теперь слово
за экономистами — они должны подсчитать, покроет ли экономия, полученная в
результате десятисекундного сокращения среднего времени ожидания, затраты
на установку и эксплуатацию следящей видеосистемы или нет.
318 Глава 15. Моделирование расписаний: участок дороги с односторонним движением
Выводы
1. В рассматриваемой системе отсутствует обслуживание в узлах (светофорах).
Заявка, покинув очередь, тем самым покидает и узел обслуживания.
2. Связь между объектами, представляющими узлы обслуживания, нужна при
динамическом расписании и не нужна — при детерминированном. В послед-
нем случае, однако, объекты должны располагать информацией о расписании
«коллеги». Эта информация хранится в одном или нескольких полях данных.
3. Задачу оптимизации расписания требуется корректно поставить. Одно из ус-
ловий постановки — наложение ограничений на область, в которой ищется
точка минимума. Одним из наиболее простых методов оптимизации, не тре-
бующих работы с градиентами, является метод покоординатного спуска.
4. Выполнение одной итерации метода покоординатного спуска (это относится
и к другим методам) заключается в многократном прогоне моделирующей про-
граммы при значениях оптимизируемых параметров, равных текущему при-
ближению к точке минимума, с последующим приравниванием усредненного
результата значению целевой функции в данной точке.
Задания для самостоятельной работы
1. Дополните программный код модели 2, чтобы собрать статистику о длитель-
ности «красного» интервала в обоих направлениях.
2. Метод покоординатного спуска, вообще говоря, гарантирует сходимость лишь
к локальному минимуму. Примените метод при других значениях начального
приближения и проверьте, не сходится ли он к какой-либо другой точке мини-
мума. значение в которой меньше значения в найденной оптимальной точке.
3. Попробуйте заменить в условии задачи экспоненциальное распределение
распределением Парето с сохранением математического ожидания. Проде-
лайте все вычисления и сравните результаты.
Глава 16
Раздельные очереди
ограниченной длины
с переходами заявок:
банк для водителей
со сменой подъездных
полос
Основные вопросы, рассматриваемые
в данной главе:
□ Проблема формализации переходов
между очередями при наличии более
двух раздельных очередей
□ Сбор статистики по распределению
выходного потока обслуженных
заявок
□ Программная реализация переходов
□ Влияние переходов на
производительность: закон
сохранения среднего времени
ожидания
□ Влияние ограничения длины очереди
на производительность
3 20 Глава 16. Раздельные очереди ограниченной длины с переходами заявок
16.1. Описание системы
В банке для автомобилистов имеется два окошка, каждое из которых обслужива-
ется одним кассиром и имеет одну подъездную полосу. Обе полосы расположе-
ны рядом. Из предыдущих наблюдений известно, что интервалы времени между
прибытием клиентов в час пик распределены экспоненциально с математическим
ожиданием, равным 0,5 единиц времени. Так как банк бывает перегружен только
в часы пик, то анализируется только этот период. Продолжительность обслужи-
вания автомобилей обоими кассирами одинакова и распределена нормально с
математическим ожиданием, равным 1, и среднеквадратичным отклонением 0,3
единицы времени. Известно также, что при равной длине очередей, а также при
отсутствии очередей клиенты отдают предпочтение первой полосе. Во всех дру-
гих случаях клиенты выбирают более короткую очередь. После того как клиент
въехал в банк, он не может покинуть его, пока не будет обслужен. Однако он мо-
жет сменить очередь, если стоит последним и разница в длине очередей состав-
ляет не менее двух автомобилей. Из-за ограниченного места на каждой полосе
может находиться не более трех автомобилей. В банке, таким образом, не может
находиться более восьми автомобилей, включая автомобили двух клиентов, об-
служиваемых в текущий момент кассирами. Если место перед банком заполнено
до отказа, прибывший клиент считается потерянным, так как он сразу же уезжает.
Цель — разработка имитационной модели, которая может быть использована
для анализа ситуации в банке с помощью следующих статистических характе-
ристик:
О загрузка кассира;
О интегрированное по времени среднее число клиентов в банке;
О интервалы времени между отъездами клиентов от окон;
О среднее время пребывания клиента в банке;
О среднее число клиентов в каждой очереди;
О процент клиентов, которым отказано в обслуживании;
О число смен подъездных полос.
»
Имитация работы системы проводится в течение 1000 единиц времени, причем
необходимо получить трассировку событий, происходящих в течение первых де-
сяти единиц времени.
16.2. Модельное время
Время в задаче задано в безразмерных единицах, и указанные средние значения
слишком малы. Применяем стандартный подход — масштабирование При моде-
лировании принят коэффициент масштабирования, равный 100. Результаты
представляем в исходных единицах времени.
16.3. Классы и объекты 321
16.3. Классы и объекты
Перечислим, что мы можем сказать о системе по ее текстовому описанию:
О система открытая — заявки поступают из внешнего потока и после заверше-
ния обслуживания покидают систему навсегда;
О система многоканальная;
О каждый канал имеет отдельную очередь;
О каждый канал имеет ограниченный буфер, равный 3. Таким образом, общее
число заявок в системе является ограниченной сверху случайной величиной,
следовательно, существование стационарного режима гарантировано даже при
условии большой нагрузки, плата за это — потери заявок;
О при выборе сервера используются приоритеты;
О допускается возможность переходов из одной очереди в другую во время
ожидания.
Из-за ограниченности буфера для моделирования очередей будут использовать-
ся массивы указателей.
В ранее рассмотренных системах с многоканальными узлами мы не фиксирова-
ли жестко количество каналов, а делали его полем данных класса, значение кото-
рого можно было менять. Здесь же мы отступим от сложившейся традиции и
число каналов — два — зафиксируем. Это придется сделать по причине, указан-
ной в последнем пункте перечня характеристик системы, — возможности смены
очередей уже в процессе ожидания. Если предположить, что очередей не две,
а несколько, то правило перехода становится неопределенным. Как поступить,
если в какой-то момент возможны несколько переходов, но после совершения
одного из них любой из остальных уже становится невозможным и, наоборот,
появляются возможности для новых переходов? И сколько переходов можно со-
вершить за один такт? Формализовать расписание переходов на случай произ-
вольного числа каналов не так-то просто, и мы не будем этого делать.
С учетом уже накопленного опыта проектирование моделирующей программы
для данной задачи трудности не представляет. Некоторые новые особенности,
конечно, есть, но реализовать их несложно. К числу таких особенностей относят-
ся следующие:
1. Индикатором совершения события является не только истечение некоторого
интервала времени, но и выполнение условия, не связанного с отсчетом вре-
мени. Речь идет о переходах, условие которых должно регулярно проверяться
на каждом такте.
2. Требование сделать трассировку событий в течение заданного периода.
3. Требование собрать статистику о распределении выходного потока.
Очевидно, что основной класс в программе — Банк (Bank). Выпишем его поля данных.
Неизменяемые поля:
О размер буфера (3);
О средняя интенсивность входного потока (2);
322 Глава 16. Раздельные очереди ограниченной длины с переходами заявок
О среднее время обслуживания (1);
О среднеквадратичное отклонение времени обслуживания (0,3);
Изменяемые поля:
О два указателя на обслуживаемые в данный момент заявки;
О два массива указателей для представления очередей;
О время до прибытия следующей заявки из входного потока;
О время до завершения обслуживания заявки первым кассиром;
О время до завершения обслуживания заявки вторым кассиром;
О время, прошедшее с момента последнего ухода обслуженной заявки из систе-
мы. Это поле данных требуется для сбора статистики по выходному потоку.
Отметим, что при генерации нормального распределения мы пренебрегаем воз-
можностью получения отрицательных значений, концентрируя их в минимально
возможном значении интервала времени — единице. Вероятность того, что нор-
мально распределенная случайная величина с параметрами 1 и 0,3 примет отри-
цательное значение, не превышает 0,01, а эту погрешность будем считать допус-
тимой.
Класс Клиент, описывающий заявки, довольно прост, поэтому давать по нему по-
яснения нет необходимости — текста программы вполне достаточно для понима-
ния.
16.4. События и методы
К традиционным событиям — поступлению новой заявки и завершению обслу-
живания — добавляется еще одно — переход. Проверка условия перехода осуще-
ствляется диспетчером, который вызывает соответствующий метод и передает
ему параметр, обозначающий, из какой очереди (первой или второй) происходит
переход. Кроме традиционных декрементов, отсчитывающих время, диспетчер
производит один инкремент — счетчика тактов с момента последнего ухода заяв-
ки из системы. Для реализации трассировки используется отдельный файл, в ко-
торый методы записывают информацию о подконтрольных им событиях (лис-
тинги 16.1, 16.2).
Листинг 16.1. Файл classes!6.h с описанием классов
#include<cstdio>
#include<cstdlib>
#include<ctime>
#include<cmath>
using namespace std:
#include “normal.h"
int K=100:
int Dump=10:
FILE *dump:
FILE *num_inbank;
FILE *time_outbank:
FILE *sojourn:
//коэффициент масштабирования времени
//длительность трассировки
//файл для трассировки
//файл для сбора статистики о числе заявок в системе
//файл для сбора статистики выходного потока
//файл для сбора статистики времени пребывания в системе
Листинг 16.1. Файл classesl6.h с описанием классов 323
int entered=0:
int completed=0:
int transition^:
int reject=0:
float quel_ave=0;
float que2_ave=0:
float num_ave=0:
float soj_ave=0:
float out_ave=0:
float ro_ave=0:
long int total:
//Протокол класса
class Client
{
int id:
int time;
public:
friend class Bank:
ClientCint i):
}:
Client::C1ient(int i)
{
id=i;
time=0;
}
//Протокол класса Bank
class Bank
{
const static int B=3:
const static float input_rate=2:
const static float serve_rate=l:
const static float serve_disp=0.3:
Client *servel. *serve2:
Client **q_servel. **q_serve2:
int to_arrival:
//Время до завершения обслуживания
int to_servel:
int to_serve2:
int from_outbank:
public:
BankO:
-Bank»:
//Моделирующие методы
void ArrivalO:
void Completed nt i):
void Transition(int i):
//счетчик поступлений
//счетчик обслуженных заявок
//счетчик переходов
//счетчик потерянных заявок
//переменная для пересчета средней длины очереди
//к первому окну
//переменная для пересчета средней длины очереди
//ко второму окну
//переменная для пересчета среднего числа заявок
//в системе
//переменная для пересчета среднего времени пребывания
//заявок системе
//переменная для пересчета средней длительности
//интервала между уходами заявок
//переменная для пересчета коэффициента загрузки
//системы
//счетчик модельного времени
Client, описывающего заявки
//размер буфера
//средняя интенсивность входного
//потока
//средняя длительность обслуживания
//среднеквадратичное отклонение
//времени обслуживания
//обслуживаемые клиенты
//очереди к кассирам
//время до прибытия следующей заявки
//время, прошедшее с момента
//последнего ухода
//конструктор
//деструктор
//прибытие новой заявки
//i-й кассир завершил обслуживание
//переход из i-й очереди
324 Глава 16. Раздельные очереди ограниченной длины с переходами заявок
void run!): //диспетчер
//Служебные методы int Choice!): //выбор очереди при поступлении
int Busy!): //число занятых кассиров
int OLength(int i): }: //длина очереди к i-му кассиру
//Конструктор: система пуста, ждет поступлений
Bank::Bank О
{
int i:
senvel=NULL; serve2=NULL:
q_senvel=new Client *[B]:
q_senve2=new Client *[B]:
for(i=0:i<B;i++)
{
q_servel[i]=NULL:
q_senve2[i]=NULL:
}
to_a rnival=(int)(get_exp(i nput_rate)*K):
if (to_arnival==0) to_arrival=l:
to_senvel=-l: to_senve2=-l:
from_outbank=-l;
}
//деструктор
Bank::-Bank()
{
if (servel) delete servel:
if (serve2) delete senve2;
forfint i=0:i<B;i++)
{
if (q_senvel[i]) delete q_servel[i]:
if (q-serve2[i]) delete q_senve2[i];
}
delete [] q_senvel;
delete [] q_senve2:
}
//Выбор очереди в соответствии с приоритетом
int Bank::Choice!)
{
int kl. k2;
kl=QLength(l): k2=QLength(2):
if (kl>k2) netunn(2);
else if (kl<k2) return!1):
else if (servel==NULL) //в системе заявок нет
return(l);
//Очередей нет. первый кассир занят, второй - нет
else if (serve2==NULL) return(2):
//Очереди не пусты, их длины равны
else if (kl!=B) return(l):
//Мест для ожидания нет
else return(O):
}
Листинг 16.1. Файл classesie.h с описанием классов 325
int Bank::BusyО
{
if (senvel==NULL)
if (s^rve2==NULL) return(O); //оба кассира свободны
else return(l); //занят только один из кассиров
else if (serve2==NULL) return(l); //занят только один из кассиров
else return(2): //заняты оба кассира
}
int Bank::QLength(int i)
{
int k:
if (i=-l)
{
fon(k=0:k<B:k++)
if (q_servel[k]=-NULL) neturn(k):
neturn(B):
}
else
{
for(k=0:k<B:k++)
if (q_serve2[k]-=NULL) neturn(k):
netunn(B):
}
}
//Поступление новой заявки
void Bank::Anrival()
{
int i: Client *ptr:
entened++: //инкремент счетчика поступлений
//запись в файл трассировки
if (total<K*Dump) fpnintf(dump. "id - поступила новая заявка\п". total):
//Разыгрываем следующее время поступления
to_annival=(int)(get_exp(input_rate)*K);
if (to_arrival=0) to_arrival=l:
i=Choice(): //выбираем, куда становиться
if (i==0) //мест нет. заявка теряется
{
if (total<K*Dump) fpnintf(dump. "Она оказалась отвергнутойХп”):
reject++: return:
} 1
ptr=new Client(entered); //места есть, генерируем новую заявку.
//Память будет возвращена в методе CompleteO после завершения обслуживания
//у первого либо у второго кассира. Или в деструкторе, если к моменту
//завершения моделирования заявка еще будет находиться в системе
if (i==l) if (Iservel) //заявка сразу идет на обслуживание
//к первому кассиру
{
if (total<K*Dump) fprintffdump. "Она получила номер id и поступила на обслуживание
к первому окну\п". entered):
to_servel=(int)(get_nornial(serve_rate. serve disp. 0.01)*K):
if (to_servel<=0) to_servel=l:
servel=ptr;
326 Глава 16. Раздельные очереди ограниченной длины с переходами заявок
}
else //заявка становится в очередь
//к первому кассиру
{
q_servel[OLength(1)]=ptr;
if (total<K*Dump) fprintf(dump. "Она получила номер £d и стала в очередь к первому
окну, номер в очереди fcd\n”. entered. OLength(1));
}
else if (!serve2) //сразу на обслуживание ко второму
//кассиру
{
if (total<K*Dump) fprintf(dump. "Она получила номер 2d и поступила на обслуживание ко
второму окну\п”. entered):
to_serve2=(int)(get_normal(serve_rate. serve_disp. 0.01)*К):
if (to_serve2<=0) to_serve2=l:
serve2=ptr:
}
else //в очередь ко второму кассиру
{
q_serve2[0Length(2)]=ptr:
if (total<K*Dump) fprintf(dump. "Она получила номер id и стала в очередь ко второму
окну, номер в очереди fcd\n", entered. QLength(l));
}
}
//Завершение обслуживания i-м кассиром
void Bank:: Completed nt i)
{
int j:
completed++:
//Если это не первый уход, делаем статистические записи
if (from_outbank!--l)
{
fprintf(time_outbank. ”fcd\n“. from_outbank):
out_ave-out_ave*(l-1.0/(completed-l))+((float)from_outbank)/(completed-l):
}
from_outbank-0: //сброс счетчика
if (i“l) //обслуживание завершил первый кассир
{
//Трассировка, статистика по среднему времени пребывания
if (total<K*Dump) fprintf(dump. "fcd - заявка N fcd покинула систему\п”. total, servel-
>id):
fprintffsojourn. “id\n". servel->time):
soj_ave-soj_ave*(1-1.0/completed)+((f1oat)(servel->time))/completed:
delete servel:
if (QLengthd) !=0) //если очередь не пуста, ставим
//на обслуживание первую заявку
{
servel=q_servel[O]:
for(j=0:j<(B-l):j++) q_servel[j]=q_servel[j+lj: //сдвиг очереди
q_servel[B-l]=NULL:
to_servel=(int)(get_normal(serve_rate. serve disp. 0.01)*K):
if (to_servel<=0) to_servel=l:
}
else //очередь пуста
Листинг 16.1. Файл classesie.h с описанием классов 327
{
servel=NULL:
q_servel[O]=NULL;
to_servel=-l:
}
}
else //обслуживание завершил второй кассир.
//те же самые действия
{
if (total<K*Dump) fprintf(dump. "fcd - заявка N fcd покинула систему\п". total.
serve2->id):
fprintf(sojourn. "fcd\n". serve2->time):
soj_ave-soj_ave*(l-1.0/completed)+((float)(serve2->time))/completed;
delete serve2:
if (QLength(2)!=0)
{
serve2-q_serve2[0]:
for(j=0;j<(B-l):j++) q_serve2[j]=q_serve2[j+l]:
q_serve2[B-l]-NULL:
to_serve2=(int)(get_normal(serve_rate. serve_disp. 0.01)*K);
if (to_serve2<=0) to_serve2-l:
}
else
{
serve2-NULL:
q_serve2[0]-NULL;
to_serve2--l;
}
}
}
//Переход из i-й очереди
void Bank::Transition(int i)
{
int tmp:
transition++:
if (1—1)
{
tmp-q_servel[QLength(1)-1]->1d;
if (total<K*Dump) fprintf(dump. "Id - переход заявки N £d с полосы fcd\n". total, tmp.
i):
//Переставляем заявку из хвоста первой очереди в хвост второй
q_serve2[QLength(2)]-q_servel[QLength(1)-1];
q_servel[QLength(1)-1]-NULL;
}
else //из второй очереди - в первую
{
tmp=q_serve2[QLength(2)-1]->1d:
if (total<K*Dump) fprintf(dump. - переход заявки N £d с полосы fcd\n". total, tmp.
i):
q_servel[QLength(l)]=q_serve2[QLength(2)-l]:
q_serve2[QLength(2)-1]=NULL:
}
}
328 Глава 16. Раздельные очереди ограниченной длины с переходами заявок
//Диспетчер
void Bank::run()
{
int j. realTime:
If (to_servel>0) to_servel--:
if (from_outbank!=-l) from_outbank++:
if (to_servel==0) Completed):
if (to_serve2>0) to_serve2--:
if (to_serve2=0) Completed):
to_arrival--;
if (to_arrival--0) ArrivalO:
//Проверка выполнения условия перехода. Для отключения переходов выделенные строки
//следует удалить
if ((QLength(2)-QLengthO))>1) Transition(2):
else if ((QLength(l)-QLength(2))>l) Transition(l);
//Инкремент счетчика проведенного времени у всех заявок в системе
if (servel!-NULL) (servel->time)++;
if (serve2!=NULL) (serve2->time)++;
for(j-0:j<B:j++)
{
if (q_servel[j]!=NULL) (q_servel[j]->time)++;
if (q_serve2[j]!-NULL) (q_serve2[j]->time)++:
}
//Каждую реальную (не модельную!) единицу времени - пересчет средних значений
if ((total+l»K-=0)
{
realTime=(total+l)/K:
j=Busy()+QLength(l)+QLength(2):
fprintf(num_1nbank. "fcd\n". j):
num_ave=num_ave*(l-1.0/realTime)+((float)j)/realTime:
quel_ave=quel_ave*(l-1.0/realTime)+((float)QLength(l))/realTime:
que2_ave=que2_ave*(l-1.0/realTime)+((float)0Length(2))/realTime:
ro_ave=ro_ave*(l-1.0/realTime)+((float)Busy())/realTime:
}
}
Листинг 16.2. Функция main()
#define N 100000 //время моделирования
#include "classesl6.h“
int mainO
{
int i:
Bank b:
num_inbank=fopen("num_inbank". "wt"):
time_outbank=fopen("time_outbank". "wt”):
sojourn=fopen("sojourn". "wt"):
dump=fopen("dump”. "wt"):
srand((unsigned)time(0)):
for(tota1=0L:tota1<N:tota1++)
b.runO:
//Закрытие файлов сбора статистики
fclose(num_inbank); fclose(time_outbank);
fclose(sojourn); fclose(dump):
16.5. Анализ результатов 3 29
//Вывод на печать результатов имитационного эксперимента
printf("Всего поступлений в систему fcd\n”. entered):
printf("Завершили обслуживание fcd\n". completed):
printfCflonfl отвергнутых заявок fc.3f\n". (float)reject/entered);
printfCKonnqecTBo переходов fcd\n“. transition);
printfC'CpeflHAfl длина первой очереди fc.3f\n". quel_ave):
printfCCpeflHflfl длина второй очереди fc.3f\n". que2_ave):
printft"Среднее число заявок в системе fc.3f\n". num_ave):
printf("Среднее время пребывания в системе fc.3f\n". soj_ave/K):
printf("Средний интервал между выходами заявок fc.3f\n”. out_ave/K):
printf("3arpy3Ka (среднее число занятых кассиров) fc.3f\n". ro_ave):
)
16.5. Анализ результатов
Усреднение по результатам 1000 прогонов по 1000 единиц времени каждый дало:
О количество поступлений — 2019;
О доля потерянных заявок — 0,076;
О количество переходов — 174;
О средняя длина первой очереди — 1,36;
О средняя длина второй очереди — 1,16;
О среднее число заявок в системе — 4,35;
О среднее время пребывания заявки в системе — 2,33 единиц времени;
О средний интервал между выходами заявок из системы — 0,54 единиц времени;
О загрузка системы — 1,84.
Проверим, как влияют на эти показатели наличие или отсутствие переходов.
Программно «отключить» переходы очень просто: для этого надо закомментиро-
вать в методе Bank::run() две выделенные строки кода. Результат следующий: со-
храняются все показатели с точностью до двух знаков после запятой, за исклю-
чением средних длин очередей, которые становятся равны, соответственно, 1,43
и 1,09. Последнее легко объяснить. Первый кассир имеет приоритет, поэтому
очередь к нему должна быть больше. Но возможность перехода несколько сгла-
живает это различие. В самом деле, потенциально большая длина первой оче-
реди приводит к тому, что переходы совершаются преимущественно во вторую
очередь. Если переходов нет, то, разумеется, длина первой очереди становится
больше, а второй — меньше. А вот среднее время пребывания заявки в системе
остается неизменным. Указанный факт является экспериментальным подтвер-
ждением известного закона сохранения среднего времени ожидания при различ-
ных дисциплинах обслуживания, установленного Л. Клейнроком [19]. Примени-
тельно к нашей системе проведем следующий мысленный эксперимент.
Допустим заявка, стоящая в очереди пятой, перешла в другую очередь длиной
три. Тем самым данная заявка, конечно, сократила свое время ожидания. После
перехода в систему поступает новая заявка. Не будь перехода, она стала бы во
вторую очередь четвертой, а так она в любом случае будет пятой. Таким образом,
330 Глава 16. Раздельные очереди ограниченной длины с переходами заявок
сокращение времени ожидания первой заявки привело к удлинению такового
для второй, то есть, грубо говоря, среднее время ожидания сохраняется.
Таким образом, возможность переходов из одной очереди в другую дает клиен-
там лишь иллюзию свободы выбора и хороша только в рекламных целях, так как
позволяет скрасить ожидание в очереди игрой. Это своеобразная лотерея — если
кому-то и повезет, то обязательно за счет другого.
Приведем пример трассировки событий в течение первых десяти единиц време-
ни (листинг 16.3). Время дано в модельных единицах.
Листинг 16.3. Пример трассировки событий. Единица времени промасштабирована
с коэффициентом 100
33 - поступила новая заявка. Она получила номер 1 и поступила
на обслуживание к первому окну
105 - поступила новая заявка. Она получила номер 2 и поступила
на обслуживание ко второму окну
126 - заявка № 1 покинула систему
184 - заявка № 2 покинула систему
211 - поступила новая заявка. Она получила номер 3 и поступила
на обслуживание к первому окну
222 - поступила новая заявка. Она получила номер 4 и поступила
на обслуживание ко второму окну
242 - поступила новая заявка. Она получила номер 5 и встала в очередь
к первому окну, номер в очереди 1
298 - поступила новая заявка. Она получила номер 6 и встала в очередь
ко второму окну, номер в очереди 1
313 - заявка № 4 покинула систему
315 - поступила новая заявка. Она получила номер 7 и встала в очередь
ко второму окну, номер в очереди 1
334 - заявка № 3 покинула систему
345 - поступила новая заявка. Она получила номер 8 и встала в очередь
к первому окну, номер в очереди 1
387 - поступила новая заявка. Она получила номер 9 и встала в очередь
к первому окну, номер в очереди 2
438 - заявка Ns 5 покинула систему
469 - заявка № 6 покинула систему
474 - поступила новая заявка. Она получила номер 10 и встала в очередь
ко второму окну, номер в очереди 1
51В - поступила новая заявка. Она получила номер И и встала в очередь
к первому окну, номер в очереди 2
560 - заявка № 8 покинула систему
574 - поступила новая заявка. Она получила номер 12 и встала в очередь
к первому окну, номер в очереди 2
607 - заявка № 7 покинула систему
607 - переход заявки N» 12 с полосы 1
611 - поступила новая заявка. Она получила номер 13 и встала в очередь
к первому окну, номер в очереди 2
637 - поступила новая заявка. Она получила номер 14 и встала в очередь
ко второму окну, номер в очереди 2
670 - заявка № 9 покинула систему
707 - заявка Ns 10 покинула систему
710 - заявка Ns 11 покинула систему
754 - поступила новая заявка. Она получила номер 15 и встала в очередь
16.5. Анализ результатов 331
к первому окну, номер в очереди 1
790 - поступила новая заявка. Она получила номер 16 и встала в очередь
к первому окну, номер в очереди 2
794 - заявка It 13 покинула систему
803 - поступила новая заявка. Она получила номер 17 и встала в очередь
к первому окну, номер в очереди 2
813 - заявка It 12 покинула систему
813 - переход заявки It 17 с полосы 1
867 - поступила новая заявка. Она получила номер 18 и встала в очередь
к первому окну, номер в очереди 2
894 - заявка It 15 покинула систему
914 - заявка It 14 покинула систему
925 - поступила новая заявка. Она получила номер 19 и встала в очередь
ко второму окну, номер в очереди 1
937 - поступила новая заявка. Она получила номер 20 и встала в очередь
к первому окну, номер в очереди 2
985 - заявка It 16 покинула систему
На рис. 16.1 изображена гистограмма для интервала выходного потока.
Так как мы не можем менять количество каналов, рассмотрим зависимости по-
казателей системы от размера буфера. На рис. 16.2-16.5 изображены зависимо-
сти от размера буфера, соответственно, вероятности потери заявки, среднего
числа заявок, среднего времени пребывания заявки в системе и загрузки си-
стемы.
332 Глава 16. Раздельные очереди ограниченной длины с переходами заявок
Размер буфера
Рис. 16.2. Зависимость вероятности потери заявки от размера буфера
Рис. 16.3. Зависимость среднего числа заявок в системе от размера буфера
16.5. Анализ результатов 333
Размер буфера
Рис. 16.4. Зависимость среднего времени пребывания заявки в системе от размера буфера
Рис. 16.5. Зависимость загрузки системы от размера буфера
334 Глава 16. Раздельные очереди ограниченной длины с переходами заявок
Понятно, что система перегружена, так как интенсивность входного потока рав-
на интенсивности обслуживания. С увеличением размера буфера фактор его
ограниченности играет все меньшую роль, а фактор перегрузки, наоборот, все
больше проявляет себя, делая поведение системы неустойчивым. Из графиков,
изображенных на рис. 16.3-16.5, однако, видно, что некое подобие стационарно-
го режима все же устанавливается, так как среднее число заявок и среднее время
пребывания заявки в системе как будто не уходят в бесконечность, а коэффици-
ент загрузки не доходит до максимального значения — 2, его рост останавливает-
ся около 1,96-1,97. Асимптотические значения этих величин достигаются в слу-
чае неограниченного буфера. Решить вопрос об оптимальном размере буфера
и критерии оптимальности предлагается в упражнении.
Выводы
1. Указанное в условии правило перехода является простым и легко реализу-
емым только для случая двух очередей.
2. Для сбора статистики по выходному потоку в класс, моделирующий обслу-
живающее устройство, требуется ввести дополнительное поле данных.
3. Результаты имитационного моделирования согласуются с законом сохране-
ния среднего времени ожидания — возможность переходов не оказывает
влияния на этот показатель.
4. Для рассмотренной системы можно поставить задачу оптимизации ограниче-
ния на длину очереди (размера буфера).
Задания для самостоятельной работы
Предположим, что каждая единица времени пребывания заявки в системе обхо-
дится банку в сумму pit а в случае потери заявки банк несет убыток р2, причем р2
существенно превышает pt. С ростом размера буфера В среднее число заявок Аср
и среднее время ожидания Гср увеличиваются, а вероятность потери R снижает-
ся. Следовательно, функция убытков /(В) = р{(Тср + Ncp) + p2R имеет минимум
при некотором В*. Задайте значения р, ир2 в соответствии с собственными пред-
ставлениями о ситуации и решите численно задачу минимизации f(B).
Глава 17
Групповое обслуживание
с несколькими этапами
и двойной очередью:
работа оптового магазина
Основные вопросы, рассматриваемые
в данной главе:
□ Моделирование группового
обслуживания
□ Первичная и вторичная очереди.
Программная реализация переходов
заявок из одной очереди в другую
□ Особенности информационной
зависимости между классами
□ Что такое минимальный индекс
группы и на что он влияет
□ Сколько клерков должно работать
в магазине?
336 Глава 17. Групповое обслуживание с несколькими этапами и двойной очередью
17.1. Описание системы
В оптовом магазине используется новая процедура обслуживания клиентов. Кли-
енты, попадая в магазин, определяют по каталогу наименования товаров, кото-
рые они хотели бы приобрести. После этого клиента обслуживает клерк, кото-
рый идет на расположенный рядом склад и приносит необходимый товар. Кли-
ент ожидает дважды: сначала приема заказа, затем — его выполнения. Каждый
из клерков может обслуживать одновременно не более шести клиентов. Время,
которое затрачивает клерк на путь к складу, равномерно распределено на интер-
вале от 0,5 до 1,5 мин. Время поиска нужного товара зависит от числа наимено-
ваний, которые клерк должен найти на складе. Это время нормально распределено
с математическим ожиданием, равным утроенному числу искомых наименова-
ний, и среднеквадратичным отклонением, равным одной пятой математического
ожидания. Следовательно, если, например, со склада надо взять товар одного на-
именования, время на его поиск будет нормально распределено с математиче-
ским ожиданием, равным 3 мин, и среднеквадратичным отклонением, равным
36 с. Время возвращения со склада равномерно распределено па интервале от 0,5
до 1,5 мин. По возвращении со склада клерк рассчитывается со всеми клиента-
ми, которых он обслуживает. Время расчета с клиентом равномерно распределе-
но па интервале от 1 до 3 мин. Расчет производится в том порядке, в каком к
клерку поступали заявки на товар. Интервалы между моментами поступления
заявок на товары от клиентов экспоненциально распределены с математическим
ожиданием, равным 2 мин. Клиентов в магазине обслуживают три клерка. Цель
моделирования — определить следующее:
О загрузку клерков;
О среднее время, необходимое на обслуживание одного клиента с момента по-
дачи заявки на товар до оплаты счета за покупку;
О среднее число заявок, удовлетворяемых клерком за один выход на склад.
Продолжительность имитационного прогона составляет 1000 мин.
17.2. Модельное время
Так как время в задаче размерное, за единицу модельного времени примем се-
кунду. Равномерное распределение будем генерировать непосредственно в секун-
дах, а нормальное и экспоненциальное — в минутах, с последующим умножением
на 60 и округлением до ближайшего целого.
17.3. Классы и объекты
Итак, в задаче описана открытая многоканальная система с неограниченным бу-
фером, имеющая, однако, ряд довольно интересных особенностей. Мы уже неод-
нократно моделировали многоканальные системы одним объектом, в котором
несколько каналов представлялись массивом (или массивами) чисел. Описанная
17.3. Классы и объекты 337
система сложнее. Обслуживание заявки в канале (клерком) представляет собой
многоэтапный процесс с параметром — количеством единовременно обслужива-
емых клиентов. Эта дисциплина носит название групповое обслуживание. Таким
образом, текущее состояние процесса обслуживания характеризуется не одним
значением — временем, оставшимся до завершения, а несколькими — номером
этапа, временем, оставшимся до завершения этапа, и числом клиентов. Таких
этапов четыре — путь на склад, поиск товара, путь обратно, расчет. На первых
трех этапах число клиентов остается постоянным, на четвертом оно постепенно
уменьшается до нуля, так как расплатившийся клиент покидает систему.
Интересна здесь также система очередей. Время пребывания клиента в магазине
состоит из двух стадий. Сначала он стоит в общей очереди (назовем ее первич-
ной) и ждет, когда один из клерков обратит на него внимание и примет заказ.
Клиенты, находящиеся в первичной очереди, не связаны пока ни с каким клер-
ком, а относятся как бы ко всему магазину в целом. После приема заказа клиент
переходит в очередь, состоящую из людей, которые сделали заказ и ждут возвра-
щения «своего» клерка со склада с товаром (назовем ее вторичной). Вторичная
очередь соотносится с конкретным клерком, ее длина, согласно условию задачи,
не может превышать шести, а количество вторичных очередей равно трем — об-
щему количеству клерков. В противоположность этому первичная очередь мо-
жет быть только одна и ограничений на длину не имеет. Разумеется, как первич-
ная, так и любая из вторичных очередей может в течение некоторого времени
быть пустой.
Заметим, что именно такая система обслуживания принята сейчас в большинст-
ве магазинов, торгующих компьютерной и оргтехникой, в том числе и в том,
услугами которого при необходимости пользуется автор. Некоторую аналогию
можно провести и с обслуживанием в ресторане, где клиент тоже сначала ждет
прихода официанта, а затем — исполнения заказа, но понятие очереди в этом
случае не столь акцентированное, да и система взаимоотношений официанта
с клиентом ресторана все-таки несколько сложнее.
Все сказанное свидетельствует в пользу того, что логику работы клерка и всего
магазина в целом надо отделить друг от друга и определить в разных классах,
иначе сам принцип объектного моделирования будет выхолощен. Введем классы
Клерк (Clerk) и Магазин (Shop). Прежде чем перечислять их поля данных, подчерк-
нем следующее обстоятельство. В условии задачи дано максимальное значение
объема группы — шесть. В общем случае можно ограничить и минимальное зна-
чение, которое назовем минимальным индексом группы (МИГ). Смысл нового по-
нятия заключается в том, что свободный клерк не начнет обслуживание клиен-
тов до тех пор, пока длина первичной очереди не станет равна значению МИГ.
Если к моменту накопления нужного количества клиентов свободных клерков
несколько, выбор клерка, начинающего обслуживать эту группу, осуществляется
случайным образом. Понятно, что стандартное значение МИГ — единица. О том,
что же дает введение МИГ и на что оно влияет, мы поговорим в разделе 17.5 при
анализе результатов.
Еще один вопрос — взаимные ссылки между классами. В рассматриваемой си-
стеме объекты классов Clerk и Shop не являются равноправными, так как каждый
338 Глава 17. Групповое обслуживание с несколькими этапами и двойной очередью
из объектов класса Clerk входит в зону ответственности единственного объекта
класса Shop, но не наоборот. Поскольку объект Shop управляет системой в целом,
ему необходим доступ к любому объекту Cl erk для передачи ему различных со-
общений (например, указание принять заказ). Каждый из клерков отвечает толь-
ко за себя, и ему ссылка на Shop не нужна, так как всем информационным обме-
ном руководит Shop. Поскольку перекрестных ссылок нет, тип указателя при
объявлении поля класса Shop можно указывать в явном виде (Cl erk**), если, ко-
нечно, класс Clerk описан в header-файле раньше, чем класс Shop.
Перечислим поля данных класса Clerk.
Неизменяемые поля:
О среднее время нахождения клерка в пути (60 с);
О максимальное отклонение от среднего для времени нахождения клерка в пути
(30 с);
О среднее время расчета одного клиента (120 с);
О максимальное отклонение от среднего для времени расчета одного клиента
(60 с);
О уникальный номер клерка.
Изменяемые поля:
О вторичная очередь. Моделируется массивом указателей на объекты класса
Client. Если клерк свободен, очередь пуста;
О клиент, с которым в данный момент производится расчет. Поле данных имеет
смысл только при нахождении клерка в состоянии Расчет;
О текущее число клиентов, у которых принят заказ и которые ожидают возвра-
щения клерка. Не то же самое, что длина вторичной очереди, поскольку в
процессе расчета клиентов длина очереди меняется. Это поле данных харак-
теризует именно размер пакета заказов. Равен -1, если клерк свободен;
О время, оставшееся до прибытия клерка на склад. Значение поля данных ак-
тивно только в состоянии движения на склад за товаром. В любом другом со-
стоянии равно -1;
О время, оставшееся до возвращения клерка со склада. Значение поля данных
активно только в состоянии движения со склада с товаром. В любом другом
состоянии равно -1;
О время, оставшееся до окончания расчета текущего клиента. Значение поля
данных активно только в состоянии расчета клиентов. В любом другом со-
стоянии равно -1;
О время, оставшееся до завершения поиска товаров. Значение поля данных ак-
тивно только в состоянии поиска товаров при нахождении клерка на складе.
В любом другом состоянии равно -1;
О время, прошедшее с момента принятия заказа. Поле данных необходимо для
сбора статистики о длительности цикла клерка — от принятия заказа до рас-
чета последнего клиента. Если клерк свободен, значение равно -1.
17.4. События и методы 339
Поля данных класса Shop.
Неизменяемые поля:
О количество клерков (3). Для удобства реализации сделано глобальной пере-
менной;
О максимальный объем одного заказа (6). Для удобства реализации сделано
глобальной переменной;
О минимальный индекс группы (1);
О средняя интенсивность входного потока (0,5 заявок в минуту);
О массив указателей на объекты класса Clerk.
Изменяемые поля:
О первичная очередь клиентов. Из-за отсутствия ограничений на максималь-
ную длину моделируется связным списком;
О время, оставшееся до прибытия следующей заявки из входного потока;
О текущая длина первичной очереди (вычисляемое поле).
Отношения дружественности между классами построены следующим образом:
друзьями класса Client являются Clerk и Shop, другом класса Clerk — класс Shop.
17.4. События и методы
Каждому из пяти возможных состояний клерка соответствует событие, в резуль-
тате которого он покидает это состояние и переходит в другое. Каждому собы-
тию, в свою очередь, сопоставлен отдельный метод. Перечислим эти события:
1. Прибытие клерка на склад.
2. Завершение поиска заказанного товара.
3. Прибытие клерка с товаром к ожидающим его клиентам.
4. Завершение расчетов с очередным клиентом.
5. Принятие заказа у клиентов из первичной очереди.
Подробнее остановимся на реализации последнего метода. Если первичная оче-
редь не пуста и ее длина достигла значения МИГ, объект Shop пытается препору-
чить как можно больше клиентов одному из свободных клерков. После того как
клерк выбран, ему посылается сообщение, соответствующее методу 5, с двумя
параметрами: указателем на первичную очередь, чтобы клерк мог скопировать
часть ее клиентов во вторичную, и количеством клиентов, заказы у которых ма-
газин предписывает принять клерку. Возвращает же он объекту Shop указатель
на клиента первичной очереди, который теперь становится в этой очереди пер-
вым, то есть на новую голову связного списка. Первичную очередь Shop продви-
гает сам. Все эти действия выполняет метод-диспетчер run().
Методы класса Shop:
О прибытие нового клиента из внешнего потока и постановка его в первичную
очередь;
340 Глава 17. Групповое обслуживание с несколькими этапами и двойной очередью
О выбор клерка, который должен принять заказ. Этот метод выбирает случай-
ным образом одного клерка из числа свободных в данный момент.
Более подробные комментарии к программной реализации модели даны в лис-
тингах 17.1, 17.2.
Листинг 17.1. Файл classesl7.h с описаниями классов
#include<cstdio>
#include<cstdlib>
#include<ctime>
#i nclude<cmath>
using namespace std:
^include "List.h”
int M=3; //число клерков
int MAX_CLIENT=6: //максимальный объем заказа
int entered=O; //счетчик поступлений
int completed=O: //счетчик обслуженных клиентов
long int *ro: //массив счетчиков загрузки клерков
float serve_ave=O: //для расчета средней длины цикла клерка
float num_ave=0: //среднее число клиентов в магазине
float soj_ave=0: //среднее время пребывания клиента в магазине
float quel_ave=O: //средняя длина первичной очереди
float orders_ave=D: //средний объем заказа
int total_ordered=0; //счетчик заказов (походов клерков за товаром)
//Файлы для сбора статистики
FILE *sojourn: //статистика по суммарному времени пребывания клиента в магазине
FILE *num; //статистика по суммарному количеству клиентов в магазине
FILE *order: //статистика по объему заказов
long int total; //счетчик тактов модельного времени
//Стандартный протокол класса Client
class Client
int id;
int time;
public:
friend class Clerk;
friend class Shop:
Clientdnt i):
Client::Client(int i)
{
id=i:
time=O; //счетчик времени, проведенного клиентом в системе
//Протокол класса Clerk
class Clerk
const static int path_ave=60;
const static int path_offset=30:
const static int money_ave=120:
const static int money_offset=60:
int id; //уникальный номер клерка
Листинг 17.1. Файл classesl7.h с описаниями классов 341
Client **queue:
Client *serving:
int units;
int to_dpath:
int to_bpath;
int to_calculate:
int to_search;
int from_onder:
public:
friend class Shop;
Clerkdnt i);
-Clerk»:
void Arrival0;
void TakeAllO:
void ComeBackO;
void Completed»:
//Принять заказ от асе
//массив указателей на клиентов вторичной очереди
//указатель на клиента, с которым производится расчет
//объем текущего заказа
//время, оставшееся до конца пути на склад
// до конца пути к клиентам
//время, оставшееся до окончания расчета с клиентом
//время, оставшееся до окончания поиска товара
//время, прошедшее с момента приема заказа
//конструктор
//деструктор
//прибытие на склад
//весь товар найден
//возвращение со склада
//расчет с клиентом завершен
клиентов из первичной очереди
ListNode<Client>* TakeOrder(ListNode<Client> *q. int асе):
void run(): //диспетчер
int QLengthO; //вычисление текущей длины вторичной очереди
int GetStateO: //определение состояния клерка; 0 - свободен.
//1 - в пути на склад и т. д.
}:
//Протокол класса Shop
class Shop
{
const static int accumulation^:
const static float 1nput_rate=0.5:
ListNode<Client> *queue;
Clerk **workers:
int qjength:
int to_arrival:
public:
ShopCClerk **w): -shopO;
void Arrival 0:
void run():
int ChoiceO:
}:
//минимальный индекс группы
//интенсивность входного потока
//указатель на голову первичной очереди
//массив указателей на объекты
//класса Clerk
//текущая длина первичной очереди
//время до прибытия следующего клиента
//прибытие нового клиента
//диспетчер
//выбор свободного клерка для передачи заказа
//Конструктор. Создает клерка, который первоначально свободен, состоянием
Clerk::ClerkCint i)
{
int j;
id=i:
queue=new Client *[MAX_CLIENT]:
for(jM:j<MAX_CLIENT:j++)
queue[j]=NULL:
servi ng=NULL:
units=-l:
* to_dpath--l;
to_bpath=-l:
to_calculate=-l;
to_search=-l;
342 Глава 17. Групповое обслуживание с несколькими этапами и двойной очередью
from_order=-l:
}
//Деструктор. Освобождает память
Clerk::-Clerk()
{
for(1nt i=O:i<QLength();i++) delete queued]:
delete [] queue:
if (serving) delete serving:
}
//Вычисление текущей длины вторичной очереди
int Clerk::QLengthО
{
int k;
for(k=0:k<MAX_CLIENT:k++)
if (queue[k]==NULL) return(k):
return(MAX_CLIENT);
}
//Определение текущего состояния клерка
int Clerk::GetState() {
if (units==-l) return(O): //клерк свободен
if (to_dpath>0) return(l): //клерк в пути на склад
if (to_search>0) return(2): //клерк на складе ищет товар
if (to_bpath>0) return(3): //клерк в пути со склада с товаром
if (to_calculate>0) return(4): //клерк рассчитывает клиента
}
//Прием заказа у первых асе клиентов из первичной очереди. Указатель
//на нее передается из метода run() класса Shop
ListNode<Client>* Clerk::TakeOrder(ListNode<Client> *q, int acc)
{
int i;
ListNode<Client> *ptr=q. *ptrl;
//Заполнение вторичной очереди указателями на объекты первичной. Первичная //очередь
будет продвинута объектом Shop. По окончании цикла ptr будет //указывать на новую
голову первичной очереди
ford=0:i<acc:i++)
{
queued ]=ptr->Data();
ptr=ptr->Next():
delete ptrl: //освобождаем память, выделенную под элемент
//первичной очереди в методе Shop::Arrival О
}
units=acc: //объем заказа
from_order=0: //начинаем отсчитывать длительность исполнения заказа
to_dpath-get_uniform(path_ave. path_offset): //переходим в следующее состояние
total_ordered++: //инкремент счетчика заказов
//Пересчет среднего объема заказа
orders_ave=orders_ave*(l-1.0/total_ordered)+((float)umts)/total_ordered:
fprintfCorder. "!Sd\n". acc):
return(ptr):
}
Листинг 17.1. Файл classesl7.h с описаниями классов 343
//Прибытие клерка на склад
void Clerk:: Arrival О
{
to_dpath--l:
//Разыгрываем время поиска товара
to_search=(i nt)(get_normal(3*units.0.6*units,0.01)*60):
if (to_search<-0) to_search=l;
}
//Поиск товара завершен
void Clerk::TakeAl1()
{
to_search--l:
to_bpath=get_uniform(path_ave. path_offset):
}
//Вернулись co склада
void Clerk::ComeBack()
{
int 1:
to_bpath=-l:
serving=queue[0]; //ставим на расчет первого клиента
//из вторичной очереди
for(i=0:i<(MAX_CLIENT-l);i++) //продвигаем вторичную очередь
queued]-queue[i+l]:
queue[MAX_CLIENT-l]=NULL:
//Разыгрываем длительность расчета
to_calculate=get_uniform(money_ave. moneyoffset):
}
//Расчет с клиентом завершен
void Clerk::Completed()
{
int i:
to_calculate=-l;
completed++: //инкремент счетчика обслуженных клиентов
soj_ave-soj_ave*(1-1.0/completed)+(f1 oat)(servi ng->time)/completed:
fprintfCsojourn. “1.3f\n". (float)(serving->time)/60);
delete serving; //удаление обслуженного клиента из системы
serving=NULL:
//Расчет произведен со всеми клиентами
if (QLength()-=0)
{
units=-l:
serve_ave-serve_ave*(l-1.0/total_ordered)+((float)from_order)/total_ordered:
from_order=-l:
return:
}
//Вторичная очередь не пуста. Ставим на расчет следующего клиента
serving=queue[0]:
for(i=0:i<(MAX_CLIENT-l):i++)
queued ]=queue[i+l]:
queue[MAX_CLIENT-1]=NULL;
to_calculate=get_uniform(money_ave. money offset):
}
344 Глава 17. Групповое обслуживание с несколькими этапами и двойной очередью
//Диспетчер класса Clerk
void Clerk::run()
{
if (to_dpath>0) to_dpath--: //клерк в пути на склад. Декремент //оставшегося времени пути
if (to_dpath--0) ArrivalO: //клерк прибыл на склад
if (to_bpath>0) to_bpath--: //клерк в пути со склада. Декремент //оставшегося времени пути
if (to_bpath--0) ComeBackO; //клерк вернулся со склада
if (to_calculate>0) to_calculate--; //клерк рассчитывает клиента. //Декремент оставшегося времени //расчета
if (to_calculate==0) CompletedO: //клерк завершил расчет клиента
if (to_search>0) to_search--: //клерк ищет на складе товар. //Декремент оставшегося времени поиска
if (to_search==0) TakeAlK): if (units!=-l) //клерк завершил поиск товара
{
from_order++:
ro[id-l]++: //инкремент счетчика загрузки клерка
}
}
//Конструктор. Сообщаем новому магазину о том. кто будет в нем клерками
Shop::Shop(Clerk **w)
{
workers=w:
queue-NULL;
q_length=0:
to_arrival-(int)(get_exp(input_rate)*60); //ждем прибытия клиента
}
//Деструктор. Удаляет клиентов в первичной очереди
Shop::-ShopО
{
wh i1е(queue) queue=L i stDelete<Cli ent>(queue.queue):
}
//Выбор свободного клерка
int Shop::Choice()
{
int kJ:
int mas[M]: //массив для сохранения номеров свободных клерков
к-0:
for(i=0:i<M:i++) //выписываем номера свободных клерков и подсчитываем
//их количество
{
if (workers[i]->GetState()“0)
{
mas[k]=i:
k++:
}
}
if (k=-0) return(-l): //все клерки заняты
Листинг 17.1. Файл classesl7.h с описаниями классов 345
if (к—1) return(mas[O]): //свободен только один клерк
i=rand()fck; //разыгрываем среди всех свободных клерков
//случайным образом
return(mas[i]);
}
//Прибытие нового клиента
void Shop::Arrival()
{
Client *p; ListNode<Client> *ptr;
to_a rri va1=(i nt)(get_exp(i nput_rate)*60):
if (to_arrival—0) to_arrival=l:
entered**: //инкремент счетчика поступлений
//Создаем новый объект класса Client и новый элемент связного списка
p=new Cl lent(entered): //память будет возвращена в методе
//Clerk::Completed() после завершения обслуживания клерком. Либо в
//деструкторе класса Shop, если к моменту завершения моделирования объект
//будет находиться в первичной очереди. Либо в деструкторе класса Clerk,
//если к моменту завершения моделирования объект будет находиться во
//вторичной очереди
ptr=new ListNode<Client>(p. NULL); //память будет возвращена в методе
//Clerk::TakeOrder() при переходе
' //заявки во вторичную очередь
//Поступивший клиент - первый в очереди
if (q_length—0)
{
queue-ptг:
q_length-l:
return;
}
//Первичная очередь уже есть, новый клиент занимает место в хвосте
ListAdd<Client>(queue.ptr);
q_length++;
}
//Диспетчер
void Shop::run()
{
int k. p. t. i. j:
ListNode<Client> *ptr;
if (to_arrival>0) to_arrival--:
if (to_arrival—0) ArrivaK);
//Выбираем минимум между текущей длиной первичной очереди и максимальным
//объемом одного заказа
if (q_length<MAX_CLIENT) p-q_length;
else p-MAX_CLIENT;
if (p<accumulation) ; //МИГ еще не достигнут
else //МИГ достигнут
{
k-Choice(): //заказ будет передан k-му клерку
if (k—-l) ; //свободных клерков нет. нужно ждать
else
{
//Сообщаем k-му клерку о том. что ему нужно выбрать из первичной очереди
//р клиентов и принять у них заказы
queue*wrkers[k]->TakeOrder(queue.p):
346 Глава 17. Групповое обслуживание с несколькими этапами и двойной очередью
q_length-=p: //декремент длины очереди на величину р
}
}
if (queue) //в первичной очереди еще остались клиенты
{
ptr-queue;
//Инкремент времени пребывания в системе для всех клиентов
while(ptr) //...в первичной очереди
{
(ptr->Data()->time)++:
ptr=ptr->Next();
}
}
for(i-0:i<M:i++)
{
//...находящихся на расчете у клерков
if (workers[i]->serving!=NULL) (workers[i]->serving->time)++;
//...находящихся во вторичных очередях
forCj-O:j<MAX_CLIENT:j++)
{
if (workers[i]->queue[j]!=NULL) (workers[i]->queue[j]->time)++:
}
}
//Подсчитываем общее число клиентов в магазине
p-q_length;
for(k-0;k<M:k++)
{
p+-workers[k]->QLength():
if (workers[k]->GetState()»-4) p++:
}
//Каждую минуту - запись и обработка статистических данных
if ((total+l)160=-0)
{
t-(total+l)/60:
fprintf(num. “ld\n". p);
num_ave-num_ave*( 1 -1.0/t)+((f 1 oat )p) /t:
quel_ave-quel_ave*(1-1.0/t)+((f1oat)q_length)/t:
}
}
Листинг 17.2. Функция main()
#define N 60000 //время моделирования в секундах
#include “classesl7.h“
int mainO
{
int i; float si:
//Выделяем память для хранения счетчиков загрузки клерков
ro=new long int[M]:
//Открываем файлы для хранения статистики
num=fopen("num”. "wt");
sojourn=fopen("sojourn". "wt”):
order=fopen('order”. "wt”):
srand((unsigned)time(O)):
//Выделение памяти для объектов класса Clerk
17.5. Анализ результатов 347
Clerk **mas = new Clerk *[M]:
for(i=0:i<M:i++) //инициализация клерков
{
mas[i]=new ClerkCi+1);
ro[i]-0:
}
Shop s(mas): //инициализация магазина
for(total-OL;total<N:total++) //основной моделирующий цикл
{
for(i-0;i<M:1++)
mas[i]->run();
s.runO:
}
//Удаление объектов
ford-0; i<M;i++)
delete mas[i]:
delete [] mas;
delete [] ro:
fclose(num): fclose(order):
fcloseCsojourn);
//Вывод на печать результатов имитационного эксперимента
printfC'Bcero поступлений id\n". entered);
printf("Обслужено клиентов Xd\n". completed):
sl=0:
for(i-0:i<M;i++)
sl-sl+((float)ro[i])/total:
printf("Среднее ЧИСЛО занятых клерков fc.3f\n". si):
printfC'CpeflHee время периода занятости клерка !L3f\n". serve_ave/60):
printfC'CpeflHBB длина первичной очереди t.3f\n". quel_ave):
printfC'CpeflHee число клиентов в магазине i.3f\n". num_ave);
printfC'CpeflHee время пребывания клиента в магазине S.3f\n". soj_ave/60)
printf("Средний объем одного заказа i.3f\n", orders_ave):
I
17.5. Анализ результатов
Приведем цифровые данные, полученные при 1000-минутном моделировании.
Усредненные результаты тысячи прогонов округлены до двух знаков после запя-
той:
О количество поступлений — 501,21;
О обслужено клиентов — 488,78;
О среднее число занятых клерков — 2,85;
О средняя длительность периода занятости клерка — 14,77 мин;
О средняя длина первичной очереди — 2,61;
О среднее число клиентов в магазине — 11,75;
О среднее время пребывания клиента в магазине — 23,45 мин;
О средний объем одного заказа — 2,61;
О среднее время пребывания в первичной очереди — 23,45 - 14,77 = 8,68 мин.
348 Глава 17. Групповое обслуживание с несколькими этапами и двойной очередью
Приведенные результаты получены при МИГ = 1. Интересно проследить влия-
ние этого параметра на показатели функционирования системы. Представление
об этом дает табл 17.1.
Таблица 17.1. Влияние параметра МИГ на показатели функционирования
Показатель Значение МИГ
1 2 3 4 5 6
Число занятых клерков 2,85 2,76 2,70 2,67 2,64 2,60
Период занятости клерка 14,77 18,81 22,33 25,69 28,68 31,36
Длина первичной очереди 2,61 3,03 3,36 3,95 4,35 5,02
Число клиентов в системе 11,75 12,84 13,85 15,33 16,65 18,26
Время пребывания клиента 23,45 25,58 27,75 30,78 33,60 37,00
в системе
Объем заказа 2,61 3,43 4,14 4,83 5,44 6,00
Время ожидания в первичной очереди 8,68 6,77 5,43 5,09 4,92 5,64
Проанализируем эту таблицу. Итак, мы видим, что увеличение МИГ позволяет
сократить среднее время ожидания клиента в первичной очереди, хотя, вроде бы,
должно быть наоборот! Почему так происходит? Дело в том, что при увеличении
МИГ части клиентов приходится ждать дольше (пока не прибудут еще клиенты
и длина очереди не достигнет значения МИГ), а части клиентов — меньше,
так как при меньшем значении МИГ они уже не застали бы клерка на месте,
а так он ждет, пока группа клиентов не достигнет необходимого объема.
Чтобы понять суть дела, поставим мысленный эксперимент. Допустим, клиенты
прибывают по одному в час, а обслуживание одного клиента от начала и до кон-
ца занимает 5 мин. Понятно, что в этом случае использование значения МИГ > 1
невыгодно, так как время ожидания в первичной очереди ни для одного клиента
не уменьшится. Таким образом, при малых нагрузках увеличивать МИГ нецеле-
сообразно1. А вот при возрастании нагрузки на систему время ожидания в пер-
вичной очереди уменьшается при увеличении МИГ. И чем больше нагрузка, тем,
по-видимому, более целесообразно увеличивать МИГ. Из табл. 17.1 мы видим,
что убывание среднего времени ожидания в первичной очереди прекращается
лишь при МИГ = 6, так как нагрузка достаточно высока — 2,85 при предельном
значении 3.
Известный закон сохранения (если где-то прибудет, то где-то и убудет) полно-
стью подтверждается и здесь. Сокращение времени ожидания в первичной оче-
реди приводит к ухудшению остальных параметров. Так, с увеличением МИГ
возрастает не только период занятости клерка (это естественно, так как ему
в среднем приходится выполнять за это время больше заказов), но и суммарное
1 Под нагрузкой понимается стандартная для теории массового обслуживания величина —
интенсивность входного потока в систему (в первичную очередь), умноженная на среднее
время обслуживания клерком (включая все стадии) и деленная на количество клерков.
17.5. Анализ результатов 349
время пребывания клиента в магазине. Тут уж программист свое дело сделал, те-
перь слово за директором — ему решать, что важнее.
Если проникнуться психологией покупателя, то ему, наверное, комфортнее ожи-
дать во вторичной очереди, зная, что заказ у него уже принят и его скоро прине-
сут. Ясно, что даже если время ожидания во вторичной очереди и увеличивается
немного, то из нее клиент все равно уже не уйдет, а будет ожидать выполнения
своего заказа. А вот если ему надоест ожидать в первичной очереди, то магазин
может потерять клиента. Таким образом, возможно, все-таки есть смысл пойти
на такое решение.
Проверим теперь нашу гипотезу об эффективности увеличения МИГ при боль-
ших и малых нагрузках. Моделировать уменьшение нагрузки будем увеличени-
ем числа клерков, при этом станем фиксировать, при каком МИГ время ожида-
ния в первичной очереди перестает убывать. Результаты приведены в табл. 17.2.
Таблица 17.2. Среднее время ожидания в первичной очереди при разных МИГ и числе клерков
МИГ Число клерков
3 4 5 6 7 8
1 8,68 2,84 1,10 0,47 0,22 0,11
2 6,77 1,45 0,43 0,19 0,14 0,14
3 5,43 0,91 0,34 0,26 0,26
4 5,09 0,76 0,48
5 4,92 0,80
6 5,64
Гипотеза полностью подтвердилась — чем меньше нагрузка, тем меньшие значе-
ния МИГ дают положительный эффект. При числе клерков 8 и более значения
МИГ > 1 никакого выигрыша в эффективности не приносят.
На рис. 17.1-17.3 приведены графики зависимости от числа клерков, соответст-
венно, загрузки системы, среднего числа клиентов и среднего времени пребыва-
ния клиента в системе. Из графиков видно, что оптимальное число клерков —
четыре. Дальнейшее увеличение числа клерков к значимому улучшению показа-
телей функционирования не приводит. Добавление же четвертого клерка все
еще позволяет существенно улучшить эти показатели. Приведем их:
О среднее число занятых клерков — 3,21;
О средняя длительность периода занятости клерка — 8,64;
О средняя длина первичной очереди — 0,48;
О среднее число клиентов в магазине — 5,77;
О среднее время пребывания клиента в магазине — 11,48 мин;
О средний объем одного заказа — 1,36;
О среднее время пребывания клиентов в первичной очереди — 11,48 - 8,64 =
= 2,84 мин.
3 50 Глава 17. Групповое обслуживание с несколькими этапами и двойной очередью
Число клерков
Рис. 17.1. Зависимость коэффициента загрузки от числа клерков
Число клерков
Рис. 17.2. Зависимость среднего числа клиентов в магазине от числа клерков
Выводы 351
Число клерков
Рис. 17.3. Зависимость среднего времени пребывания клиента
в магазине от числа клерков
Выводы
1. Состояние клерка в процессе обслуживания клиентов характеризуется сово-
купностью значений трех полей данных.
2. В системе имеется два типа очередей — первичная (общая, неограниченной
длины, относится к магазину) и несколько вторичных (конечной длины, каж-
дая относится к своему клерку).
3. Прежде чем начать обслуживание, клерк накапливает некоторое количество
заказов (МИГ).
4. Связь клерка с магазином — односторонняя. Поэтому ни приведение бести-
повых указателей, ни предварительное объявление класса при реализации свя-
зи не требуется.
5. Увеличение МИГ при большой нагрузке позволяет сократить среднее время
ожидания клиента в первичной очереди. При малой нагрузке увеличение МИГ
нецелесообразно.
6. Результаты моделирования показали, что оптимальное количество клерков при
заданных в описании задачи условиях — четыре.
352 Глава 17. Групповое обслуживание с несколькими этапами и двойной очередью
Задания для самостоятельной работы
1. Замените в условии задачи экспоненциальное распределение входного пото-
ка распределением Парето с сохранением математического ожидания. Сохра-
няются ли при этом установленные качественные зависимости? В какую сто-
рону меняются количественные?
2. Проанализируйте загрузку каждого клерка (каждый элемент массива го в от-
дельности). Одинакова ли она? Затем замените в методе ChoiceO правило вы-
бора свободного клерка следующим: из всех свободных клерков выбирается
клерк с наименьшим номером (id). Что можно сказать о сравнительной за-
грузке клерков в этом случае?
Глава 18
Дискретно-непрерывное
моделирование:работа
камерной печи
Основные вопросы, рассматриваемые
в данной главе:
□ Задание длительности обслуживания:
взгляд с более общих позиций
□ Решение дифференциального
уравнения с дискретным случайным
воздействием
□ Что произойдет при увеличении числа
камер
3 54 Глава 18. Дискретно-непрерывное моделирование: работа камерной печи
18.1. Описание системы
В процессе обработки на металлургическом заводе стальные отливки поступают
в камерную печь с интервалом, распределенным экспоненциально с математиче-
ским ожиданием 2,5. Отливки нагреваются в печи в целях рационализации даль-
нейшего хода технологического процесса. Изменение температуры отливки в печи
описывается следующим дифференциальным уравнением:
^L=(H-hi)C, (18.1)
at
где hj — температура i-й отливки в камере; С — коэффициент скорости нагрева,
равный X + 0,1, где X — нормально распределенная величина с математическим
ожиданием 0,05 и среднеквадратичным отклонением 0,01; Н — температура печи,
которая раскаляется до 2600 °F1 с постоянным коэффициентом скорости нагрева
0,2, то есть
^=(2600 -Н) -0,2. (18.2)
at
Отливки влияют друг на друга так, что помещение «холодной» отливки в печь
снижает температуру в печи и изменяет тем самым время нагрева находящихся в
ней в данный момент отливок. Снижение температуры равно разности темпера-
тур печи и отливки, деленной на количество отливок в печи. Всего в печи 10 ка-
мер. Когда «холодная» отливка поступает к заполненной печи, она складируется
рядом с печью. Предполагается, что начальная температура поступающих отли-
вок равномерно распределена на интервале 400-600 °F. Предполагается также,
что все складируемые перед печью отливки при загрузке в нее имеют температу-
ру 400 °F.
Стратегия управления технологическим процессом состоит в том, что отливки
в печи нагреваются до тех пор, пока температура одной из них не достигнет
2200 °F. Как только эта температура достигается, все отливки с температурой
выше 2000 °F удаляются. Начальная температура печи равна 1650 °F.
Целью исследования является моделирование описанной системы для получе-
ния оценок следующих величин:
О времени нагрева отливок;
О конечного распределения температур отливок;
О времени ожидания «холодных» отливок перед печью;
О загрузки камерной печи.
18.2. Как моделировать систему?
Поставленная задача существенно отличается от всех тех, которые рассматрива-
лись прежде. Основной вопрос, который нужно решить, — каким образом зада-
1 Чтобы получить температуру по шкале Цельсия из температуры по шкале Фаренгейта,
нужно вычесть из последней 32 и умножить результат на 5/9.
18.2. Как моделировать систему? 355
вать время обслуживания? Чтобы ответить на него, давайт г вспомним, как мы
делали это раньше, с более общих позиций. Состояние процесса обслуживания
задавалось одним параметром — числом тактов модельного времени, оставшихся
до завершения обслуживания. Значение этого параметра с течением времени из-
менялось по следующему закону:
P(O=P(0)-i, (18.3)
где р(0) генерировалось датчиком случайных чисел в соответствии с заданным
в условии задачи законом распределения длительности обслуживания. Обслу-
живание считалось завершенным в тот момент, когда значение параметра p(t)
достигало нуля. Однако выражение (18.3) — не единственно возможный закон
изменения параметра, характеризующего состояние процесса обслуживания,
время до завершения — не единственно возможная смысловая интерпретация
этого параметра, конечное значение которого тоже не обязательно должно быть
равно нулю. Понятно, что в качестве параметра в задаче про камерную печь вы-
ступает температура отливки, конечные значения задаются (2200 — для первой
отливки, 2000 — для остальных), а вот аналогом формулы (18.3) является систе-
ма дифференциальных уравнений (18.1)-(18.2). Сама по себе система очень
проста: сначала решаем (18.2) как уравнение с разделяющимися переменными,
затем, подставив готовое H(f), решаем (18.1) методом вариации произвольной
постоянной. Поскольку мы не договаривались решать в этой книге дифференци-
альные уравнения, выпишем только конечный результат (подробности можно
посмотреть в любом учебнике, например в [49]):
Н(О = 2600-950ехр(-0,2£); (18.4)
h(t -t0) = C2 exp(-C(t -t0)) + 2600 + (26OO~H^o))c exp(_0i2(t -t0)), (18.5)
0,2 C
r . 1650C-520 r „ , ,
где C2 = n(t0 ) +-------; С — описанный в условии коэффициент скорости
0,2 С
нагрева; t — абсолютное время, прошедшее с момента начала наблюдения за пе-
чью (когда ее температура была равна 1650 °F); t0 — время загрузки в камеру
печи отливки, то есть момент начала ее нагрева; Л(?о) — начальная температура
отливки при ее загрузке в камеру.
Если бы температура печи и отливок действительно описывалась функциями (18.4)
и (18.5), время нагрева отливки до заданной температуры можно было рассчи-
тать заранее, при ее загрузке (при начальной температуре отливки 400-600 °F и
температуре печи в момент загрузки 1650 °F это время равно приблизительно 15,
что нетрудно увидеть, построив график функции (18.5)). Но задача существенно
осложняется тем, что температура печи, а значит, и отливок, подвержена влия-
нию случайного фактора — загрузки «холодных» отливок, поступающих в печь
в случайные моменты времени и снижающих температуру печи. Понятно, что
в результате этого время нагрева отливок увеличивается. Понятно также, что для
описания изменения температур функции (18.4) и (18.5) не подходят, так как
функции H(t) и h(t) становятся случайными.
356 Глава 18. Дискретно-непрерывное моделирование: работа камерной печи
Мы не будем здесь углубляться в теорию стохастических дифференциальных
уравнений [26], [48], а предложим метод расчета, пригодный для данного кон-
кретного случая. Разобьем шкалу времени на интервалы точками 0, tlt t„ ...,
являющимися (кроме нуля) моментами загрузки «холодных» отливок в печь.
Заметим, что так как освобождение камер печи происходит группами, то и в ка-
ждый из моментов Г, в печь может поступить не одна, а несколько (до десяти) от-
ливок. Тогда на каждом из интервалов [£,-_,; ?,] изменение температуры печи и от-
ливок описываются обычными (не стохастическими) уравнениями (18.1)-(18.2)
с начальными условиями и полученными из расчетов на предыду-
щем интервале, и последующим пересчетом температуры печи Н после загрузки
«холодной» отливки. Если эти условия известны, можно получить решение си-
стемы (18.1)—(18.2) на интервале [£,_,; £,], которое будет выглядеть так:
//(0 = 2600-^ ехр(-0,2(Г-Г.)); =2600-7/(6.); (18.6)
h{t) = C2 ехр(-С(6-6.)) + 2600 + —^-^-ехр(-0,2(6-6,)); (18.7)
0,2 С
С, =Л(6.)-2600—
2 0,2-С ”*
Начальные моменты времени здесь, в отличие от (18.4) и (18.5), как бы выравни-
ваются, так как и для отливок, и для печи текущей «точкой отсчета» становится
момент последнего поступления «холодной» отливки.
Разумеется, мы заранее не знаем значение правой граничной точки tt текущего
интервала «нестохастичности». Оно определяется динамически при возникнове-
нии события «Загрузка в печь новой отливки». Момент генерации моделиру-
ющей программой этого события и становится значением Г,. В этот момент пере-
считывается температура печи значения Ct и С2, t, полагается равным
и продолжаются расчеты по формулам (18.6) и (18.7).
Момент завершения обслуживания 6зав фиксируется, когда хотя бы для одной от-
ливки значение /г(бзав) достигает 2200 °F. После этого выполняются все предпи-
санные в постановке задачи действия, подробные комментарии к которым при-
ведены в листинге 18.1.
18.3. Модельное время
Время в задаче — безразмерное. Коэффициент масштабирования нужен, так как
средний интервал входного пуассоновского потока равен 2,5, а этого слишком
мало для того, чтобы без явного ущерба для точности моделирования округлять
дробные значения результатов экспоненциального ГСЧ. Если время масштаби-
руется с коэффициентом К, то это должно найти и отражение в функциях (18.6)
и (18.7) — каждое вхождение в них разности t-t, следует заменить выражением
(t-t. )/К. В программе принято К = 10.
18.4. Классы и объекты 357
18.4. Классы и объекты
Для моделирования нагрева отливок создадим два класса: Отливка (Client) и Печь
(Stove). В этой задаче вновь стоит уделить внимание классу Client. Ведь кроме
неизменяемого поля данных — времени, проведенного в системе, для объектов
этого класса необходимо поддерживать также информацию об их температуре —
как текущей, так и в последний момент времени £,, предшествующий поступле-
нию в печь новой отливки. Но объекты класса Client не являются постоянными
в системе — они появляются и исчезают в процессе моделирования. Поэтому
этот класс не имеет метода гип() и каких-либо других методов, кроме конструк-
тора и вывода на печать; руководство всеми действиями по изменению состоя-
ния отливок возлагается на печь. Перечислим поля данных класса Client.
Неизменяемые поля:
О средняя температура отливок при поступлении в систему (500);
О максимальное отклонение начальной температуры отливок (100);
О уникальный идентификатор поступившей отливки;
О константа С — коэффициент скорости нагрева.
Изменяемые поля:
О общее время, проведенное в системе;
О время, проведенное в печи;
О текущая температура;
О температура в последний из моментов tP предшествующих поступлению но-
вой отливки в печь. Имеет смысл только для заявок, находящихся в печи.
Поля данных класса Stove.
Неизменяемые поля:
О количество камер (10);
О температура, при достижении которой одной из отливок начинается процесс
выгрузки из печи (2200 °F);
О температура, при которой отливки выгружаются из печи вместе с отливкой,
достигшей температуры 2200 °F (2000 °F);
О начальная температура отливки, поступающей в печь после ожидания в оче-
реди (400 °F);
О средняя интенсивность входного потока (0,4).
Изменяемые поля:
О отливки, находящиеся в печи. Моделируются массивом указателей на объек-
ты класса Client, так как их количество ограничено числом камер;
О время, оставшееся до прибытия следующей заявки из входного потока;
О температура печи в момент последней загрузки отливки;
О текущая температура печи;
358 Глава 18. Дискретно-непрерывное моделирование: работа камерной печи
О время, прошедшее с момента последней загрузки отливки. Это поле данных
и есть разность t-t. из уравнений (18.6) и (18.7);
О очередь отливок перед печью. Моделируется связным списком, так как ее
размер не ограничен;
О текущая длина очереди.
18.5. События и методы
Выделим три события, приводящих к изменению состояния печи:
1. Поступление новой отливки из внешнего входного потока (Arrival).
2. Достижение одной из нагреваемых отливок температуры 2200 °F (Complete).
3. Загрузка «холодной» отливки в печь (putlnStove).
Следует отметить, что по значениям изменяемых полей данных метод run() от-
слеживает наступление только первых двух событий и вызывает для них методы
(листинги 18.1, 18.2). Метод putlnStoveO вызывается либо из метода Arrival О,
если прибывшая отливка не застает очереди перед печыо, либо из метода Completed
после завершения выгрузки нагретых отливок, если очередь перед печью есть.
Методу putlnStoveO передаются два параметра — загружаемая отливка и номер
камеры, в которую нужно эту отливку загрузить.
Для принятия программой решений целесообразно оформить в виде методов
следующие алгоритмические действия:
О вычисление текущей температуры печи по формуле (18.6);
О перевычисление текущей температуры печи при загрузке «холодной» отливки;
О вычисление текущей температуры всех нагреваемых отливок по формуле (18.7);
О вычисление текущего количества загруженных камер;
О определение наименьшего номера свободной камеры.
Листинг 18.1. Файл classes!8.h с описанием классов
#include<cstdio>
#include<cstdlib>
#include<ctime>
#include<cmath>
using namespace std:
#include "List.h"
#include "normal.h”
int K-10:
int entered=C:
int completed=O:
int queue_stove=0:
int groups=0;
FILE *timeInQueue:
FILE *timeInStove:
FILE *outGroup:
//коэффициент масштабирования времени
//счетчик поступлений отливок в систему
//счетчик нагретых отливок
//счетчик отливок, загруженных в печь (для расчета
//среднего времени ожидания в очереди)
//счетчик групп отливок, загруженных в печь (размер
//группы от 1 до 10)
//сбор статистики по длительности ожидания в очереди
//сбор статистики по длительности нагрева в печи
//сбор статистики по размеру выгруженной группы
Листинг 18.1. Файл classeslS.h с описанием классов 359
FILE *outTemper;
FILE *stoveTemper;
float que_ave-0;
float stove_ave-0:
float queN_ave-0:
float ro_ave-0:
float group_ave=0:
float temper_ave=O;
long int total:
//Протокол класса Client
class Client
{
static const float temp_median-500;
static const float temp_offset-100:
//сбор статистики по температуре выгруженных отливок
//сбор статистики по температуре печи
//для расчета среднего времени ожидания
//для расчета среднего времени нагрева
//для расчета средней длины очереди
//для расчета среднего числа занятых камер
//для расчета среднего размера выходной группы
//для расчета средней температуры выгруженной отливки
//счетчик тактов модельного времени
int id:
float C:
int total_time:
int stove_time:
float c_temper;
float s_temper:
public:
friend class Stove:
Clientdnt a):
};
//Конструктор
Client::C1ient(int a)
{
id-a:
//Разыгрываем начальную температуру
s_temper=get_uni form(500,100):
total_time=0:
stove_time=0:
c_temper-s_temper;
//Разыгрываем константу С
C=get_normal(0.05.0.01.0.0001)+0.1:
}
//Протокол класса Stove
class Stove
{
static const int M-10;
static const float thresholdl-2200:
static const float threshold2-2000:
static const float s_client-400;
static const float input_rate-0.4;
Client **serving;
//средняя начальная температура
//максимальное отклонение начальной
//температуры отливки от среднего
//значения
//уникальный идентификатор отливки.
//равен текущему значению счетчика
//поступлений
//коэффициент скорости нагрева
//отливки
//счетчик общего времени.
//проведенного в системе
//счетчик времени, проведенного
//в печи
//текущая температура отливки
//температура отливки в ближайший
//момент времени ti
отливки
//число камер
//нагреваемые отливки
360 Глава 18. Дискретно-непрерывное моделирование: работа камерной печи
Int to_arrival:
float temperjast;
float temper_cur:
float from_last:
L1stNode<Client> *queue:
Int qjength;
public:
Stove(float a);
-Stoved;
void setTemperatured:
void resetTemperature(float a):
//«холодной» отливки
float setCTemperature(Client *client):
int getNumberO;
int firstAvailO;
void putInStove(C11ent *client. int k):
void ArrivaK):
void Completed;
void rund;
}:
//время до прибытия новой заявки
//температура печи в ближайший
//момент времени t(i)
//текущая температура печи
//текущая длительность
//«нестохастического» интервала
//очередь отливок перед печью
//текущая длина очереди
//конструктор, параметр -
//начальная температура печи
//деструктор
//расчет текущей температуры печи
//перерасчет текущей температуры
//печи после поступления
//расчет текущей температуры
//нагреваемой отливки
//поступление в печь «холодной»
//отливки
//прибытие новой отливки
//завершение нагрева одной
//или более отливок
//диспетчер
//Конструктор. Создает пустую печь, очереди
Stove:;Stove(float a)
{
int i;
from_last-0;
to_a rri va1-(i nt)(get_exp(i nput_rate)*K):
if (to_arrival“0) to_arrival-l;
temper_cur-a:
temper_la st=temper_cur:
serving-new Client *[M]:
for(i-0:i <M:1++) servi ng[i]-NULL:
queue-NULL;
q_length-0:
}
нет
//выделение памяти под массив
//указателей
//инициализация массива
//инициализация связного
//списка
//Расчет текущей температуры печи по формуле (18.6)
void Stove;:setTemperature().
{
float cl:
cl-2600-temper_last:
temper_cur=2600-cl*exp(-0.2*from_last/K);
}
//Расчет текущей температуры нагреваемой отливки по формуле (18.7)
float Stove::setCTemperature(Cllent *client)
18.5. События и методы 361
{
float a.b.d. cl. с2:
a-client->s_temper;
b-client->C:
cl-2600-temper_last:
c2-a-2600-cl*b/(0.2-b);
d=c2*exp(-b*from_last/K)+2600+exp(-0.2*froni_last/K)*cl*b/(0.2-b);
client->c_temper=d:
return(d):
}
//Определение числа занятых камер
int Stove::getNumber()
{
int 1. s-0;
for(i-0:i<M:i++)
if (serving[i]!=NULL) s++:
return(s);
}
//Определение наименьшего номера свободной камеры
int Stove::flrstAva11()
{
for(1nt i-0:i<M:i++)
if (serving[i]==NULL) return(i);
return(-l):
}
//пересчет текущей температуры печи. Параметр - температура загружаемой //отливки
void Stove::resetTemperature(float а)
{
temper_cur=temper_cur-(temper_cur-a)/(getNumber()+l):
}
//Загрузка в печь «холодной» отливки
void Stove::put!nStove(Cllent *client. int k)
{
int 1:
client->stove_time=0: //инициализация счетчика длительности
//нагрева
client->c_temper-client->s_temper: //текущую температуру отливки полагаем
//равной начальной
resetTemperature(client->c_temper); //пересчитываем температуру печи
//Назначение текущих начальных температур для печи и отливок
temper_last-temper_cur:
for(i-0;i<M:i++)
{
if (serving[i]!=NULL)
{
servi ng C1]->s_temper=servi ng[i]->c_temper;
}
}
from_last=0:
serving[k]-client: //отливка загружена
}
362 Глава 18. Дискретно-непрерывное моделирование: работа камерной печи
//Прибытие новой отливки
void Stove::Arrival()
{
Client *ptr; int k;
L1stNode<Client> *p:
to_arri val-(1 nt)(get_exp(1nput_rate)*K):
if (to_arrival==0) to_arrival=l:
entered++:
ptr-new Client(entered):
k=firstAvail():
if (k!=-l) //есть
{
queue_stove++:
que_ave-que_ave*(1-1.0/queue_stove);
fprintf(timeInQueue."O\n”);
putInStove(ptr. k):
}
else
{
p-new ListNode<C11ent>(ptr, NULL):
//разыгрываем новое время между прибытиями
//инкремент счетчика поступлений
//создаем новую отливку. Память
//будет возвращена в методе
//Completed либо деструктором
//есть ли свободная камера?
q_length++:
if (q_length==l) queue=p:
else ListAdd<Client>(queue.p):
}
}
//время ожидания равно нулю
//загружаем отливку в печь
//нет. надо ставить отливку в очередь
//создаем новый элемент связного
//списка. Память будет
//возвращена в методе Complete
//после загрузки отливки
//в печь либо деструктором
//инкремент длины очереди
//очередь была пуста
//очередь существовала, ставим
//отливку в конец
//Завершение нагрева
void Stove: Completed
{
int g. 1. a. k: float b:
groups++: //инкремент счетчика групп
g=0:
//Выгружаем все отливки, температура которых достигла 2200 градусов
for(i=0:i<M:1++)
{
if (serving[1]!=NULL)
if (serving[i]->c_temper>-threshold2) //контрольный случай
{
g++: //инкремент счетчика размера группы
completed++: //инкремент счетчика выгруженных отливок
a-serving[i]->stove_time:
fprintf(timeInStove."Xd\n". а/К): //записываем время нагрева
//Пересчет среднего времени нагрева
stove_ave=stove_ave*(1-1.0/completed)+((f1oat)а)/completed:
b-servTng[i]->c_temper:
//Пересчет средней температуры выходящей отливки
temper_ave=temper_ave*(1-1.0/completed)+((float)b)/completed:
fprintf(outTemper."X.2f\n". b):
Листинг 18.1. Файл classesl8.h с описанием классов 363
delete serving[i]; //возвращаем память
serving[i]-NULL; //камера свободна
}
}
//Пересчет среднего размера группы
group_ave-group_ave*(1-1.О/groups)+((float)g)/groups:
fpr1ntf(outGroup."Xd\n". g):
If (q_length“0) return; //очередь отсутствует, ничего загружать не надо
while(queue) //очередь имеется, загружаем из нее в печь
//столько отливок, сколько возможно
{
k-firstAvail(): //есть ли еще свободные камеры?
if (k=--l) break; //свободных камер больше нет. выходим из цикла
//Свободная камера есть
queue_stove++: //инкремент счетчика загруженных отливок
//Пересчет среднего времени ожидания в очереди
a=queue->Data()->total_time:
que_ave=que_ave*(1-1.0/queue_stove)+((f1oat)а)/queue_stove:
fp rintf(ti meInQueue.”Xd\n". a/К):
//Устанавливаем начальную температуру отливки 400 °F. так как она ожидала
//в очереди
queue->Data()->s_temper=s_cllent;
put!nStove(queue->Data(). k); //загружаем отливку из очереди в печь
LlstNode<Cllent> *ptrl-queue;
queue=queue->Next(); //продвигаем очередь
delete ptrl; //возвращаем память, выделенную для элемента очереди
qjength--; //декремент длины очереди
}
}
//Метод-диспетчер
void Stove::run()
{
int i. a; ListNode<Client> *ptr;
if (to_arrival>0) to_arrival--;
if (to_arrival==0) ArrivalO; //поступление новой отливки
from_last++: //инкремент текущей длины
//«нестохастического» интервала
setTemperature(); //рассчитываем текущую температуру печи
//Рассчитываем текущую температуру всех отливок в печи
for(i=0:i<M;i++)
if (serving[i]!=NULL) setCTemperature(serving[i]):
//Проверяем, не завершить ли процесс нагрева
for(1=0;i<M;i++)
{
if (serving[i]’-NULL)
{
if (serving[i]->c_temper>“thresholdl)
{
CompleteO;
break: //метод CompleteO делает все.
//что требуется и для других отливок.
//поэтому цикла продолжать не надо
}
}
}
364 Глава 18. Дискретно-непрерывное моделирование: работа камерной печи
//Для всех отливок в печи делаем инкремент...
for(i=0;1<M;1++)
{
If (serving[1]!=NULL)
{
(serving[1]->total_tlme)++: //...времени пребывания
(serving[1]->stove_time)++: //...времени нагрева
}
//Для всех отливок в очереди делаем инкремент времени пребывания
ptr=queue:
whlle(ptr)
{
(ptr->Data()->total_t1me)++:
ptr=ptr->Next():
}
}
//В каждую единицу реального времени собираем статистику
if (Ctotal+DIK-O)
{
a-(total+l)/K:
queN_ave=queN_ave*(1-1.0/a)+((f1oat)q_length) / a;
ro_ave=ro_ave*(l-1.0/a)+(float)(getNumber())/a:
fprintf(stoveTemper."Xd X.2f\n". a. temper_cur):
}
}
Листинг 18.2. Функция main()
#define N 100000 //длительность моделирования
#lnclude "classeslB.h"
int main()
{
int i: float si:
//Открытие файлов сбора статистики
timeInQueue=fopen("timeInQueue". "wt"):
timelnStove-fopenC'timelnStove". "wt");
outGroup=fopen("outGroup". “wt"):
outTemper=fopen("outTemper". “wt"):
stoveTemper=fopen("stoveTemper", "wt");
srandC(unsigned)tlmeCO)):
//Инициализация обьекта Печь
Stove s(650):
//модельный цикл
for(total=0L:total<N;total++)
s.runO:
fclose(timelnQueue): fclose(timelnStove):
fclose(outGroup): fclose(outTemper):
fclose(stoveTemper):
//Вывод на печать результатов эксперимента
printfC"Всего поступило отливок Xd\n”. entered):
printfC"Завершили нагрев Xd\n". completed):
printf("Количество вышедших из печи групп Xd\n". groups):
printfC"Среднее число занятых камер $.3f\n". ro_ave):
printfC"Среднее время ожидания $.3f\n", que_ave/K):
printf("Среднее время нагрева X.3f\n”. stove_ave/K):
18.6. Анализ результатов 365
printf("Средняя длина очереди X.3f\n". queN_ave):
printf("Средний размер группы fc.3f\n". group_ave):
printf("Средняя температура выгруженной отливки X.3f\n". temper_ave);
}
18.6. Анализ результатов
По усредненным результатам тысячи прогонов по 10000 единиц времени каж-
дый характеристики системы таковы:
О число поступлений — 4078;
О завершили нагрев — 4069;
О количество групп — 686,4;
О среднее число занятых камер — 8,19;
О среднее время ожидания в очереди — 33,7;
О среднее время нагрева — 20,1;
О средняя длина очереди — 1,38;
О средний объем выгруженной группы — 5,93;
О средняя температура выгруженной отливки — 2140.
Рис. 18.1. Зависимость температуры печи от времени
366 Глава 18. Дискретно-непрерывное моделирование: работа камерной печи
На рис. 18.1 показан фрагмент графика изменения температуры печи. Монотон-
ное возрастание температуры каждый раз прерывается воздействием случайного
фактора — поступления одной или нескольких «холодных» отливок, и темпера-
тура падает на ту или иную величину, зависящую от температуры новой отливки
и количества нагреваемых отливок. На этом же графике жирной линией показан
график изменения температуры печи в условиях отсутствия случайного фактора.
Мы видим, что решение (18.4) обыкновенного дифференциального уравнения
довольно быстро позволяет получить предельную температуру нагрева 2600 °F,
а наличие случайного воздействия не позволяет печи разогреться до этой темпе-
ратуры.
Проанализируем зависимость показателей системы от количества камер. На
рис. 18.2 изображен график изменения среднего числа загруженных камер. Мы
видим, что при 10 камерах этот показатель равен 8,2, а предельное значение —
8,6. Поэтому дальнейшее увеличение числа камер нецелесообразно — в целом
увеличения производительности не произойдет, если, конечно, не увеличить од-
новременно и интенсивность входного потока. Если же нашей целью является
снижение времени ожидания в очереди, то есть смысл добавить еще одну или
даже больше камер, так как в результате этот показатель снизится существенно.
Так, при десяти камерах он равен 33,7 ед. времени, при одиннадцати — 19,8, при
двенадцати — 12,6 и т. д.
Число камер
Рис. 18.2. График изменения среднего числа загруженных камер
18.6. Анализ результатов 367
Число камер
Рис. 18.3. График изменения среднего времени ожидания в очереди
Число камер
Рис. 18.4. График изменения среднего времени нагрева в печи
368 Глава 18. Дискретно-непрерывное моделирование: работа камерной печи
Интересно, что, в отличие от среднего времени ожидания, среднее время нагрева
при увеличении числа камер возрастает, правда, незначительно (рис. 18.3-18.5).
Объяснить это можно так. Если камер стало больше, то чаще появляется воз-
можность загружать в печь «холодные» отливки, следовательно, чаще происхо-
дит и снижение температуры печи, вызываемое этим событием, а значит, общее
время нагрева возрастает.
В заключение отметим, что эксперименты с изменением количества камер вызы-
вают довольно быстрый переход от нестационарного режима к достижению по-
казателями асимптотических значений. Так, например, при наличии шести ка-
мер можно наблюдать явное отсутствие стационарного режима, ибо среднее
время ожидания равно 3704, а средняя длина очереди 254 (эти точки не показа-
ны на графиках). А уже при наличии десяти камер такие показатели, как среднее
число загруженных камер, среднее время нагрева и средняя длина очереди,
очень близки к своим предельным значениям.
Число камер
Рис. 18.5. График изменения средней длины очереди перед печью
Выводы
1. Длительность обслуживания заявки рассчитывается из решения кусочно-де-
терминированного дифференциального уравнения с учетом воздействия дис-
Задания для самостоятельной работы 369
кретного случайного фактора (загрузка холодных отливок). Для выполнения
расчетов в класс, представляющий обслуживающее устройство (печь), необ-
ходимо ввести несколько чисто вычислительных, немоделирующих методов.
2. Кроме времени, проведенного в системе, объекты-заявки (отливки) должны
хранить еще одну непрерывную величину — температуру.
3. Каналы обслуживания (камеры) моделируются массивом, очередь перед об-
служивающим устройством (печью) — связным списком.
4. При увеличении числа камер некоторые из показателей производительности
системы улучшаются, некоторые — ухудшаются. Следовательно, говорить об
оптимальном количестве камер можно только после формулировки критерия
оптимизации.
Задания для самостоятельной работы
1. Измените фрагмент программного кода таким образом, чтобы можно было
собирать статистику по коэффициенту загрузки каждой камеры в отдельно-
сти. Так как метод Stove::firstAvai1() выбирает для помещения отливки сво-
бодную камеру с наименьшим номером, можно ожидать, что с увеличением
порядкового номера камеры ее коэффициент загрузки будет уменьшаться.
Проверьте это утверждение.
2. Измените теперь метод Stove: :firstAvaiК) так, чтобы он случайным образом
равновероятно выбирал свободную камеру среди всех свободных. Загрузка
камер в результате этого должна выровняться. Проверьте это.
Глава 19
Замкнутая система
с регулирующим объектом
танкерный флот,
обслуживающий
нефтеочистительную
установку
Основные вопросы, рассматриваемые
в данной главе:
□ Выбор единицы модельного времени
□ Программная реализация влияния
регулирующего объекта
□ Информационные зависимости между
классами
□ Каково оптимальное число танкеров?
□ Какое время требуется
регулирующему объекту для
вхождения в стационарный режим?
19.2. Модельное время 371
19.1. Описание системы
Флот, состоящий из N танкеров, перевозит сырую нефть из пункта А в пункт В.
Предполагается, что все танкеры могут быть загружены в А одновременно. В В име-
ется только один разгрузочный док, с которого разгружаемая нефть поступает в
хранилище, а затем по трубопроводу — на очистительную установку. Нефть по-
ступает в хранилище с разгружаемого в доке танкера с постоянной скоростью
48 единиц в сутки. Хранилище непрерывно снабжает сырой нефтью очиститель-
ную установку с постоянной скоростью 34,8 единиц в сутки. Разгрузочный док
работает с 6.00 до 24.00 ч. Правила безопасности требуют прекращения разгруз-
ки в момент закрытия дока. Разгрузка танкера заканчивается, когда объем остав-
шейся в нем нефти становится меньше 3 единиц.
Емкость хранилища равна 1000 единиц. Когда хранилище заполнено до предела,
разгрузка прерывается до тех пор, пока объем нефти в хранилище не снизится до
80 % от его емкости. Когда хранилище становится почти пустым (меньше 5 еди-
ниц), снабжение очистительной установки прекращается до тех пор, пока объем
нефти в хранилище не станет равным 50 единицам. Это делается для устранения
вероятности частых остановок и запусков очистительной установки.
Характеристики танкеров:
О номинальная грузоподъемность — 400 единиц;
О время в пути загруженного танкера распределено нормально с математиче-
ским ожиданием 5 суток и среднеквадратичным отклонением 1,5 суток;
О время в пути порожнего танкера распределено нормально с математическим
ожиданием 4 суток и среднеквадратичным отклонением 1 сутки;
О время погрузки распределено равномерно на интервале от 2,9 до 3,1 суток.
Необходимо смоделировать работу нефтяного терминала В в течение пяти лет
и определить оптимальное количество танкеров во флоте.
19.2. Модельное время
В качестве единицы модельного времени в этой задаче примем 1ч — это логично
и с точки зрения оптимальности значений параметров распределений, и с точки
зрения наличия у дока рабочих и нерабочих часов в течение суток. Тогда ско-
рость разгрузки танкера равна 2 ед/ч , а скорость снабжения очистительной ус-
тановки — 1,45 ед/ч. Общая длительность моделирования составит 24 365 • 5 =
= 43 800 ч.
При реализации модели необходимо сгенерировать случайную величину, равно-
мерно распределенную на промежутке [2,9; 3,1]. При переводе границ интервала
в часы получаем [69,6; 74,4], то есть граничные значения остаются дробными,
между тем как ГСЧ равномерного распределения — функция get_uniform() —
требует целочисленных аргументов. Поскольку переходить на измерение време-
ни в минутах только из-за этого неприятно, действовать будем так: перед вызо-
вом ГСЧ умножим оба граничных значения на десять, а результат, возвращен-
372 Глава 19. Замкнутая система с регулирующим объектом
ный ГСЧ, разделим на десять и округлим до ближайшего целого. Это, конечно,
внесет определенную погрешность, но ею вполне можно пренебречь (по сравне-
нию с 60-кратным увеличением длительности моделирования в случае измере-
ния времени в минутах), так как такая точность нигде, кроме как для представ-
ления времени погрузки, не требуется. В конце концов, любая имитационная
модель — это набор компромиссов.
19.3. Классы и объекты
В поставленной задаче рассмотрена обычная замкнутая система с постоянным
числом заявок. Обстоятельство, определяющее интерес к ней, — это наличие кро-
ме заявок (танкеры) и сервера (док) еще одного объекта — хранилища, состояние
которого (текущая наполненность сырой нефтью) является важной характери-
стикой для определения дальнейшего поведения системы. Другая особенность —
«законные» периоды простоя сервера в ночное время. Очистительная установка
не является для этой задачи моделируемым объектом, так как, во-первых, в ус-
ловии не представлено никакой информации о ней, во-вторых, после перекачи-
вания нефти из хранилища в очистительную установку отслеживание производ-
ственного процесса обработки нефти прекращается. Таким образом, вводим три
класса:
О класс Танкер (Tanker), представленный в системе несколькими объектами, ко-
личество которых требуется оптимизировать;
О класс Док (Dok), представленный одним объектом;
О класс Хранилище (OilHouse), представленный одним объектом.
Полный рейс (цикл) каждого танкера состоит из четырех стадий: путь в порож-
нем состоянии, загрузка, путь в загруженном состоянии, нахождение в доке. На
первых трех стадиях танкер никем не обслуживается и должен моделировать свое
поведение сам. При нахождении в доке на разгрузке состояние танкера контро-
лируется объектом Док, который выполняет все необходимые действия. С точки
зрения реализации это означает, что метод Tanker:: run() не выполняет никаких
действий, если танкер находится в доке. Существенно облегчает моделирование
предположение о том, что в порту А все танкеры могут загружаться одновремен-
но. Это позволяет моделировать состояние танкера при загрузке одним парамет-
ром — временем, оставшимся до окончания загрузки, так как очередь на загрузку
отсутствует.
Разберемся в вопросе о связях между классами. Танкер и Док должны быть связа-
ны однонаправленной связью, так как прибывший в док танкер должен каким-то
образом проинформировать док о своем прибытии, а для этого необходимо по-
слать сообщение. Между доком и хранилищем необходима двунаправленная связь.
Обоснуем это решение. Если уровень нефти в хранилище превысил критическую
отметку, оно должно послать доку сообщение с предписанием временно прекра-
тить работу. Когда же уровень нефти приходит в норму, необходимо вновь
послать доку сообщение — на этот раз о возобновлении работы. Следователь-
но, хранилище должно иметь такую возможность. Док тоже должен иметь связь
19.3. Классы и объекты 373
с хранилищем, так как работа дока оказывает непосредственное влияние на уро-
вень нефти — каждую единицу рабочего времени док повышает его на заданную
величину. Это также делается путем пересылки сообщения от дока к хранилищу.
Перечислим поля данных класса Tanker.
Неизменяемые поля:
О вместимость танкера;
О среднее значение времени пути в загруженном состоянии;
О среднеквадратичное отклонение времени пути в загруженном состоянии;
О среднее значение времени пути в порожнем состоянии;
О среднеквадратичное отклонение времени пути в порожнем состоянии;
О среднее время загрузки;
О максимальное отклонение от среднего времени загрузки;
О минимальная заполненность танкера, при которой разгрузка прекращается;
О номер танкера;
О указатель на объект класса Док.
Изменяемые поля:
О время, оставшееся до прибытия в док. Поле активно только во время нахож-
дения в пути загруженного танкера;
О время, оставшееся до прибытия в порт А. Поле активно только во время нахо-
ждения в пути порожнего танкера;
О время, оставшееся до окончания загрузки. Поле активно только во время за-
грузки;
О текущая заполненность танкера нефтью. Поле активно только при разгрузке;
его значение изменяется методами класса Док, а не Танкер.
Следующие три поля данных не участвуют в процессе моделирования, они нуж-
ны для сбора статистики:
О текущее время ожидания в очереди на разгрузку. Поле активно только при
нахождении танкера в очереди; при переходе на разгрузку значение поля дан-
ных обрабатывается, затем сбрасывается. Используется для сбора статистики
о времени ожидания;
О текущая длительность полного рейса танкера. Точка фиксации — момент на-
чала загрузки. Используется для сбора статистики о длительности полного
рейса;
О текущая длительность разгрузки. Используется для сбора статистики о дли-
тельности разгрузки.
Перечислим поля данных класса Oil House.
Неизменяемые поля:
О емкость хранилища;
О скорость снабжения нефтью очистительной установки;
374 Глава 19. Замкнутая система с регулирующим объектом
О уровень, на который должна опуститься нефть для возобновления разгрузки
танкера;
О минимальный уровень нефти в хранилище;
О уровень, на который должна подняться нефть для возобновления снабжения
очистительной установки;
О указатель на объект класса Dok.
Изменяемые поля:
О текущий уровень нефти в хранилище;
О признак состояния хранилища:
□ 0 — нет снабжения очистительной установки из-за малого количества
нефти;
□ 1 — идет снабжение очистительной установки;
□ 2 — нет снабжения хранилища из дока из-за избыточного количества нефти.
Перечислим поля данных класса Dok.
Неизменяемые поля:
О скорость снабжения хранилища нефтью;
О указатель на объект класса Oil House.
Изменяемые поля:
О указатель на разгружаемый танкер, NULL — в случае отсутствия такового;
О массив указателей на танкеры, находящиеся в очереди на разгрузку. Так как
размер очереди ограничен общим числом танкеров, используется массив;
О время, оставшееся до начала рабочего дня;
О время, оставшееся до наступления ночного перерыва;
О признак переполнения хранилища:
□ 1 — хранилище принимает нефть;
□ 0 — хранилище нефть принимать временно не может.
19.4. События и методы
Методы класса Tanker обрабатывают переходы танкера из одной стадии полного
рейса в другую. Всего их четыре:
О LoadArrivalO — завершение пути в порожнем состоянии, прибытие на погруз-
ку. Вызывается из метода Tanker: :run();
О LoadCompleteO — завершение загрузки, отправление в путь полного танкера.
Вызывается из метода Tanker: :run();
О DocArrivalO — завершение пути в заполненном состоянии, прибытие к доку.
Вызывается из метода Tanker::run();
О DokCompleteO — завершение разгрузки, отправление в путь пустого танкера.
Вызывается из метода Dok: :run().
19.4. События и методы 375
Класс Oil House моделирующих методов не имеет, все действия помещаются внут-
ри диспетчера этого класса. Класс Dok имеет следующие моделирующие методы:
О Arrival О — прибытие танкера к доку. Имеет параметр — указатель на при-
бывший танкер. Вызывается из метода Tanker: :run();
О Completed — завершение разгрузки танкера. Параметров не имеет. Вызывает-
ся из метода Dok:: run О;
О Night() — наступление ночного перерыва. Параметров не имеет. Вызывается
из метода Dok:: run();
О MorningO — наступление рабочего дня. Параметров не имеет. Вызывается из
метода Dok: :run();
О stopWorkd — прекратить разгрузку. Параметров не имеет. Вызывается из ме-
тода Oi 1 House:: run();
О resumeWorkd — возобновить разгрузку. Параметров не имеет. Вызывается из
метода OilHouse::run().
Дальнейшие комментарии к методам приведены в листингах 19.1, 19.2.
Листинг 19.1. Файл classesl9.h с описаниями классов
#include<cstdio>
#include<cstdl 1b>
#include<ctime>
#include<cmath>
using namespace std;
#include "normal.h"
int entered=0:
int completed=0:
int rounds=0:
int timeoutl=0:
int timeout2=0;
int T-l:
1 ong i nt rOU=0;
FILE *oilVolume:
FILE *fullTime:
FILE *waitTime:
FILE *nQueue;
float vol_ave=0:
float fpath_ave-O;
float queN_ave=0;
float wait_ave=0:
float serve_ave:
long int total:
//счетчик прибытий танкеров к доку
//счетчик разгруженных танкеров
//счетчик сделанных танкерами полных рейсов
//число прерываний снабжения очистительной установки
//число прерываний разгрузки танкеров
//из-за переполнения
//количество танкеров
//счетчик времени, в течение которого очистительная
//установка снабжается нефтью
//сбор статистики об уровне нефти в хранилище
//сбор статистики о времени полного рейса
//сбор статистики о времени ожидания в очереди
//сбор статистики о длине очереди
//расчет среднего уровня нефти в хранилище
//расчет средней длительности полного рейса
//расчет средней длины очереди
//расчет среднего времени ожидания
//расчет среднего времени разгрузки
//счетчик тактов модельного времени
//Протокол класса Танкер
class Tanker
{
//Неизменяемые поля данных
static const float storage=400: //грузоподъемность
static const float full_ave=5; //мат. ожидание длительности пути //загруженного танкера
376 Глава 19. Замкнутая система с регулирующим объектом
static const float full_offset-1.5; //среднеквадратичное отклонение для
//длительности пути загруженного танкера
static const float empty_ave=4; //мат. ожидание длительности пути порожнего танкера
static const float empty_offset-l: //среднеквадратичное отклонение для
//длительности пути порожнего танкера
static const float load_ave=72: //среднее время погрузки (в часах)
static const float 1oad_offset=2.4: //максимальное отклонение времени
//погрузки от среднего (в часах)
static const float min_storage=3: //остаточный обьем нефти после разгрузки
int id:
void *dok:
//Изменяемые поля данных
int to_fullPath; //сколько осталось до прибытия к доку
int to_emptyPath: //сколько осталось до прибытия на погрузку
int to_loading: //сколько осталось до конца погрузки
float cur_storage; //текущий уровень нефти в танкере
int cur_wait: //время ожидания в очереди
int cur_round: //время, прошедшее с начала рейса
int cur_serve: //время нахождения на разгрузке
public:
friend class Dok: //класс Док имеет возможность беспрепятственно
//манипулировать танкерами
Tanker(int i):
void putDokCDok *d): //настройка связи с доком
void LoadArrivaK):
void LoadCompleteO:
void DokArrivalO:
void DokCompleteO:
void run():
}:
//Протокол класса Хранилище
class Oil House
{
//Неизменяемые поля данных
static const float capacity=1000: //емкость хранилища
static const float OU_speed=1.45: //скорость снабжения нефтью очистительной
//установки (единиц в час)
static const float less_ratio=0.8; //относительный уровень нефти в
//хранилище, необходимый для возобновления разгрузки
static const float almost_empty=5: //уровень нефти в хранилище, при котором
//прекращается снабжение очистительной установки
static const float more_ratio-10: //во сколько раз должен увеличиться
//уровень нефти для возобновления снабжения очистительной установки
void *dok;
//Изменяемые поля данных
float oil Level:
int check: //состояние хранилища
publi c:
friend class Dok: //класс Док имеет возможность заливать хранилище
//сырой нефтью
OilHouseO:
void putDokCDok *d): //настройка связи с доком
Листинг 19.1. Файл classesl9.h с описаниями классов 377
void run():
};
//Протокол класса Док
class Dok
{
static const float Tanker_speed-2;
void *House:
//Изменяемые поля данных
Tanker ‘serving;
Tanker “queue:
int to_break:
int to_work:
int working;
public:
Dok():
-DokO;
void putHouseCOilHouse *p);
int QLengthO:
void Arrival(Tanker *ptr):
void Completed;
void NightO:
void MorningO;
void stopWorkd:
void resumeWork();
void run():
};
//скорость разгрузки танкера
//связь с хранилищем
//разгружаемый танкер
//очередь танкеров на разгрузку
//время, оставшееся до конца ночи
//время, оставшееся до наступления ночи
//признак перегрузки хранилища
//настройка связи с хранилищем
//вычисление текущей длины очереди
//Конструктор. Исходное состояние танкера - начало загрузки
Tanker::Tanker(int i)
{
id-i:
to_fullPath=-l;
to_emptyPath=-l;
cur_storage=0;
//Разыгрываем время погрузки танкера
to_loadi ng=get_un form((i nt)(1oad_ave*10). (i nt)(1oad_offset*10))/10:
dok=NULL:
cur_wait--l:
cur_round-0;
cur_serve--l:
}
//Прибытие танкера на погрузку. Завершение полного рейса
void Tanker::LoadArrivalО
{
to_emptyPath=-l:
//Разыгрываем время погрузки танкера
to_loadi ng=get_uniform((i nt)(1oad_ave*10). (i nt)(1oad_offset*10))/10:
rounds++; //инкремент счетчика полных рейсов
//Сбор статистики о длительности полного рейса
fpath_ave-fpath_ave*(1-1.0/rounds)+((float)cur_round)/rounds:
fprlntfCfullTime. ”fc.3f\n". ((float)cur_round)/24);
cur_round-0: //сброс счетчика длительности полного рейса
}
378 Глава 19. Замкнутая система с регулирующим объектом
//Погрузка завершена. Полный танкер убывает.
void Tanker::LoadComplete()
{
to_loading--l;
cur_storage-storage:
//Разыгрываем длительность пути
to_fullPath-(int)(get_normal(full_ave. full_pffset. 0.01)*24):
if (to_fullPath<-0) to_ful1Path=l;
}
//Прибытие танкера к доку
void Tanker::DokArrival ()
{
to_fullPath=-l;
//Танкер сообщает доку о своем прибытии
((Dok*)dok)->Arrival(this):
}
//Разгрузка танкера завершена. Пустой танкер убывает
void Tanker::DokCompleteO
{
//Разыгрываем длительность пути
to_emptyPath=(int)(get_normal(empty_ave. empty_offset. 0.01)*24);
if (to_emptyPath<=0) to_emptyPath-l;
//Сбор статистики о длительности разгрузки
serve_ave=serve_ave*(1-1.0/completed)+((fl oat)cur_serve)/comp1eted;
fprintf(fullTime. "£.3f\n", ((float)cur_round)/24):
cur_serve=-1: //сброс счетчика длительности разгрузки
}
//Диспетчер
void Tanker::run()
{
cur_round++;
//Проверка состояния танкера и вызов
if (to_fullPath>0) to_fullPath--:
if (to_fullPath“0) DokArrivaK):
if (to_emptyPath>0) to_emptyPath--;
if (to_emptyPath=0) LoadArrivalO:
if (to_loading>0) to_loading--:
if (to_loading=0) LoadCompleteO:
}
void Tanker::putDok(Dok *d) { dok=d:
//инкремент счетчика длительности
//полного рейса
нужного метода
//загруженный танкер в пути
//танкер прибыл в док
//порожний танкер в пути
//танкер прибыл на погрузку
//танкер загружается
//загрузка танкера завершена
}
//Конструктор класса Хранилище. В исходном состоянии хранилище пусто
Di 1House::OilHouse()
{
oil Level-0:
check=0;
dok-NULL;
}
void Oi1 House::putDok(Dok *d) { dok-d; }
//Диспетчер
Листинг 19.1. Файл classesl9.h с описаниями классов 379
void OilHouse::run()
{
int a;
//Уровень нефти достаточен для возобновления снабжения очистительной установки
if (check==0) if (oilLevel>-almost_empty*more_ratio) check-1;
//Хранилище переполнилось
if (check—1) if (oilLevel>-capacity)
{
timeout2++: //инкремент счетчика прерываний
check=2: //переход в новое состояние
((Dok*)dok)->stopWork(): //сигнализируем доку о прекращении разгрузки
}
//Один раз в сутки собираем статистику об уровне нефти в хранилище
if ((total+l)fc24==0)
{
a-(total+l)/24:
vol_ave=vol_ave*(1-1.0/a)+oi1 Level/a;
fpri ntf (oi 1 Vol ume. "Г 3f\n". oil Level):
}
if (check==0) return; //снабжения очистительной установки нет
oi1 Level-=0U_speed: //закачиваем нефть в очистительную установку
rOU++: //инкремент счетчика тактов снабжения очистительной
//установки
//Нефти стало слишком мало
if (check==l) if (oilLevel<almost_empty)
{
timeoutl++;
check=0;
return;
}
else return:
else //произошло переполнение...
if (oilLevel<=capacity*less_ratio) //...но на данном такте нефть
//опустилась до нужного уровня
{
check=l;
((Dok*)dok)->resumeWork(): //просим док возобновить разгрузку
return;
}
}
//Конструктор. Очередь пуста, разгрузки нет. Время - полночь.
Dok::Dok()
{
serving=NULL;
House=NULL:
//Выделяем память для размещения очереди
queue=new Tanker *[Т];
forCint i=0;i<T:i++) queue[i]-NULL:
to_work--1;
to_break-6;
working-1:
}
//деструктор. Возвращает память
//Так как количество танкеров в системе - постоянная величина память для них выделяется
380 Глава 19. Замкнутая система с регулирующим объектом
//и освобождается в функции mainO. К моменту вызова деструктора класса Dok память,
//выделенная для обьектов-танкеров, уже будет освобождена функцией mainO.
//поэтому удалять объекты, находящиеся в очереди, здесь не надо
Dok::-DokО
{
delete [] queue:
}
void Dok::putHouse(Di 1 House *p) { House=p; }
//Вычисление текущей длины очереди
int Dok::OLength()
{
for(int i-0;i<T:1++)
if (queue[i]==NULL) return(i):
return(T);
}
//Прибытие танкера
void Dok::Arrival(Tanker *ptr)
{
entered++: //инкремент счетчика прибытий
if (serving==NULL) //на разгрузке танкера нет
{
serving-ptr: //ставим прибывший танкер на разгрузку
//его время ожидания равно нулю
wait_ave-wait_ave*(1-1.О/(completed+1)):
fprintf(waitTime."0\n");
ptr->cur_serve“O; //инициализируем счетчик длительности разгрузки
}
else //место на разгрузке занято другим танкером
{
ptr->cur_wait=O: //инициализируем счетчик длительности ожидания
queue[QLength()]-ptr: //ставим танкер в конец очереди
}
}
//Разгрузка танкера завершена
void Dok::Completed
{
completed++: //инкремент счетчика разгруженных танкеров
serving->DokComplete(): //отправляем разгруженный танкер в путь
serving=NULL: //место на разгрузке свободно
if (queue[O]!=NULL) //очередь не пуста
{
serving=queue[0]; //ставим первый в очереди танкер на разгрузку
//Сбор статистики о длительности ожидания
wait_ave-wait_ave*(1-1.0/(completed+1))+(fl oat)(queue[0]-> cur_wait)/(completed+1):
fprintf(waitTime.”!L3f\n". (float)(queue[0]->Cur_wait)/24):
//Продвижение очереди
for(int i=0:i<(T-l);i++) queue[i]=queue[i+l];
queue[T-l]=NULL:
}
return:
Листинг 19.1. Файл dassesl9.h с описаниями классов 381
//Наступила полночь. Прекращаем работать.
void Dok::Night()
{
to_break=6:
to_work=-l:
)
//Наступило утро. Начинаем работать
void Dok::Morning()
{
to_break=-l:
to_work=18:
)
//Приостановить разгрузку, так как хранилище переполнено
void Dok::stopWorkО
{
working-0;
)
//Возобновить разгрузку, хранилище частично освободилось
void Dok::resumeWork()
{
working-1;
)
//Диспетчер
void Dok:-.runО
{
int a:
//Проверка наступления утра или ночи
if (to_break>0) to_break--:
if (to_break—0) MorningO:
if (to_work>0) to_work--;
if (to_work—0) NightO:
if (servIng!-NULL) //на разгрузке стоит танкер
{
serving->cur_serve++: //инкремент счетчика длительности разгрузки
if ( (working!-О)&&(to_work>0) ) //разгрузка происходит
{
serving->cur_storage-=Tanker_speed: //закачка нефти в хранилище.
//В танкере нефти стало на столько же меньше...
(((OllHouse*)House)->oilLevel)+=Tanker_speed: //...на сколько в хранилище больше
if (serving->cur_storage<serving->rnin_storage) CompleteO: //проверка
//условия завершения разгрузки
}
}
//Инкремент счетчика длительности ожидания для всех танкеров в очереди
for(int i=0:i<T:i++)
if (queuefi]!-NULL) (queue[i]->cur_wait)++;
//Раз в сутки - сбор статистики о длине очереди
if ((total+l)«24-0)
{
382 Глава 19. Замкнутая система с регулирующим объектом
a=(total+l)/24;
queN_ave=queN_ave*(1 -1.0/а)+((f 1oat)QLength())/а:
fprintf (nQueue. "Шп". QLengthO);
}
}
Листинг 19.2. Функция main()
#define N 35040 //продолжительность моделирования - 4 года
#include "classesl9.h"
Int mainO
{
int i:
Tanker **t:
//Открываем файлы для сбора статистики
oi1 Vol ume=fopen("oi1 Volume". "wt"):
fu11 Ti me=fopen("fu 11Ti me". "wt"):
waitTime=fopen("waitTime". "wt”):
nQueue=fopen("nQueue". "wt");
srand((unsigned)time(O)):
//Создаем объекты моделируемой системы
t=new Tanker *[T]:
for(i=0:i<T:i++)
t[i]=new Tanker!i+1);
Dok d:
Oil House o;
//настраиваем связи между обьектами
for(i=0;i<T;i++)
t[i]->putDok(&d):
o.putDok(&d);
d.putHouse(&o):
//основной цикл моделирования
for(tota1=0L;tota1<N:tota1++)
{
for(i=0;i<T:i++)
t[i]->run():
d.runO:
o.runO;
}
//удаление обьектов
for(i=0:i<T;i++)
delete t[i];
delete [] t;
//Закрытие файлов сбора статистики
fclose(oilVolume):
fclose(fullTime); fclose(waitTime):
fclose(nQueue):
//Выдача результатов моделирования
printf!"Всего поступлений в док £d\n". entered):
printf("Разгружено £d\n", completed):
printf!"Прерывания снабжения очистительной установки £d\n", timeoutl);
printfCnpepbiBaHHfl разгрузки fcd\n". timeout2):
printf("Доля времени, в течение которого снабжалась нефтью очистительная установка
ГЗАп". ((float)rOU)/total):
printf!"Средний уровень нефти в хранилище !L3f\n". vol_ave):
19.5. Анализ результатов 383
printf("Средняя длительность полного рейса танкера t.3f\n". fpath_ave/24):
printf("Средняя длительность ожидания в очереди £.3f\n". wait_ave/24):
printf("Средняя длина очереди Jt.fXn". queN_ave):
printf("Средняя длительность разгрузки £.3f\n“. serve_ave/24):
}
19.5. Анализ результатов
Результаты имитационных экспериментов за пятилетний срок при различных
значениях количества танкеров представлены в табл. 19.1. Показатели пронуме-
рованы следующим образом:
1. Количество поступлений танкеров в док.
2. Количество разгруженных танкеров — показатель производительности системы.
3. Количество «зашкаливаний» нефти в хранилище за нижний уровень.
4. Количество «зашкаливаний» нефти в хранилище за верхний уровень.
5. Доля времени, в течение которого очистительная установка снабжалась нефтью.
6. Средний уровень нефти в хранилище.
7. Среднее время полного рейса танкера.
8. Среднее время ожидания танкера в очереди.
9. Средняя длина очереди.
10. Среднее время разгрузки.
И. Максимальный за 5 лет уровень нефти в хранилище.
Таблица 19.1. Результаты имитационных экспериментов
Номер показателя Число танкеров
1 2 3 4 5
1 79 151 164 164 164
2 79 150 161 161 161
3 79 38 0 0 0
4 0 0 2 2 2
5 0,5 0,94 0,99 0,99 0,99
6 29,16 49.26 602,82 603,45 603,5
7 23,06 24,25 33,85 45,02 56,1
8 0 0,99 10,42 23,68 32,97
9 0 0,08 0,93 1,93 2,94
10 11,25 11,25 11,25 11,25 11,25
И 66,1 87,15 999 999 999
384 Глава 19. Замкнутая система с регулирующим объектом
Из данных, приведенных в таблице, со всей очевидностью вытекает, что опти-
мальное количество танкеров равно трем. В самом деле, изменение количества
танкеров с двух до трех переводит систему в режим работы в условиях большой
нагрузки. Хранилище впервые начинает наполняться до отказа, средний уровень
нефти в нем резко возрастает, очистительная установка снабжается нефтью
практически беспрерывно. Скачкообразно возрастают время ожидания и длина
очереди, «зашкаливания» вниз исчезают, «зашкаливания^ вверх появляются.
Дальнейшее увеличение числа танкеров не приводит к увеличению производи-
тельности, так как общее число разгруженных танкеров остается неизменным,
зато другие показатели, такие как время полного рейса, длительность ожидания
и длина очереди, продолжают быстро ухудшаться.
Таким образом, при наличии трех танкеров система работает почти со стопро-
центным КПД, ее максимальная производительность составляет 161 разгружен-
ный танкер.
Довольно интересен график изменения уровня нефти в хранилище (рис. 19.1).
Рис. 19.1. График изменения уровня нефти в хранилище в течение пяти лет
На представленном графике уровень нефти фиксировался один раз в 9 дней. Из
графика видно, что хранилище заполняется полностью только на четвертом году
работы, а до этого уровень нефти монотонно возрастает. Создается впечатление,
что почти 3 года и 9 месяцев длится переходный период и только затем для слу-
Выводы 385
чайного процесса, характеризующего уровень нефти в хранилище, устанавлива-
ется стационарный режим, характеризующийся устойчивыми колебаниями
уровня нефти. Чтобы проверить это, увеличим длительность моделирования до
десяти лет (рис. 19.2). Предположение подтверждается — стационарный режим
для рассматриваемого случайного процесса на графике прослеживается очень
четко, поэтому истинное среднее значение уровня нефти в хранилище не 602,
а 900. Но если предположить, что каждые пять лет весь нефтяной терминал ста-
вится на осмотр и проведение регламентных работ, а затем все начинается «с нуля»,
стационарное среднее значение в этом случае может достигаться только теорети-
чески.
Количество дней (1:18)
Рис. 19.2. График изменения уровня нефти в хранилище в течение десяти лет
Выводы
1. В системе имеется объект, не являющийся ни клиентом, ни узлом обслужива-
ния — хранилище нефти. Оно выполняет регулирующую функцию для про-
цесса обслуживания в доке.
2. У обслуживающего устройства (дока) существует также и свое собственное
расписание (закрытие на ночь).
386 Глава 19. Замкнутая система с регулирующим объектом
3. Между разными парами классов, согласно логике моделируемой системы, су-
ществуют как двунаправленные, так и однонаправленные связи.
4. Из результатов имитационного моделирования следует, что оптимальное ко-
личество танкеров — три.
5. Стационарный режим для случайного процесса изменения уровня нефти
в хранилище (устойчивые периодические колебания) устанавливается только
к концу четвертого года работы.
Задания для самостоятельной работы
Зафиксируйте количество танкеров, равное двум. Проверьте, к чему приводит
увеличение емкости танкеров при сохранении их количества. Проведите два экс-
перимента. В первом случае изменяйте только емкость танкеров, во втором —
увеличивайте по некоторому закону (например, прямой пропорциональности)
время погрузки и пути в загруженном состоянии. Получаем ли мы в каком-либо
случае выигрыш в производительности? Если да, существует ли предельное зна-
чение емкости, превышение которого уже не приводит к положительному эф-
фекту?
Приложение 1
Реализация класса Palata
с помощью контейнера List
из библиотеки STL
(к главе 6)
Приведем альтернативный вариант реализации класса Pal ata с помощью контей-
нера класса List из библиотеки Standard Template Library. Для навигации по списку
нам понадобится также объект класса Iterator. Заметим, что аргументом метода
departure!) теперь становится итератор, а не указатель на элемент списка, кото-
рый следует удалить. В этом нет ничего удивительного, так как итератор в STL
можно рассматривать как инкапсулированный указатель.
Вызовы библиотечных STL-функций выделены и прокомментированы.
Листинг ПЛ. Реализация класса Palata с помощью контейнера List
#i nclude<list>
#1 nclude<1terator>
using namespace std:
class Palata
{
int current_number;
//Объявляем новое поле данных - список пациентов палаты Конструктор по //умолчанию
создает пустой список.
list<Pacient> ill:
const static int volume=25;
const static float borderl-41:
const static float border2-47:
const static float healthy-49;
public:
Palata!):
void run();
void arrival!):
388 Приложение 1. Реализация класса Palata с помощью контейнера List
list<Pacient>::iterator departure(list<Pacient>:iterator p): //метод,
//моделирующий выписку больного из палаты, значение р указывает на удаляемый
//из списка элемент
}:
Palata::Palata() //метод-конструктор
{
current jiumber=0:
}
void Palata: :rund
{
int i. j;
float ro_val:
list<Pacient>::iterator pos: //объявляем итератор для навигациипо списку
//Обход всех больных и пересчет их текущих оценок путем вызова глобальной
//фукнции for_each. вызывающей для каждого элемента списка метод changejnark
//mem_fun_ref - специальный адаптер для вызова метода класса
for eachl ill. beginl). i 11. endl). mem fun ref(&Paci ent:: changejnark));
//обход всех больных и выписка выздоровевших
pos=ill.beginl): //установка итератора на начало списка.
while(pos!=ill.endl)) //пока итератор не вышел за пределы списка
{
//Удаляем элемент и получаем итератор, указывающий на позицию вслед
//за удаляемой. Удаление оформлено в отдельный метод, так как кроме вызова
//библиотечного метода erase требуется еще статистический учет
if (pos->current_mark>healthy)
pos=departure(pos);
//Если удаление не требуется, продвигаем итератор дальше по списку
else ++pos:
}
//Прибытие двух новых больных
for(i=0; i <2; i++)
{
arrival О:
}
pos=ill.beginl): //установка итератора на начало списка
//Обход всех больных и инкремент числа дней, проведенных в палате
. while(pos!=ill.endl))
{
pos->days_i n_hosp++:
++pos;
}
//Вычисление текущей загрузки и запись в файл
ro_val=((float)(ill.sizel)))/vplume;
fpr1ntf(ro."fcf\n'’. ro_val);
//Пересчет средней загрузки. Число дней вдвое меньше total
ro_aver=ro_aver*(1-2.О/total)+2.0*ro_val/total:
}
void Palata::arrival()
{
int j:
list<Pacient>: iterator pos: //объявление итератора для навигации по списку
Pacient *p=new Pacient(O): //создание нового объекта класса Пациент
total++:
if (ill.sized «volume) //в палате есть свободные места. Метод
//sized возвращает текущий размер списка
Приложение 1. Реализация класса Palata с помощью контейнера List 389
{
ill,push_back(*p): //добавляем новый элемент в конец списка
entered++:
return:
}
if (p->current_mark>borderl) //свободных мест в палате нет. начальная
//оценка превышает 41 балл
{
delete р: //удаление пациента
rejectl++:
return;
}
//Свободных мест нет. начальная оценка не превышает 41 балл
pos=ill.beginO: //установка итератора на начало списка
while(pos!=ill.endO) //ищем пациента, которого можно досрочно
//выписать
{
if (pos->current_mark >= border2) //пациент найден
{
pos=departure(pos); //выписка
ill.push_back(*p): //добавляем нового пациента
entered++:
return:
}
}
delete р: //принять пациента в палату не удалось.
//удаляем объект
reject2++:
return;
)
list<Pacient>:iterator Palata::departure(list<Pacient>:iterator pos)
{
Int j. sojourn_val:
//Выписываемый больной выздоровел
If (pos->current_mark > healthy) complete++:
//Досрочная выписка
else if (pos->currentjnark >= border2) earlier++:
soj ourn_va1=pos->day s_in_hosp:
//Записываем в файл число дней, которое выписанный больной провел
//в палате
fprlntfCsojourn. "fcdXn". sojourn_val):
//Пересчитываем среднее время пребывания в палате
111_aver=i11_aver*(1-1.0/(comp1ete+earlier))+1.0*sojourn_val/(compl ete+ea rli er):
currentjiumber--:
//Удаляем элемент из списка. Библиотечный метод erase удаляет из списка
//элемент, на который указывает итератор pos и возвращает итератор.
//указывающий на позицию вслед за удаленным элементом.
pos=ill.erase!pos):
return pos: //...который мы и возвращаем в качестве
//результата
}
Приложение 2
Вывод условия
существования
стационарного режима
(к главе 8)
Для системы, изображенной на рис. 8.1, назвав пункт контроля Узел1, а пункт
настройки Узел2, введем следующие обозначения: X — средняя интенсивность
внешнего входного потока; at — среднее время обслуживания в узле 1; а2 — сред-
нее время обслуживания в узле 2; — количество каналов в узле 1; N2 — коли-
чество каналов в узле 2; р — маршрутная вероятность перехода из узла 1 в узел 2;
Т — среднее время обслуживания заявки в системе (без учета времени ожидания
в очередях); ц — средняя интенсивность обслуживания заявки в системе.
Для нашей системы X = 0,18, at = 9,а2 = 30, Nt = 2,N2=l,p = 0,15. Условием суще-
ствования стационарного режима, то есть устойчивого функционирования на
сколь угодно большом интервале времени, является выполнение неравенства
Х/р < 1. Основная трудность при проверке условия заключается в вычислении р,
так как заявка на протяжении своего пребывания в системе обслуживается в раз-
ных узлах.
Введем еще одно определение. Раундом обслуживания назовем последователь-
ное обслуживание заявки в узле 2 и узле 1 (но не наоборот!). Тогда можно ска-
зать, что обслуживание заявки в системе складывается из первоначального об-
служивания в узле 1 и некоторого (возможно, нулевого) количества раундов.
Теорема. Стационарный режим в описанной системе существует при выполне-
X(ct + ра2) ,
НИИ условия-5--— < 1.
Nx + pN2
Доказательство. Необходимо доказать, что р = (А\ + pN^/{a{ + ра^. Найдем
среднее количество раундов, которое совершает одна заявка. Вероятность прохо-
Приложение 2. Вывод условия существования стационарного режима 391
ждения i раундов равна (1 - р)р?. Следовательно, среднее число раундов равно
(1 -р)У гр' - (методы вычисления таких сумм приведены во многих учеб-
i=0 1-р
никах, например в [10]). Тогда
Т = а । р(а‘ + а^-а> + ра*
1 1-р 1-р
Средняя интенсивность обслуживания в узле 1 равна р, = М/а,. Средняя интен-
сивность обслуживания во время раунда равна
_ a, N, а2 N2_Nl+N2
Ра — "• —
at + а2 а, а, + а2 а2 а, + а2
Тогда средняя интенсивность обслуживания заявки в целом равна
Используя найденные выражения для нахождения Т, р( и р2, получим то, что
и требовалось доказать.
Таким образом, для рассматриваемой системы Х/Н = 0,18/0,16 > 1, и стационар-
ного режима в ней не существует. Поэтому при разной длительности модели-
рования среднестатистические показатели будут иметь значимые различия. До-
стичь стационарного режима можно следующими способами:
О увеличить число контролеров до трех;
О увеличить число настройщиков до трех;
О снизить вероятность неудачного завершения проверки до 0,086.
В каждом из этих трех случаев условие существования стационарного режима
будет выполняться. Например, для^первого случая получим = 3,
X.(at + pa2) _ 0,18 (9+0,15-30) _ 2,43
Nt + pN2 ~ 3+0,15 "3,15 <
Приложение 3
Реализация класса Emptier
с помощью контейнера
Priority Queue из библиотеки
STL (к главе 10)
Приведем альтернативный вариант реализации класса Emptier с помощью кон-
тейнера класса Priority Queue из библиотеки Standard Template Library. Этот
контейнер мы используем для представления поля данных queue, моделирующе-
го очередь самосвалов к измельчителю. Напомним, что приоритет в этой очереди
имеют 50-тонные самосвалы. Обратите внимание на следующие факторы:
1. Для класса HeavyCar, объекты которого составляют содержимое очереди, необ-
ходимо'перегрузить оператор сравнения <, чтобы объекты в очереди можно
было упорядочить по значению приоритета. Вот метод, который необходимо
добавить в описание класса HeavyCar:
friend bool operator <(const HeavyCar& hl. const HeavyCar& h2)
{
if (hl.pr<h2.pr) return(true):
return(false):
1
2. Метод Emptier::choiceO теперь не требуется, так как все необходимые дейст-
вия по выборке элемента и сдвигу очереди произведет метод priority_
queue::рор().
3. Метод priority queue: :top() возвращает первый элемент в очереди, но не из-
влекает его; метод priority queue:: рор() извлекает из очереди первый элемент,
но ничего не возвращает. Таким образом, если первый элемент очереди дол-
жен передаваться куда-либо для дальнейшей обработки, необходим последо-
вательный вызов обоих методов.
4. Согласно документации библиотеки STL, методы рор() и top() не обеспечива-
ют выборку по принципу «первым пришел — первым обслужен» среди эле-
Приложение 3. Реализация класса Emptier с помощью контейнера Priority Queue 393
ментов с одинаковым приоритетом, то есть может быть выбран элемент не
с самым большим временем нахождения в очереди. Таким образом, функцио-
нальность класса Emptier не сохраняется. Чтобы сохранить ее, необходимо в
реализации оператора «<» учитывать не только тоннаж, но и время пребыва-
ния заявки (самосвала) в очереди.
5. Так как слово queue является одним из ключевых для библиотеки STL, поле
данных Emptier::queue во избежание коллизий при компиляции назовем que.
Вызовы библиотечных STL-функций выделены и прокомментированы.
Листинг П.2. Реализация класса Emptier с помощью контейнера Priority Queue
#i nclude<queue>
using namespace std:
class Emptier
{
//Вместо queue - que. метода choiceO нет. все остальное - без изменений
priority_queue<HeavyCar> que: //создание пустой очереди с приоритетами
}:
Emptier::Emptier(Fuller *f)
{
//Динамическую память под que выделять не надо, остальное - без изменений
)
void Emptier::Print()
{
int i:
priority_queue<HeavyCar> buffer; //объявляем локапьную переменную
buffer=que: //копируем в нее очередь
printfC'B очереди к измельчителю - td самосвалов\п", que.sized):
i-1:
//Извлекаем элементы под одному и печатаем информацию о них. Оригинал при //этом не
меняется
whi1е(!buffer.empty())
{
printfC'ld-й в очереди • самосвал № Xd\n”. i. buffer.topO.id);
buffer.рор();
++i:
}
//Оставшаяся часть метода - без изменений
)
void Emptier::Arrival(HeavyCar *h)
{
int k. p: float mu;
if (h->pr==l) mu-perf20;
else mu=perf50:
k=FirstAvail():
//Ставим прибывший самосвал в очередь
if (k—1)
394 Приложение 3. Реализация класса Emptier с помощью контейнера Priority Queue
{
h->state-4;
que.push(*h): //вставка в очередь нового элемента
}
//Оставшаяся часть метода - без изменений
)
void Emptier:: Comp] eted nt i)
{
int j:x
float mu:
serving[i]->state=6:
servi ng[i]->to_pempty-serving[i]->back:
if (que.emptyO) //очередь пуста
{
to_empty[i]=-l:
serving[i]=NULL:
)
//очередь не пуста
else
{
//Выбираем из очереди самосвал и ставим на разгрузку
HeavyCar who»que.top(): //получаем первый элемент очереди
que.popO: //удаляем его из очереди
if (who.pr=l) mu»perf20:
else mu=perf50:
to_empty[i]=(int)(get_exp(mu)*60):
serving[i]=&who; //ставим выбранный из очереди элемент
//на обслуживание
serving[i]->state=5:
)
)
void Emptier::гулО
{
//До сбора статистики - без изменений
fprintf(qu2."Xd\n". que.sizeO):
que2_ave-que2_ave*(l-1.0/(total+l))+(( float )que. sized)/(total+1);
ro_emptier-ro_emptier*(1-1.0/(total+!))+(((float)Busy())/volume)/(total+1):
Приложение 4
Реализация time-
driven-подхода для
календарно-
событийной программы
(к главе 5)
#define пО 6
#define nl 3
#define nt nO+nl
#define nd 4
Int n[2]:
int s[2]:
double tc[2]={N.O. 5.0 }. td-30.0. sd-2.5:
struct token_info
{
int cis:
double ts:
}:
int nts-500:
//Класс Facility описывает многоканальный прибор с обшей очередью
//и дисциплиной обслуживания в порядке поступления
class Facility { int id: //номер прибора
int volume: //количество каналов
int *to_complete: //время, оставшееся до завершения //обслуживания на каждом канале (-1. если //канал свободен)
struct token_info **serving; //массив указателей на обслуживаемые заявки
struct token_info **queue: Facility *other: //массив указателей на заявки, находящиеся в очереди //прибор, на обслуживание к которому далее //переходят заявки
396 Приложение 4. Реализация time-driven-подхода
public:
//Конструктор
Facilltydnt a.int b.struct tokenjnfo **c. struct tokenjnfo **d. int *e):
void Completednt 1): //1-й канал завершил обслуживание
void Arrival (struct tokenjnfo *h); //прибытие новой заявки
void run(): //диспетчер
void putOther(Facility *a): //установление связи co следующим прибором
}:
Facility::Facilltydnt a.int b.struct tokenjnfo **c. struct tokenjnfo **d. int *e)
{
int 1;
id=a:
volume=b:
to_complete= d nt*)ma11oc(volume*s1zeof d nt)):
servi ng=(struct tokenJ nfo**)ma11oc(volume*s1zeof(struct tokenJ nfo*)):
queue-(struct tokenjnfo**)malloc(nt*sizeof(struct tokenjnfo*)):
for (1=0;1<volume:i++)
{
to_complete[i]=e[i]:
serving[i]=c[1J:
)
for (1=0:1<nt:1++)
queued ]=d[i];
)
void Facility::putOther(Facility *a)
{
other=a:
)
void Facility::Arrival (struct tokenjnfo *h)
{
int i. choice:
choice=-l:
//Определяем первый свободный канал
for d=0;i<volume;i++)
{
if (to_complete[i]==-l)
{
choice=i:
break:
)
)
//Свободный канал найден
if (choice>=0)
{
if dd==0) //...если прибор - ЦПУ
to_complete[choice]=expnt(tc[h->cls]):
else to_complete[choice]=erlang(td. sd): //...если прибор - диски
serving[choice]=h: //...ставим прибывшую заявку
//на обслуживание
)
//Прибор - ЦПУ. он занят, но прибывшая заявка имеет более высокий
//приоритет
else if (dd==O)&&(h->cls==l)&&(serving[O]->cls==O))
{
Приложение 4. Реализация time-driven-подхода 397
//Обслуживаемая заявка возвращается в голову очереди
for(1=nt-1:1>0;1 --) queue[1]=queue[i-1]:
queue[O]=serving[O]:
//Прибывшая заявка ставится на обслуживание на ЦПУ
to_complete[0]=expnt(tc[l]);
serving[0]=h;
}
//Прибывшая заявка ставится в конец очереди из-за отсутствия каналов
else
{
for (1=0:i<nt;i++)
{
if (queue[i]=-NULL)
{
queue[i]=h:
break:
)
)
)
)
void Facility::Complete(int i)
{
int j:
if (id=-l) //заявка завершила обслуживание на одном из дисков
{
//Фиксация статистики по завершившемуся циклу
s[serving[i]->cls]+=serving[i]->ts:
n[serving[i]->cls]++:
serving[i]->ts=O;
)
//Генерируем событие - поступление этой заявки к следующему устройству
other->Arri val(servi ng[i]);
//Если очередь не пуста, ставим на обслуживание новую заявку и сдвигаем
//очередь
if (queue[O]!=NULL)
{
if (id-=0)
to_complete[i]=expnt(tc[queue[OJ->cls]):
else to_complete[i]=erlang(td. sd):
serving[i]=queue[OJ;
for(j=0:j<nt-l;j++)
queue[j]=queue[j+l]:
queue[nt-l]=NULL:
)
else //очередь пуста, канал объявляется свободным
{
to_complete[i]“-l: }
serving[i]-NULL:
)
void Facility::run()
{
int i:
398 Приложение 4. Реализация time-driven-подхода
//Для всех заявок на устройстве (обслуживаемых и стоящих в очереди)
//выполняем инкремент длительности текущего цикла
for(1=0:1<volume;i ++)
if (to_complete[i]>0) serving[i]->ts++;
for(i=0;i<nt:i++)
{
if (queue[1J—NULL) break;
queue[i]->ts++:
)
//Выполняем декремент счетчика времени обслуживания, в случае завершения -
//генерируем событие
for(i=0;i<volume;i++)
{
if (to_complete[i]>0) to_complete[i]--:
if (to_complete[i]=-0) Completed):
)
)
int mainO
{
struct token_info task[nt]. **servl. **serv2. **quel. **que2:
int *masl. *mas2. i;
n[0]-n[l]-0:
s[0]-s[l]-0.0:
//Инициализация заявок
for(i-0;i<nt:i++)
{
task[i].clS“(1>-n0) ? 1:0;
task[i].ts-0:
)
//Инициализация объектов. В начальном состоянии первая заявка начинает
//обслуживаться на ЦПУ. остальные стоят в очереди к ЦПУ. Очередь к дискам
//пуста
masl-(int*)malloc(sizeof(int));
masl[0]-expnt(tc[task[0].cis]):
mas2-(i nt*)mal1ос(nd*s i zeof(int)):
for(i-0:i<nd:i++)
mas2[i]“-l:
servl-(struct token_info**)malloc(sizeof(struct tokenjnfo*)):
servl[0]-task:
serv2-(struct token_info**)malloc(nd*sizeof(struct tokenjnfo*));
for(i-0:i<nd;i++)
serv2[i]-NULL:
quel-(struct token_info**)malloc(nt*sizeof(struct tokenjnfo*)):
for(i-0;i<nt-l;i++)
quel[i]-task+i+l;
quel[nt-l]-NULL:
que2-(struct tokenjnfo**)malloc(nt*sizeof(struct tokenjnfo*)):
for(i-0:i<nt;i++)
que2[i]-NULL:
Facility cpu(O.l.servl.quel.masl):
Facility disks(1.4.serv2.que2.mas2):
Приложение 4. Реализация time-driven-подхода 399
//Взаимная настройка связи между ЦПУ и дисками
cpu.putOther(&d1sks):
disks.putOther(&cpu);
//Основной цикл
while(n[O]+n[l]<=nts)
{
cpu.runO:
disks.runO;
)
printfCclass 0 tour time=£.2f\n”. ((float)s[0])/n[0J):
printfCclass 1 tour time=X.2f\n". ((float)s[l])/n[l]):
Список литературы
и источников
из Интернета
1. Адлер Ю. П. Статистические методы в имитационном моделировании. — М.:
Мир, 1990.
2. Аммерааль Л. STL для программистов на C++. — М.: ДМК, 2000.
3. Аноприенко А. Я., Святный В. А. Универсальные моделирующие среды // Сб.
тр. Донецк, гос. тсхнич. ун-та. Сер.: Информатика, кибернетика и вычисли-
тельная техника. — 1996. — Вып. 1. — С. 8-23.
4. Башарин Г. П., Бочаров П. П., Коган Я. А. Анализ очередей в вычислительных
сетях. — М.: Наука, 1989.
5. Бенъкович Е., Колесов Ю., Сениченков Ю. Практическое моделирование дина-
мических систем. — СПб.: BHV, 2002.
6. Боев В. Моделирование систем. Инструментальные средства GPSS World. —
СПб.: BHV, 2004.
7. Вагнер Г. Основы исследования операций: В 3 т. Т. 1. — М.: Мир, 1973.
8. Вагнер Г. Основы исследования операций: В 3 т. Т. 3. — М.: Мир, 1973.
9. Васильев Ф. П. Численные методы решения экстремальных задач. — М.: Нау-
ка, 1980.
10. Воробьев Н. Н. Теория рядов. - М.: Наука, 1986.
И. Голованов О. В., Дуванов В. Г., Смирнов В. Н. Моделирование сложных дис-
кретных систем на ЭВМ третьего поколения (опыт применения GPSS). — М.:
Энергия, 1978.
12. Грэхем Р., Кнут Д., Поташник О. Конкретная математика: основание инфор-
матики. — М.: Мир, 1998.
Список литературы и источников из Интернета 401
13. Джосъютпис Н. C++. Стандартная библиотека. Для профессионалов. — СПб.:
Питер, 2004.
14. Дьяконов В. Maple 7. — СПб.: Питер, 2002. (Сер. Учебный курс)
15. Дьяконов В. Maple 8 в математике, физике, образовании. — М.: Солон, 2003.
16. Закс Ш. Теория статистических выводов. — М.: Мир, 1975.
17. Келыпон В., Лоу А. Имитационное моделирование. — СПб.: Питер, 2004. (Сер.
Классика CS.)
18. Кендалл М. Дж., Стьюарт А. Статистические выводы и связи. — М.: Наука,
1973.
19. Клейнрок Л. Вычислительные системы с очередями. — М.: Мир, 1979.
20. Клейнрок Л. Теория массового обслуживания. — Л.: Машиностроение, 1979.
21. Колесов Ю. Б., Сениченков Ю. Б. Визуальное моделирование — СПб.: Мир; Се-
мья Интерлайн, 2000.
22. Колесов Ю. Б., Сениченков Ю. Б. Имитационное моделирование сложных дина-
мических систем // Exponenta Pro. Математика в приложениях. — 2004. —
№ 1 // www.nnspu.ru/Exponenta_Ru/soft/others/mvs/ds_sim.asp.htm.
23. Коллинз У. Структуры данных и стандартная библиотека шаблонов. — М.: Би-
ном, 2004.
24. Конвей Р. В., Максвелл В. Л., Миллер Л. В. Теория расписаний. — М.: Наука,
1975.
25. Кудрявцев Е. М. GPSS World. Основы имитационного моделирования различ-
ных систем. — М.: ДМК, 2003.
26. Маккин Г. Стохастические интегралы. — М.: Мир, 1972.
27. Мейерс С. Эффективное использование STL. Библиотека программиста. —
СПб.: Питер, 2003.
28. Мелник М. Основы прикладной статистики. — М.: Энергоатомиздат, 1983.
29. Москвин П. В. Азбука STL. — М.: Горячая линия; Телеком, 2003.
30. Остерн М. Обобщенное программирование и STL. — СПб.: Невский диалект,
2004.
31. Поллард Дж. Справочник по вычислительным методам статистики. — М.: Фи-
нансы и статистика, 1982.
32. Приемы объектно-ориентированного проектирования: паттерны проектирова-
ния / Э. Гамма, Р. Хелм, Р. Джонсон, Д. Влиссидес. — СПб.: Питер, 2001.
33. Прицкер А. Введение в имитационное моделирование на языке SLAM II. —
М.: Мир, 1984.
34. Рыжиков Ю. И. Имитационное моделирование: теория и технологии. — М.:
Альтекс-А, 2004.
35. Саати Т. Л. Элементы теории массового обслуживания и ее приложения. —
М.: Сов. радио, 1971.
36. Семишин Ю. А., Гуржий В. П., Литвинова О. К. Моделирование дискретных
систем на ДАСИМ. — М.: Моя Москва, 1995.
402 Список литературы и источников из Интернета
37. СУБД Cache: объектно-ориентированная разработка приложений / В. Кир-
стен, М. Принтер, Б. Рериг, П. Шульте. — СПб.: Питер, 2001.
38. Таранов В. В. Концепция универсальной системы имитационного моделиро-
вания //http://www.az.rU/natlieb/articles/l/USIM.htm.
39. Томашевский В. Н. Имитационное моделирование в среде GPSS. — М.: Бест-
селлер, 2003.
40. Томашевский В. Н., Жданова Е. Г. Имитационное моделирование средствами
GPSS/PC. - Киев: ВИПОЛ, 1998.
41. Халперн П. Стандартная библиотека C++ на примерах. — СПб.: Вильямс, 2001.
42. Шамис В. A. Borland C++ Builder 6. — СПб.: Питер, 2004. (Сер. Для профес-
сионалов.)
43. Шеннон Р. Имитационное моделирование систем — искусство и наука. — М.:
Наука, 1978.
44. Шерр А. Анализ вычислительных систем с разделением времени. — М.: Мир,
1970.
45. Шлеер С., Меллор С. Объектно-ориентированный анализ: моделирование мира в
состояниях. — Киев: Диалектика, 1993.
46. Шрайбер Т. Д. Моделирование на GPSS. — М.: Машиностроение, 1980.
47. Элджер Д. Язык C++. — СПб.: Питер, 2001. (Сер. Библиотека программиста.)
48. Эллиотт Р. Стохастический анализ и его приложения. — М.: Мир, 1986.
49. Эльсгольц Л. Э. Обыкновенные дифференциальные уравнения. — СПб.: Лань,
2002.
50. Яшков С. Ф. Анализ очередей в ЭВМ. — М.: Радио и связь, 1989.
51. Abrams М. Parallel discrete event simulation: fact or fiction? // ORSA Journal on
Computing. — 1993. — № 5(3). — P. 231-233.
52. Adan I., Resing J. Queueing Theory / Department of Mathematics and CS. Eind-
hoven University of Technologies. — Eindhoven, 2001.
53. Allen A. O. Probability, statistics and queueing theory. With computer science
applications. — Boston, MA: Academic Press, 1990.
54. Begain K., Bolch G., Herold H. Practical Performance Modeling. Application of
the MOSEL language. — Kluwer Academic Publishing, 2000.
55. Bobillier P. A., Kahan В. C., Probst A. R. Simulation with GPSS and GPSS V. —
Englewood Cliffs, NJ: Prentice-Hall, 1976.
56. Carmichael D. Engineering Queues in Construction and Mining. — Chichester:
Ellis Horwood, 1987.
57. Cooper R. B. Introduction to queueing theory. — London: North-Holland (Else-
vier), 1981.
58. Copstein B., Pereira С. E., Wagner F. R. The Object-Oriented Approach and the
Event Discrete Simulation Paradigms // Proceedings, Society for Computer
Simulation. — 10th European Simulation Multiconference. — Budapest, Hungary,
1996. - June. - P. 57-61.
Список литературы и источников из Интернета 403
59. Daigle J. N. Queueing theory for telecommunications. — Reading, MA: Addison-
Wesley, 1992.
60. Discrete-Event Simulation. 3rd ed. / J. Banks, J. S. Carson, B. L. Nelson,
D. M. Nicol. — Englewood Cliffs: Prentice Hall, NJ, 2000.
61. Feldmann A., Whitt W. Fitting Mixtures of Exponentials to Long-Tail Distri-
butions to Analyze Network Performance Models // Performance Evaluation. —
1998. - № 31. - P. 245-254.
62. Fishman G. S. Principles of Discrete Event Simulation. — New York: Wiley&
Sons, 1978.
63. Fishwick P. A. SimPack: getting started with simulation programming in C and
C++ // Technical Report TR 92-022, Computer and Information Sciences / Uni-
versity of Florida. — Gainesville, Florida, 1992.
64. Fujimoto R. M. Parallel discrete event simulation // Communications of the
ACM. - 1990. - № 33(10). - P. 30-59.
65. Fujimoto R. M. Parallel discrete event simulation: will the field survive? // ORSA
Journal on Computing. — 1993. — № 5(3). — P. 213-230.
66. German R. Performance analysis of communication systems. Modeling with non-
Markovian stochastic Petri nets. — Chichester: John Wilet&Sons, 2000.
67. Gordon G. System Simulation. — Englewood Cliffs, NJ: Prentice Hall, 1978.
68. Gordon G. The Application of GPSS V to Discrete System Simulation. — Engle-
wood Cliffs, NJ: Prentice Hall, 1975.
69. Gross D., Harris С. M. Fundamentals of queueing theory. — New York: John
Wiley&Sons, 1985.
70. Haverkort B. Performance of Computer Communication Systems: A Model-Based
Approach. — New York: John Wiley&Sons, 1998.
71. JAVA 1.1: Энциклопедия / M. Моррисон, Д. Эблин, M. Аферган и др. — Киев:
ДиаСофт, 1998.
72. King J. Computer and Communication Systems Performance Modeling. — Engle-
wood Cliffs, NJ: Prentice Hall, 1990.
73. Kirschnick M. XPEPSY-Handbuch / Universitat Erlangen-Nurnberg; Institut fur
Mathematische Maschinen und Datenverarbeitung IV; Tech. Report. — 1993.
74. Kiviat P. J., Villanueva R., Markowitz H. M. SIMSCRIPT II.5 Programming Lan-
guage. — Los Angeles: Consolidated Analysis Centers, 1973.
75. Lindemann C. DSPNexpress: A Software Package for the Efficient Solution of
Deterministic and Stochastic Petri Nets // Performance Evaluation. — 1995. —
№ 22. P. 3-21.
76. Lindemann C. Performance Modeling with Deterministic and Stochastic Petri
Nets. — New York: John Wiley&Sons, 1998.
77. Little J. D. A proof of the queueing formula L = XW // Operations Research. —
1961. - № 9. - P. 383-387.
78. Misra J. Distributed discrete event simulation // ACM Computing Surveys. —
1986. - № 18(1). - P. 39-65.
404 Список литературы и источников из Интернета
79. Overeinder В. J., Hertzberger L. О., Sloot Р. М. A. Parallel Discrete Event Simu-
lation // The Third Workshop Computer Systems. — Eindhoven, The
Netherlands. — May, 1991. — P. 19-30.
80. Page E. H., Nance R. E. Parallel Discrete Event Simulation: A Modeling Metho-
dological Perspective // Proceedings of the АСМ/IEEE. SCS 8th Workshop on
Parallel and Distributed Simulation. — Edinburgh, Scotland, 6-8 July, 1994. —
P. 88-93.
81. Pritsker A. GASP IV Simulation Language. — New York: John Wiley&Sons, 1974.
82. Quantitative System Performance: Computer system analysis using queueing net-
work models // E. D. Lazowska, J. Zahorjan, G. S. Graham, К. C. Sevcik. —
Englewood Cliffs, NJ: Prentice Hall, 1984.
83. Queueing Networks and Markov Chains / G. Bolch, S. Greiner, H. de Meer,
K. Trivedi. — New York: John Wiley&Sons, 1998.
84. Reiser M., Sauer С. H. Queueing Network Models: Methods of Solution and Their
Program Implementation // Current Trends in Programming Methodology. Vol. 3:
Software Modeling. — Englewood Cliffs, NJ: Prentice Hall, 1975.
85. Shannon R. E. Simulation: A Survey with Research Suggestions // AIIE Trans-
actions. — 1975. — № 3.
86. Survey of languages and runtime libraries for parallel discrete-event simulation /
Y.-H. Low, Ch-C. Lim, W. Cai, at al. // Simulation. — 1999. — № 72(3). —
P. 170-186.
87. Tijms H. Stochastic Modeling and Analysis: A Computational Approach. — New
York: John Wiley&Sons, 1986.
88. Trivedi K. Probability and Statistics with Reliability. Queueing and Computer
Science Applications. — Englewood Cliffs, NJ: Prentice Hall, 1982.
89. Vee V.-Y., Hsu W.-J. Parallel discrete event simulation: a survey // Technical
Report, Centre for Advanced Information Systems / Nanyang Technological
University. — Singapore, 1999.
90. Williams T. Modelling Complex Projects. — New York: John Wiley&Sons, 2002.
91. www.dspnexpress.de
92. www.b-club.ru
93. www.cache.ru
94. www.dialog.samara.ru
95. www.gpss.ru
96. www.gpssworld.narod.ru
97. www.interface.ru/sysmod
98. www.lionhrtpub.com/orms/surveys/Simulation/Simulation.html
99. www.nwsta.ru/Soft/devel/arena.php
100. www.ospu.odessa.ua
101. www.powersim.com
Список литературы и источников из Интернета 405
102. www.uni-klu.ac.at/asi/easa/submisiions/DSPNexpress.html
103. www.xjtek.ru
104. www4.informatik.uni-erlangen.de/Projects/MOSEL
105. www.simulation.org.ua
Дополнительная литература
Вайнер Р., Пинсон Л. C++ изнутри. — Киев: ДиаСофт, 1993.
Гнеденко Б. В., Коваленко И. Н. Введение в теорию массового обслуживания. —
М.: Наука, 1987.
Гулътпяев А. Имитационное моделирование в среде Windows. — СПб.: Корона
принт, 1999.
Емельянов А. А., Власова Е. А., Дума Р. В. Имитационное моделирование эконо-
мических процессов. — М.: Финансы и статистика, 2002.
Калашников В. В. Организация моделирования сложных систем. — М.: Высш,
шк., 1990.
Кобелев Н. Б. Основы имитационного моделирования сложных экономических
систем. — М.: Дело, 2003.
Максимей И. В. Имитационное моделирование на ЭВМ. — М.: Радио и связь,
1988.
Матвеев В. Ф., Ушаков В. Г. Системы массового обслуживания. — М.: Изд-во
Моск. гос. ун-та, 1984.
Новиков О. А., Петухов С. И. Прикладные вопросы теории массового обслужива-
ния. — М.: Сов. радио, 1969.
Овчаров Л. А. Прикладные задачи теории массового обслуживания. — М.: Маши-
ностроение, 1969.
Павловский Ю. Н. Имитационные модели и системы. — М.: Высш, шк., 1990.
Советов Б. Я., Яковлев С. А. Моделирование систем. — М.: Высш, шк., 1998.
Тараканов К В., Овчаров Л. А., Тырышкин А. Н. Аналитические методы исследо-
вания систем. — М.: Сов. радио, 1974.
Томашевский В. Н. Имитационное моделирование систем и процессов. — Киев:
ВИПОЛ, 1994.
ANSI/ISO C++ Professional Programmer’s Handbook. — Indianapolis: Macmillan
Computer Publishing, 1999.
Carson J. S. Convincing User's of Model's Validity is Challenging Aspect of Mode-
ler's Job // Industrial Engineering. — 1996. — June. — P. 75-85.
Дополнительная литература 407
Harrell С. R., Tumay К. Simulation Made Easy: A Manager’s Guide. — Industrial
Engineering Press. — 1995.
Hoover S. V., Perry R. F. Simulation: A Problem Solving Approach. — Reading, MS:
Addison-Wesley. — 1990.
Kneppel P. L., Arangno D. C. Simulation Validation: A Confidence Assessment
Methodology // IEEE Computer Society Press. — 1994.
Korn G. A. Interactive Dynamic System Simulation Under Microsoft Windows 95
and NT. — Gordon and Beach, London. — 1998.
Law A. M. Designing and Analysis Simulation Experiments // Industrial Engi-
neering. — 1991. — № 3 — P. 20-23.
Naine P. Basic Elements of Queueing Theory: Applications to the Modelling of Com-
puter Systems: Lecture notes // www-net.cs.umass.edu/pe2000/nain.pdf.
Neelamkavil F. Computer Simulation and Modeling. — New York: John Wiley&Sons. —
1987.
Rumbaugh P. Object Oriented Modeling and Design. — Englewood Cliffs, NJ: Pren-
tice Hall. — 1991.
Thesen A., Travis L. E. Simulation for Decision Making. — St. Paul: West Publishing
Company. — 1992.
Алфавитный указатель
I inline-функции, 59 д деструктор, 59, 64 чисто виртуальный, 64
А абстрактный класс, 64 диаграмма состояний, 269 динамическое прогнозирование, 111
Б базовый класс, 60 библиотека CPSIM, 111 GTW, 111 ParaSol, 111 PSK, 111 Simkit, 111 SimPack, 114 SPaDES, 111 SPEEDES, 111 STL, 77 TWOS, 111 WARPED, 111 дисциплина обслуживания, 21 FCFS, 24 IS, 25 LCFS, 24 PS, 24 Round Robbin, 24 SIF, 24 SIRO, 24 абсолютный приоритет, 25, 31 относительный приоритет, 25, 31, 208 дружественные классы, 59 дружественный класс, 249
блок catch, 73 try, 73 3 замкнутая система, 185 значимость, 292
В время ожидания, 27, 30 время отклика, 27, 30 И инкапсуляция, 55 исключительная ситуация, 72
Г генератор случайных чисел, 79, 87, 141 генератор случайных чисел (ГСЧ), 81 гонки, 112 групповое обслуживание, 337 К календарь событий, 110 каналы неоднородные, 185, 188 однородные, 189 Кендалла—Башарина нотация, 24
Алфавитный указатель 409
конструктор, 58, 64
инициализации, 58
копирования, 58
по умолчанию, 58
коэффициент загрузки, 26, 30
критический путь, 290
критичность, 292
Л
Литтла закон, 27, 30-31
м
массив указателей, 321
метод
вариации произвольной
постоянной,355
конгруэнций, 81
математической индукции, 30
обратной функции, 79
остатков, 81
покоординатного спуска, 313
половинного деления, 84
регенерации, 41
Симпсона, 89
срединных квадратов, 81
механизм отката, 111
минимальный индекс группы, 337
модель, 33
н
наследование, 60, 248
оператор
delete, 68
new, 68
throw, 73
перегруженный, 68
П
параллельное моделирование, 109
консервативное, 111
оптимистичное, 111
паттерн проектирования, ИЗ
поле данных
закрытое, 106
поле данных {продолжение)
защищенное, 106
изменяемое, 122
неизменяемое, 122
открытое, 106
статическое, 58
поле данных класса
закрытое, 55
защищенное, 55
открытое, 55
порядок создания объектов, 219
предварительное объявление
класса, 143
принцип подстановки, 62
причинно-следственная
зависимость, 110
производный класс, 60, 248
Р
распределение вероятностей
«с тяжелым хвостом», 92
гиперэкспоненциальное, 24, 86
дискретное, 80, 91
нормальное, 42, 87, 184, 267, 320,
336, 354, 371
Парето, 82
Пуассона, 21, 35
равномерное, 81, 121, 152, 165,
230, 247, 336, 354, 371
треугольное, 281
экспоненциальное, 21, 24, 29, 80,
91, 115, 139, 199, 208, 229, 246,
267, 303, 320, 354
Эрланга, 24, 84, 115, 184,
199, 267
распределение числа
заявок, 26
С
сервер, 21, 31, 43, 80, 108
сетевая модель проекта, 281
система массового
обслуживания, 21, 26
замкнутая, 209, 372
многоканальная, 321, 336
открытая, 230, 321, 336
410 Алфавитный указатель
система моделирования @RISK for Projects, 295 Any Logic, 51 Arena, 47 DSPNexpress, 43 MVS, 51 NetStimulator, 50 PEPSY, 46 Powersim, 49 Process Model, 50 RESQ, 43 Stella/iThink, 49 Vensim, 49 ДАСИМ, 47 случайное блуждание, 230 средняя длина очереди, 27, 30 статическая переменная, 90 статический метод, 58 стационарный режим, 23, 26, 40, 177, 313, 321 схема моделирования event-driven, 47, 110, 114 time-driven, 47, 109 условие баланса, 29 нормировки, 30, 162 Ф, Ш фаза моделирования валидации, 36 верификации, 36 проектирования, 36 функция виртуальная, 61 дружественная, 58-59 целевая, 242 чисто виртуальная, 64 шаблон, 74 Я язык моделирования, 42 APOSTLE, 111 GASP, 43 GPSS, 42 ModSim, 111 MOSEL, 44 Parsec, 111
Т тупик, 113 SIMAN, 47 SIMSCRIPT, 43 SLAM II, 43
У узкое место системы, 205 универсальная моделирующая среда, 49 уравнение дифференциальное, 354 YADDES, 111 язык программирования Cache Object Script, 107, 143 Fortran, 43, 50 Java, 51, 63
Илья Иосифович Труб
Объектно-ориентированное моделирование на C++:
Учебный курс
Главный редактор
Заведующий редакцией
Руководитель проекта
Научный редактор
Литературный редактор
Художник
Иллюстрации
Корректоры
Верстка
Е. Строганова
А. Кривцов
А. Адаменко
В. Лаптев
Н. Рощина
Л. Адуевская
С. Романов
И. Смирнова, И. Хохлова
Ю. Сергиенко
Лицензия ИД № 05784 от 07.09.01.
Подписано в печать 01.07.05. Формат 70X100/16. Усл. п. л. 33,54. Тираж 3000 экз. Заказ № 2435.
ООО «Питер Принт». 194044, Санкт-Петербург, пр. Б. Сампсониевский, 29а.
Налоговая льгота — общероссийский классификатор продукции ОК 005-93, том 2; 953005 — литература учебная.
Отпечатано с готовых диапозитивов в ФГУП «Печатный двор» им. А. М. Горького
Федерального агентства по печати и массовым коммуникациям.
197110, Санкт-Петербург, Чкаловский пр., 15.
В1997 году по инициативе генерального директора Издательского дома «Питер»
Валерия Степанова и при поддержке деловых кругов города в Санкт-Петербурге
был основан «Книжный клуб Профессионал». Он собрал под флагом клуба про-
фессионалов своего дела, которых объединяет постоянная тяга к знаниям и любовь
к книгам. Членами клуба являются лучшие студенты и известные практики из раз-
ных сфер деятельности, которые хотят стать или уже стали профессионалами в той
или иной области.
Как и все развивающиеся проекты, с течением времени книжный клуб вырос
в «Клуб Профессионал». Идею клуба сегодня формируют три основные «клубные»
функции:
• неформальное общение и совместный досуг интересных людей;
• участие в подготовке специалистов высокого класса
(семинары, пакеты книг по специальной литературе);
• формирование и высказывание мнений современного профессионала
(при встречах и на страницах журнала).
КАК ВСТУПИТЬ В КЛУБ?
Для вступления в «Клуб Профессионал» вам необходимо:
• ознакомиться с правилами вступления в «Клуб Профессионал»
на страницах журнала или на сайте www.piter.com;
• выразить свое желание вступить в «Клуб Профессионал»
по электронной почте postbook@piter.com или по тел. (812) 103-73-74;
• заказать книги на сумму не менее 500 рублей в течение любого времени
или приобрести комплект «Библиотека профессионала».
«БИБЛИОТЕКА ПРОФЕССИОНАЛА»
Мы предлагаем вам получить все необходимые знания, подписавшись на «Библио-
теку профессионала». Она для тех, кто экономит не только время, но и деньги.
Покупая комплект - книжную полку «Библиотека профессионала», вы получаете:
• скидку 15% от розничной цены издания, без учета почтовых расходов;
• при покупке двух или более комплектов - дополнительную скидку 3%;
• членство в «Клубе Профессионал»;
• подарок - журнал «Клуб Профессионал».
ПЗДАГВПЬСКИЯ аом
ПИТЕР
WWW.PITER.COM
Закажите бесплатный журнал
«Клуб Профессионал».
^ППТЕР*
Нет времени
ходить по магазинам?
www.piter.com
Новые книги — в момент выхода из типографии
Информацию о книге — отзывы, рецензии, отрывки
Старые книги — в библиотеке и на CD
И наконец, вы нигде не купите
наши книги дешевле!
КНИГАПОЧТОЙ
ЗАКАЗАТЬ КНИГИ ИЗДАТЕЛЬСКОГО ДОМА «ПИТЕР»
МОЖНО ЛЮБЫМ УДОБНЫМ ДЛЯ ВАС СПОСОБОМ:
• по телефону: (812) 103-73-74;
• по электронному адресу: postbook@piter.com;
• на нашем сервере: www.piter.com;
• по почте: 197198, Санкт-Петербург, а/я 619,
ЗАО «Питер Поср>.
ВЫ МОЖЕТЕ ВЫБРАТЬ ОДИН ИЗ ДВУХ СПОСОБОВ ДОСТАВКИ
И ОПЛАТЫ ИЗДАНИЙ:
® Наложенным платежом с оплатой заказа при получении посылки на
ближайшем почтовом отделении. Цены на издания приведены ориентиро-
вочно и включают в себя стоимость пересылки по почте (но без учета
авиатарифа). Книги будут высланы нашей службой «Книга-почтой»
в течение двух недель после получения заказа или выхода книги из печати.
Оплата наличными при курьерской доставке (для жителей Москвы
и Санкт-Петербурга). Курьер доставит заказ по указанному адресу
в удобное для вас время в течение трех дней.
ПРИ ОФОРМЛЕНИИ ЗАКАЗА УКАЖИТЕ:
• фамилию, имя, отчество, телефон, факс, e-mail;
• почтовый индекс, регион, район, населенный пункт,
улицу, дом, корпус, квартиру;
• название книги, автора, код, количество заказываемых
экземпляров.
Вы можете заказать бесплатный
журнал «Клуб Профессионал»
ИЗДАТЕПЬСКПП ДОМ
ПИТЕР
WWW.PITER.COM
ИЗДАТЕЛЬСКИЙ ДОМ
СПЕЦИАЛИСТАМ
КНИЖНОГО БИЗНЕСА!
ПРЕДСТАВИТЕЛЬСТВА ИЗДАТЕЛЬСКОГО ДОМА «ПИТЕР»
предлагают эксклюзивный ассортимент компьютерной, медицинской,
психологической, экономической и популярной литературы
РОССИЯ
Москва м. «Калужская», ул. Бутлерова, д 176, офис 207,240; телефакс (095) 777-54-67;
e-mail: sales@piter.msk.ru
Санкт-Петербург м. «Выборгская», Б. Сампсониевский пр., д. 29а;
тел. (812) 103-73-73, факс (812) 103-73-83; e-mail: sales@piter.com
Воронеж ул. 25 января, д. 4; тел. (0732) 39-61 -70;
e-mail: piter-vrn@vmail.ru; piterv@comch.ru
Екатеринбург ул. 8 Марта, д. 2676; тел./факс (343) 225-39-94, 225-40-20;
e-mail: piter-ural@isnet.ru
Нижний Новгород ул. Совхозная, д. 13; тел. (8312) 41 -27-31;
e-mail: piter@infonet.nnov.ru
Новосибирск ул. Немировича-Данченко, д. 104, офис 502;
тел./факс (3832) 54-13-09,47-92-93,11-27-18,11-93-18; e-mail: piter-sib@risp.ru
Ростов-на-Дону ул. Ульяновская, д. 26; тел. (8632) 69-91-22;
e-mail: jupiter@rost.ru
Самара ул. Новосадовая, д. 4; тел. (8462) 37-06-07; e-mail: piter-volga@sama.ru
УКРАИНА
Харьков ул. Суздальские ряды, д. 12, офис 10-11; тел. (057) 751-10-02, (0572) 58-41-45,
телефакс (057) 712-27-05; e-mail: piter@kharkov.piter.com
Киев пр. Красных Казаков, д. 6, корп. 1; тел./факс (044) 490-35-68,490-35-69;
e-mail: office@piter-press.kiev.ua
БЕЛАРУСЬ
Минск ул. Бобруйская, д. 21, офис 3; тел./факс (37517) 226-19-53;
e-mail: office@minsk.piter.com
Ищем зарубежных партнеров или посредников, имеющих выход на зарубежный рынок.
Телефон для связи: (812) 103-73-73.
E-mail: grigorjan@piter.com
Издательский дом «Питер» приглашает к сотрудничеству авторов.
Обращайтесь по телефонам: Санкт-Петербург - (812) 327-13-11,
Москва — (095) 777-54-67.
Заказ книг для вузов и библиотек: (812) 103-73-73.
Специальное предложение - e-mail: kozin@piter.com
ИЗДАТЕЛЬСКИЙ ДОМ
УВАЖАЕМЫЕ ГОСПОДА!
КНИГИ ИЗДАТЕЛЬСКОГО ДОМА «ПИТЕР»
ВЫ МОЖЕТЕ ПРИОБРЕСТИ
ОПТОМ И В РОЗНИЦУ
У НАШИХ РЕГИОНАЛЬНЫХ ПАРТНЕРОВ.
Башкортостан
Уфа, «Азия», ул. Зенцова, д. 70 (оптовая продажа),
маг. «Оазис», ул. Чернышевского, д. 88,
тел./факс (3472) 50-39-00.
E-mail: asiaufa@ufanet.ru
Дальний Восток
Владивосток, «Приморский торговый дом книги»,
тел./факс (4232) 23-82-12.
E-mail: bookbase@mail.primorye.ru
Хабаровск, «Мирс»,
тел. (4212) 30-54-47, факс 22-73-30.
E-mail: sale_book@bookmirs.khv.ru
Хабаровск, «Книжный мир»,
тел. (4212) 32-85-51, факс 32-82-50.
E-mail: postmaster@worldbooks.kht.ru
Европейские регионы России
Архангельск, «Дом книги»,
тел. (8182) 65-41-34, факс 65-41-34.
E-mail: book@atnet.ru
Калининград, «Вестер»,
тел./факс (0112) 21 -56-28,21 -62-07.
E-mail: nshibkova@vester.ru
http://www.vester.ru
Северный Кавказ
Ессентуки, «Россы», ул. Октябрьская, 424,
тел./факс (87934) 6-93-09.
E-mail: rossy@kmw.ru
Сибирь
Иркутск, «ПродаЛитЪ»,
тел. (3952) 59-13-70, факс51-30-70.
E-mail: prodalit@irk.ru
http://www.prodalit.irk.ru
Иркутск, «Антей-книга»,
тел./факс (3952) 33-42-47.
E-mail: antey@irk.ru
Красноярск, «Книжный мир»,
тел./факс (3912) 27-39-71.
E-mail: book-world@public.krasnet.ru
Нижневартовск, «Дом книги»,
тел. (3466) 23-27-14, факс 23-59-50.
E-mail: book@nvartovsk.wsnet.ru
Новосибирск, «Топ-книга»,
тел. (3832) 36-10-26, факс 36-10-27.
E-mail: office@top-kniga.ru
http://www.top-kniga.ru
Тюмень, «Друг»,
тел./факс (3452) 21 -34-82.
E-mail: drug@tyumen.ru
Тюмень, «Фолиант»,
тел. (3452) 27-36-06, факс 27-36-11.
E-mail: foliant@tyumen.ru
Челябинск, ТД «Эврика», ул. Барбюса, д. 61,
тел./факс (3512) 52-49-23.
E-mail:evrika@chel.surnet.ru
Татарстан
Казань, «Таис»,
тел. (8432) 72-34-55, факс 72-27-82.
E-mail: tais@bancorp.ru
Урал
Екатеринбург, магазин № 14,
ул Челюскинцев, д. 23,
телефакс (3432) 53-24-90.
E-mail: gvardia@mail.ur.ru
Екатеринбург, «Валео-книга»,
ул. Ключевская, д. 5,
тел./факс (3432) 42-56-00.
E-mail: valeo@etel.ru
АНТИВИРУС
ИГОРЯ ДАНИЛОВА
Dr.WEB
www.drweb.ru
Illi IIIIIIII НИИ
Илья Труб
ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ МОДЕЛИРОВАНИЕ НА £ + +
УЧЕБНЫЙ КУРС
и-
Существует очень много специализированных языков и визуальных сред моделирования,
но как понять, что именно нужно для решения конкретной задачи? Из этой книги вы
узнаете, как в короткие сроки создать информативную имитационную модель сложной
системы, не используя специальных пакетов и библиотек. На многочисленных примерах
вы разберетесь со всеми этапами формирования модели, включая проектирование
классов, выделение состояний и событий, обмен сообщениями, генерацию случайных
чисел, масштабирование времени, планирование эксперимента, оценку достоверности
результатов, постановку и решение задач оптимизации систем. Освоите безграничные
возможности языка C++, которые позволяют решать эти задачи. Книга будет полезна
студентам, преподавателям, программистам, научным работникам в качестве учебного
пособия и в качестве источника для нахождения практических решений.
^ППТС-Р
Заказ книг:
197198, Санкт-Петербург, а/я 619, тел.: (812) 103-73-74, postbook@piter.com
61093, Харьков-93, а/я 9130, тел.: (057) 712-27-05, piter@kharkov.piter.com
www.piter.com — вся информация о книгах и веб-магазин