![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
Давайте покажу вам в упрощённом варианте, чем я занимаюсь по работе последние полтора десятка лет. Симуляторы компьютерных архитектур, которые по сути сводятся к так называемому Discrete-event simulation.
Промоделируем на Си++ классическую задачку: пять обедающих философов.
Пятеро молчаливых философов сидят за круглым столом с мисками с едой. Между каждой парой философов раскладываются вилки. Весь день философы по очереди едят и думают. Чтобы есть, философу нужно две вилки, но каждой вилкой одновременно может пользоваться только один философ. В любой момент философ может взять или положить вилку справа или слева от себя, но не может начать есть, пока не получит обе вилки.

На видео показано, как процесс может выглядеть а текстовом экране. Думающий философ показан синим цветом, кушающий - зелёным, ожидающий - красным.
Смоделируем это дело на Си++. Представим каждого философа объектом класса Philosopher, каждую вилку объектом класса Fork. Объект Simulator будет разворачивать процесс во времени. Вот такая главная программа:
Собственно, это всё про устройство философов. Теперь заглянем в устройство симулятора. Что такое "событие"? Оно определяет виртуальный метод behave(), который должен быть реализован для всякого симулируемого объекта. Также событие имеет приватное поле start_time - момент времени, когда событие должно наступить. В этот момент будет вызван метод behave().
Симулятор состоит из счётчика времени (поле master_clock) и очереди событий (поле queue). Метод wait() добавляет событие в очередь, откладывая его на указанный интервал времени относительно "сейчас". Метод run() активируем следующее событие из очереди.
Ожидание, то есть занесение объекта в очередь будущих событий, делается очень просто. Запоминаем нужное значение времени, затем вызываем метод insert() класса std::set.
Промоделируем на Си++ классическую задачку: пять обедающих философов.
Пятеро молчаливых философов сидят за круглым столом с мисками с едой. Между каждой парой философов раскладываются вилки. Весь день философы по очереди едят и думают. Чтобы есть, философу нужно две вилки, но каждой вилкой одновременно может пользоваться только один философ. В любой момент философ может взять или положить вилку справа или слева от себя, но не может начать есть, пока не получит обе вилки.

На видео показано, как процесс может выглядеть а текстовом экране. Думающий философ показан синим цветом, кушающий - зелёным, ожидающий - красным.
Смоделируем это дело на Си++. Представим каждого философа объектом класса Philosopher, каждую вилку объектом класса Fork. Объект Simulator будет разворачивать процесс во времени. Вот такая главная программа:
Вилка штука простая. Она находится в одном из двух состояний: свободна или занята.int main()
{
Fork forks[5]{};
Philosopher a(0, forks[0], forks[1]);
Philosopher b(1, forks[1], forks[2]);
Philosopher c(2, forks[2], forks[3]);
Philosopher d(3, forks[3], forks[4]);
Philosopher e(4, forks[4], forks[0]);
Simulator sim;
a.behave(sim);
b.behave(sim);
c.behave(sim);
d.behave(sim);
e.behave(sim);
while (sim.is_active()) {
sim.run();
}
}
Философ имеет уникальный идентификатор (index), ссылки на левую и правую вилки, а также состояние думаем/едим/ждём. Приватный метод random() генерит случайное число в указанном диапазоне - как долго философ будет "думать". Приватный метод display() отобразит состояние философа на экране.enum class Fork {
FREE,
TAKEN,
};
Самое интересное происходит в методе behave().class Philosopher : public Event {
private:
const unsigned index;
Fork &left_fork;
Fork &right_fork;
enum class State {
THINKING,
EATING,
WAITING,
};
State state{};
unsigned random(unsigned lo, unsigned hi);
void display(uint64_t current_time) const;
public:
Philosopher(unsigned i, Fork &l, Fork &r) : index(i), left_fork(l), right_fork(r) {}
void behave(Simulator &sim) override;
}
Заметьте, что класс Philosopher унаследован от класса Event. Его я покажу позже, но суть в том, что симулятор будет ставить философов в очередь событий, и в нужное время вызывать для них метод behave(). При этом в качестве аргумента передаётся ссылка на сам симулятор. Чтобы продолжить ожидание на некоторый интервал времени (здесь в миллисекундах), философ вызывает метод sim.wait().void behave(Simulator &sim) override
{
if (state != State::EATING) {
// Хотим начать есть.
if (left_fork == Fork::FREE && right_fork == Fork::FREE) {
left_fork = Fork::TAKEN;
right_fork = Fork::TAKEN;
state = State::EATING;
sim.wait(*this, 1000);
} else {
// Ждём, когда освободятся вилки.
state = State::WAITING;
sim.wait(*this, 10);
}
} else {
// Прекращаем есть, начинаем думать.
left_fork = Fork::FREE;
right_fork = Fork::FREE;
state = State::THINKING;
sim.wait(*this, random(10, 2000));
}
display(sim.get_clock());
}
Собственно, это всё про устройство философов. Теперь заглянем в устройство симулятора. Что такое "событие"? Оно определяет виртуальный метод behave(), который должен быть реализован для всякого симулируемого объекта. Также событие имеет приватное поле start_time - момент времени, когда событие должно наступить. В этот момент будет вызван метод behave().
Также для событий задан способ упорядочивать их по времени - структура Cmp с оператором ().class Event {
public:
virtual void behave(Simulator &sim) = 0;
private:
uint64_t start_time{};
// Сравниваем два события на больше-меньше, для упорядочивания по времени.
struct Cmp {
bool operator()(const Event *a, const Event *b) const
{
if (a->start_time < b->start_time)
return true;
if (a->start_time > b->start_time)
return false;
return (intptr_t)a < (intptr_t)b;
}
};
friend class Simulator;
};
Симулятор состоит из счётчика времени (поле master_clock) и очереди событий (поле queue). Метод wait() добавляет событие в очередь, откладывая его на указанный интервал времени относительно "сейчас". Метод run() активируем следующее событие из очереди.
Обратите внимание очередь queue реализована как std::set - множество указателей на объекты Event, упорядоченное по времени посредством метода Event::Cmp(). Дёшево и сердито.class Simulator {
private:
// Текущее время
uint64_t master_clock{};
// Очередь событий
std::set<Event *, Event::Cmp> queue;
public:
uint64_t get_clock() const { return master_clock; }
// Есть ли чего в очереди?
bool is_active() const { return !queue.empty(); }
// Добавление события в очередь
void wait(Event &event, unsigned num_cycles);
// Выполнение следующего события из очереди
void run();
};
Ожидание, то есть занесение объекта в очередь будущих событий, делается очень просто. Запоминаем нужное значение времени, затем вызываем метод insert() класса std::set.
Напомню, что основной цикл симуляции в программе main() работает так:void wait(Event &event, unsigned num_cycles)
{
event.start_time = master_clock + num_cycles;
queue.insert(&event);
}
То есть пока есть события в очереди - продолжаем их обрабатывать. А вот как выглядит обработка одного события:while (sim.is_active()) {
sim.run();
}
Вот в общих чертах вся премудрость симуляторов дискретного времени. Очередь событий и никаких гвоздей. 😀 По жизни, конечно, оно обрастает массой деталей, но это уже второстепенное.void run()
{
// Извлекаем следующее (по времени) событие из очереди.
Event *event = queue.extract(queue.begin()).value();
// Если нужно - наращиваем счётчик времени.
// Также подвешиваем программу на это время, для наглядности.
if (event->start_time > master_clock) {
unsigned msec = event->start_time - master_clock;
usleep(msec * 1000);
}
master_clock = event->start_time;
// Выполняем событие.
event->behave(*this);
}
Исходники здесь: github.com/sergev/vak-opensource/tree/master/languages/c++/philosophers
no subject
Date: 2023-10-19 23:43 (UTC)no subject
Date: 2023-10-19 23:47 (UTC)no subject
Date: 2023-10-19 23:58 (UTC)no subject
Date: 2023-10-20 00:05 (UTC)no subject
Date: 2023-10-20 00:14 (UTC)no subject
Date: 2023-10-20 01:57 (UTC)Обычно паскальный софт не сильно востребован в индустрии.
no subject
Date: 2023-10-20 11:44 (UTC)Но даже после переписывания под дотнет оно все равно пиплу не зашло. Вообще, это довольно специфичная тема, на выхлопе имеем статистическую картину а не детерминированый расклад, возможно в этом причина.
no subject
Date: 2023-10-20 00:19 (UTC)no subject
Date: 2023-10-20 00:23 (UTC)А можно тоже ссылочку на статью
Date: 2023-10-20 11:40 (UTC)no subject
Date: 2023-10-20 00:22 (UTC)no subject
Date: 2023-10-20 00:26 (UTC)Языки, требующие виртуальной машины, не годятся по соображениям производительности.
no subject
Date: 2023-10-20 00:47 (UTC)Сейчас на Си/Си++ писать производительнее чем C#, надо хорошо знать низкоуровневые функции, типа AVX.
Да и в них есть подвижки в .NET
no subject
Date: 2023-10-20 01:06 (UTC)no subject
Date: 2023-10-20 16:37 (UTC)Я не ожидал что вы поставите golang в тот же ряд как C++ и Rust. Он вроде даже по меркам Java
убогнеуклюж.Fun fact: Мартин Одерски похожий пример выбрал в своей книге про Scala еще в 2009: "10.3 Extended Example: Discrete Event Simulation".
no subject
Date: 2023-10-20 20:36 (UTC)no subject
Date: 2023-10-20 00:33 (UTC)А где в приведённом примере поможет GC? Там же вроде нет никакого явного управления памятью, никаких malloc()/free()/new/delete. Какой мусор он будет собирать?
no subject
Date: 2023-10-20 01:13 (UTC)Даже в этом примере видна проблема. Тут я размещаю объекты Event на стеке, но обычно они выделяются динамически. В очередь событий заносятся только указатели на них. Что есть программа по какой-то причине уничтожит объект Event, который всё ещё находится в очереди событий? Всё крашанётся по кривому указателю. А такие вещи в большой системе сложно отследить. Использовать uniq_ptr в очереди событий сложно, морока с передачей объекта по цепочке вызовов. Вроде как share_ptr выглядит решением, но он добавляет расходы по памяти и эффективности, сравнимые или хуже чем хороший сборщик мусора.
no subject
Date: 2023-10-20 01:52 (UTC)С циклическими ссылками (графы) нужен другой подход.
no subject
Date: 2023-10-20 01:58 (UTC)no subject
Date: 2023-10-21 01:53 (UTC)С циклическими ссылками и у сборщика мусора могут возникнуть проблемы. Особенно если система реального времени, которую нельзя остановить на произвольное время сборки мусора.
no subject
Date: 2023-10-21 03:13 (UTC)Есть алгоритмы GC, требующие остановки только на короткие периоды.
Можно запрещать GC на критические периоды real-time, сделав достаточный резерв памяти.
Да и Си-шный malloc()/free() не гарантирует фиксировнного overhead
Приходится консолидировать списки блоков, занимать у других пулов, если собственнй пул треда исчерпался, и т.д.
Строгий Real-Time разве что на статических переменных можно писать.
no subject
Date: 2023-10-20 03:34 (UTC)// Get next event from queue.
Event *event = *queue.begin();
queue.erase(event);
Эффективнее (без лишнего поиска по множеству) так:
или даже
no subject
Date: 2023-10-20 04:28 (UTC)Поправлю в тексте.
Век живи, век учись.
no subject
Date: 2023-10-20 07:17 (UTC)О как элегантно написано! Восхищен.
no subject
Date: 2023-10-20 21:50 (UTC)no subject
Date: 2023-10-21 05:47 (UTC)Не в смысле книжку Хора, CSP?
no subject
Date: 2023-10-21 06:17 (UTC)Красиво изложено, но почему-то такие теории ни разу мне не пригождались. Не дают ответа на вопрос: как сесть и написать хороший симулятор.
no subject
Date: 2023-10-21 06:44 (UTC)Да я бы не квалифицировал это как теорию; скорее как философию.
no subject
Date: 2023-10-21 07:50 (UTC)no subject
Date: 2023-10-21 16:25 (UTC)Понимаю что в данном случае проблем не будет, но всё равно, когда вижу такие конструкции без локов - аж вздрагиваю. :)
no subject
Date: 2023-10-22 03:43 (UTC)no subject
Date: 2023-10-22 08:18 (UTC)P.S. Не обращай внимания, у меня профдеформация на предмет того, что мир вокруг нас асинхронный и многопоточный.
no subject
Date: 2023-10-26 13:19 (UTC)Тут вы обошлись без семафоров (и потенциальных дедлоков) за счёт "телепатии" - замены индетерминированного wait на детерминированный wait с ordered queue.
Этот подход не сработал бы, если бы "философы" действительно были бы случайны, и не знали бы, сколько они будут "думать" заранее перед тем как начать "думать". Скажем, если бы "философ" ориентировался на какое-то внешнее случайное (точнее, непредсказуемое) событие для окончания "думания".
no subject
Date: 2023-11-19 06:03 (UTC)Представьте, что у вас миллионы или миллиарды таких "философов". Как встречается сплошь и рядом в симуляции цифровой логики или физики частиц. Никаких потоков не хватит. Семафоры и переключение контекста это слишком дорого.