vak: (Default)
[personal profile] vak
Давайте покажу вам в упрощённом варианте, чем я занимаюсь по работе последние полтора десятка лет. Симуляторы компьютерных архитектур, которые по сути сводятся к так называемому Discrete-event simulation.

Промоделируем на Си++ классическую задачку: пять обедающих философов.

Пятеро молчаливых философов сидят за круглым столом с мисками с едой. Между каждой парой философов раскладываются вилки. Весь день философы по очереди едят и думают. Чтобы есть, философу нужно две вилки, но каждой вилкой одновременно может пользоваться только один философ. В любой момент философ может взять или положить вилку справа или слева от себя, но не может начать есть, пока не получит обе вилки.



На видео показано, как процесс может выглядеть а текстовом экране. Думающий философ показан синим цветом, кушающий - зелёным, ожидающий - красным.



Смоделируем это дело на Си++. Представим каждого философа объектом класса 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();
}
}
Вилка штука простая. Она находится в одном из двух состояний: свободна или занята.
enum class Fork {
FREE,
TAKEN,
};
Философ имеет уникальный идентификатор (index), ссылки на левую и правую вилки, а также состояние думаем/едим/ждём. Приватный метод random() генерит случайное число в указанном диапазоне - как долго философ будет "думать". Приватный метод display() отобразит состояние философа на экране.
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;
}
Самое интересное происходит в методе behave().
    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());
}
Заметьте, что класс Philosopher унаследован от класса Event. Его я покажу позже, но суть в том, что симулятор будет ставить философов в очередь событий, и в нужное время вызывать для них метод behave(). При этом в качестве аргумента передаётся ссылка на сам симулятор. Чтобы продолжить ожидание на некоторый интервал времени (здесь в миллисекундах), философ вызывает метод sim.wait().

Собственно, это всё про устройство философов. Теперь заглянем в устройство симулятора. Что такое "событие"? Оно определяет виртуальный метод behave(), который должен быть реализован для всякого симулируемого объекта. Также событие имеет приватное поле start_time - момент времени, когда событие должно наступить. В этот момент будет вызван метод behave().
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;
};
Также для событий задан способ упорядочивать их по времени - структура Cmp с оператором ().

Симулятор состоит из счётчика времени (поле master_clock) и очереди событий (поле queue). Метод wait() добавляет событие в очередь, откладывая его на указанный интервал времени относительно "сейчас". Метод run() активируем следующее событие из очереди.
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();
};
Обратите внимание очередь queue реализована как std::set - множество указателей на объекты Event, упорядоченное по времени посредством метода Event::Cmp(). Дёшево и сердито.

Ожидание, то есть занесение объекта в очередь будущих событий, делается очень просто. Запоминаем нужное значение времени, затем вызываем метод insert() класса std::set.
    void wait(Event &event, unsigned num_cycles)
{
event.start_time = master_clock + num_cycles;
queue.insert(&event);
}
Напомню, что основной цикл симуляции в программе main() работает так:
    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

Date: 2023-10-19 23:43 (UTC)
kondybas: (Default)
From: [personal profile] kondybas
Товарищ в конце 1990-х наваял полную либу компонентов для делфи для симуляций такого рода. А я на этой либе сварганил решение этой задачи Дейкстры (и еще нескольких других, более интересных). И мое решение даже тиснули в "Simulation News/Europe" за 1998-й, кажется. Самым бесценным было выражение лиц начальства в конторе, когда на мое имя пришел толстенный иностранный пакет DHL с авторскими экземплярами :)

Date: 2023-10-19 23:58 (UTC)
kondybas: (Default)
From: [personal profile] kondybas
В личку отправил

Date: 2023-10-20 00:14 (UTC)
kondybas: (Default)
From: [personal profile] kondybas
Если интересно, могу свести с автором либы, он ее потом еще для дотнета переписал, но коммерчески она, почему-то, не взлетела. Штука, на самом деле, крайне кузявая, я делал на ней модель полевого госпиталя для МЧС, для оптимизации штата, оборудования и складов под разные виды дизастеров - пожары, землетрясения, наводнения етц.

Date: 2023-10-20 11:44 (UTC)
kondybas: (Default)
From: [personal profile] kondybas
Это совершенно оригинальная разработка с нуля.
Но даже после переписывания под дотнет оно все равно пиплу не зашло. Вообще, это довольно специфичная тема, на выхлопе имеем статистическую картину а не детерминированый расклад, возможно в этом причина.

Date: 2023-10-20 00:23 (UTC)
kondybas: (Default)
From: [personal profile] kondybas
Эх, детство босоногое! Слезы умиления наворачиваются :)
From: [personal profile] carrauntoohil
Заранее большое спасибо )

Date: 2023-10-20 00:22 (UTC)
cali4nickation: (Default)
From: [personal profile] cali4nickation
А есть не связанные с историей аргументы почему не использовать языки из этого века и может быть даже с GC (на работе но для этого домена)?

Date: 2023-10-20 00:47 (UTC)
ircicq: (Default)
From: [personal profile] ircicq
JIT сильно продвинулись по производительности за последние 10 лет

Сейчас на Си/Си++ писать производительнее чем C#, надо хорошо знать низкоуровневые функции, типа AVX.
Да и в них есть подвижки в .NET

Date: 2023-10-20 16:37 (UTC)
cali4nickation: (Default)
From: [personal profile] cali4nickation
Плюсы моя первая любовь, так что я не критиковал а мне было интересно у кого за пределами embedded остались причины его использовать. Где под причинами кажется обычно понимают способность нанять молодежь.

Я не ожидал что вы поставите golang в тот же ряд как C++ и Rust. Он вроде даже по меркам Java убог неуклюж.

Fun fact: Мартин Одерски похожий пример выбрал в своей книге про Scala еще в 2009: "10.3 Extended Example: Discrete Event Simulation".
Edited Date: 2023-10-20 16:39 (UTC)

Date: 2023-10-20 00:33 (UTC)
sobriquet9: (Default)
From: [personal profile] sobriquet9

А где в приведённом примере поможет GC? Там же вроде нет никакого явного управления памятью, никаких malloc()/free()/new/delete. Какой мусор он будет собирать?

Date: 2023-10-20 01:52 (UTC)
ircicq: (Default)
From: [personal profile] ircicq
shared_ptr (на подсчёте ссылок) решает проблему освобождения только древовидных моделей.
С циклическими ссылками (графы) нужен другой подход.

Date: 2023-10-21 01:53 (UTC)
sobriquet9: (Default)
From: [personal profile] sobriquet9

С циклическими ссылками и у сборщика мусора могут возникнуть проблемы. Особенно если система реального времени, которую нельзя остановить на произвольное время сборки мусора.

Date: 2023-10-21 03:13 (UTC)
ircicq: (Default)
From: [personal profile] ircicq
Это решают найстройкой режимов.

Есть алгоритмы GC, требующие остановки только на короткие периоды.
Можно запрещать GC на критические периоды real-time, сделав достаточный резерв памяти.

Да и Си-шный malloc()/free() не гарантирует фиксировнного overhead
Приходится консолидировать списки блоков, занимать у других пулов, если собственнй пул треда исчерпался, и т.д.

Строгий Real-Time разве что на статических переменных можно писать.



Date: 2023-10-20 03:34 (UTC)
ircicq: (Default)
From: [personal profile] ircicq

// Get next event from queue.
Event *event = *queue.begin();
queue.erase(event);


Эффективнее (без лишнего поиска по множеству) так:
auto it = queue.begin();
Event *event = *it;
queue.erase(it);


или даже
Event *event = queue.extract(queue.begin()).value();   // C++17

Edited Date: 2023-10-20 03:38 (UTC)

Date: 2023-10-20 07:17 (UTC)
juan_gandhi: (Default)
From: [personal profile] juan_gandhi

О как элегантно написано! Восхищен.

Date: 2023-10-21 05:47 (UTC)
juan_gandhi: (Default)
From: [personal profile] juan_gandhi

Не в смысле книжку Хора, CSP?

Date: 2023-10-21 06:44 (UTC)
juan_gandhi: (Default)
From: [personal profile] juan_gandhi

Да я бы не квалифицировал это как теорию; скорее как философию.

Date: 2023-10-21 16:25 (UTC)
ufm: (Default)
From: [personal profile] ufm
            if (left_fork == Fork::FREE && right_fork == Fork::FREE) {
                left_fork  = Fork::TAKEN;
                right_fork = Fork::TAKEN;
                state      = State::EATING;
                sim.wait(*this, 1000);
            }

Понимаю что в данном случае проблем не будет, но всё равно, когда вижу такие конструкции без локов - аж вздрагиваю. :)

Date: 2023-10-22 08:18 (UTC)
ufm: (Default)
From: [personal profile] ufm
Дык я и говорю - понятно, что оно тут не неужно, но... Разработчики питона тоже так говорили. :)

P.S. Не обращай внимания, у меня профдеформация на предмет того, что мир вокруг нас асинхронный и многопоточный.

Date: 2023-10-26 13:19 (UTC)
From: [personal profile] igaa
Для "задачи философов" напрашивается многопоточность. Насколько помню, у Дейкстры как раз были семафоры.

Тут вы обошлись без семафоров (и потенциальных дедлоков) за счёт "телепатии" - замены индетерминированного wait на детерминированный wait с ordered queue.

Этот подход не сработал бы, если бы "философы" действительно были бы случайны, и не знали бы, сколько они будут "думать" заранее перед тем как начать "думать". Скажем, если бы "философ" ориентировался на какое-то внешнее случайное (точнее, непредсказуемое) событие для окончания "думания".