Перейти к содержанию
    

Система, управляемая событиями и SST(super-simple tasker)

Да копаю я Erlang, но пока много кода на плюсах сделано, так что пока идет переходной процесс.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

:bb-offtopic:

'Энтузизизм' это конечно хорошо, но надо знать меру :)

Был у меня в студенческую пору (где то конец 80х) научный руководитель (который сам года 2 назад еще был студентом), и написал я (от нечего делать) простой оконный менеджер (a-la Windows, но в текстовом режиме), и даже добавил туда thread'ы (все это собиралось на Borland C++ 3.1). В принципе было похоже на Turbo Vision (от того же Borland), но на тот момент он еще не вышел в массы :)

 

Принцип построения этой системы так вдохновил моего 'научного руководителя', что он бегал по всему НИИ и с диким воссторгом и пеной у рта рассказывал всем, какая это крутая штука. Кроме того, он начал тщательно исследовать ее внутренности и писать маленькие програмки на ней.

При этом он совершенно забил на свои прямые обязанности (времени не осталось) и чуть не вылетел с работы :(

 

Что касается SST, то в общем и целом писать на ней сложнее, чем на традиционной RTOS (что бы brag не говорил), т.к. простую монолитную задачу придется разрезать на части, и обеспечить их вызов в правильной последовательности. Это крайне легко сделать для чисто последовательных задач (именно такие мы в основном тут и видели), и сложнее, если flow исходной задачи более сложное.

 

Чисто формально для преобразования монолитной задачи в набор задач для SST необходимо проделать такие шаги:

  1. Найти в задаче все точки, в которых производится ожидание внешних событий
  2. Сделать граф, в котором точки из п1 являются вершинами, а control flow пути между ними - дугами
  3. Весь код по дугам оформить в виде отдельных Task'ов
  4. Полученный набор Task'ов может быть запущен под SST

Это все отлично работает для линейного графа (что мы уже видели), хуже работает для произвольного графа в пределах одной функции, и совершенно не работает если есть вызовы функций с ожиданиями внутри и тем паче если такие вызовы динамические. Что бы заставить этот случай заработать, надо преобразовать вызываемые функции так, что бы точки ожидания оказались за их пределами (что мы и видели на примере printf). Это означает координальную переделку всего и вся :rolleyes:

 

Более того, если таких функций много, и граф их вызовов чисто динамический, и если в него еще входят вызовы чего то библиотечного, то такую систему будет проще запустить на класической RTOS, чем на SST.

 

Посмотрите на вышеизложенный алгоритм - количество путей между узлами графа зависит от связанности графа (т.е. от того, из каких точек ожидание в какие мы можем попасть), и в предельном случае (если можно попасть из любой точки в любую) количество дуг будет экспотенциально зависить от количества узлов (связь все ко всем). А само количество узлов может линейно зависить от размера исходной программы. Таким образов при переводе в SST этот кодовый монстр вырастет в экспотенциальной прогрессии :crying:

 

Что касается блокировок, синхронизации и пр, то все не так плохо, как тут было обрисованно. В нормально сделанной системе они должны быть локализованны и быть их должно немного. Если у вас это не так, и синхронизация размазана равномерным слоем по всей программе, то это неправильное проектирование архитектуры системы, а не родовой недостаток RTOS :biggrin:

 

 

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Там, где подобного механизма нет - считаю загрузку вручную через хуки.

Я не любитель загружать проц по максимуму, всегда оставляю резерв на будущее.

 

А теперь посмотрите сколько отладочной информации и инструментов есть у MQX.

И чего вы лишаетесь переходя на доморощенные решения.

Перегрузку процессора я вижу даже когда она намного превышает 100%

 

post-2050-1473947939_thumb.png

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

И чего вы лишаетесь переходя на доморощенные решения.

Это я в курсе. Совместимости с блокинг-кодом тут нет. А инструментов своих хватает. Да и не работаю я под виндой, так что...

 

Перегрузку процессора я вижу даже когда она намного превышает 100%

А это как, был проц на 72мгц а вдруг стал работать на 300? :)

 

Что бы заставить этот случай заработать, надо преобразовать вызываемые функции так, что бы точки ожидания оказались за их пределами (что мы и видели на примере printf).

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

 

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

...

Таким образов при переводе в SST этот кодовый монстр вырастет в экспотенциальной прогрессии crying.gif

Можно пример кода, а то я не понял в чем собственно проблема?

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

:bb-offtopic:

Был у меня в студенческую пору (где то конец 80х)

...

(все это собиралось на Borland C++ 3.1).

...

В принципе было похоже на Turbo Vision (от того же Borland), но на тот момент он еще не вышел в массы :)

 

Ох, что-то с памятью у вас не то... :) Borland C++ 3.1 вышел в 1992 году. И в нём уже был Turbo Vision.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Ох, что-то с памятью у вас не то... :) Borland C++ 3.1 вышел в 1992 году. И в нём уже был Turbo Vision.

Да, а то и позже. Вообще первый реально рабочий Борлондячий 1.01 С++ в 91 году появился. С него начинал сишный путь. Первым опытом была оконая библиотека :). В 80х плюсов у борланда не было вообще.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Да, а то и позже. Вообще первый реально рабочий Борлондячий 1.01 С++ в 91 году появился. С него начинал сишный путь. Первым опытом была оконая библиотека :). В 80х плюсов у борланда не было вообще.
Возможно это был не BC 3.1, а нечто более раннее. Было это в 89-90 годах, и TV в нем точно не было (а С++ был)

 

 

 

Можно пример кода, а то я не понял в чем собственно проблема?
Проблема в том, что это вылезет только на очень болльшом коде, такой пример тут не привести

 

 

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Проблема в том, что это вылезет только на очень болльшом коде, такой пример тут не привести

Так большой и не надо, приведите простой, а мы попытаемся прикинуть на сколько оно страшно будет при большом коде. На NodeJS пишут очень большие и сложные приложения, и ничего вроде как

Ну и еще не стоит забывать, что размер кода для мк довольно ограничен ;)

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Ну хорошо. Что нгибудь вроде такого (code? - куски кода без ожиданий чего либо, wait? - точки ожидания, cond? - какие нибудь условия)

code1();
while(cond1())
{
  code2();
  if (cond2()) {code3(); wait1(); code4();}
  while(cond3()) {code5(); wait2(); code6();}
  if (cond3()) {code7(); wait3(); code8();}
}
code9();

Еще можно такой паттерн:

interface Worker {
virtual void work() =0;
};

void func(Worker* worker)
{
while(cond1())
{
   code1();
   worker->work();
   code2();
}
}

Реализация интерфейса Worker может заблокировать нить исполнения в любом месте внутри метода work()

 

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Гость TSerg
а нечто более раннее. Было это в 89-90 годах, и TV в нем точно не было (а С++ был)

Turbo C 87 г.

Turbo C++ 90 г.

Borland C++ 2.0 90 г.

Borland C++ 3.1 (OWL + TV) 92 г.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

XVR, Большое спасибо за примеры.

 

Ну хорошо. Что нгибудь вроде такого (code? - куски кода без ожиданий чего либо, wait? - точки ожидания, cond? - какие нибудь условия)

code1();
while(cond1())
{
  code2();
  if (cond2()) {code3(); wait1(); code4();}
  while(cond3()) {code5(); wait2(); code6();}
  if (cond3()) {code7(); wait3(); code8();}
}
code9();

Ну этот пример очень абстрактный(теоретический) да и стиль тут чисто(слишком) синхронный, мало того, слишком много неявных зависимостей - побочных эффектов. Например, по коду не видно как зависит cond3 от code5 и code6 и зависит ли вообще.

Современный стиль программирования предполагает избавление от подобных спагетти. Зависимости должны быть явные, побочные эффекты либо вовсе отсутствовать(функциональный стиль), либо их минимизация и приведение в понятный явный вид. Разбиение сложной задачи на более простые итд.

Я понятия не имею, какую конкретную задачу должен решать этот код. Скорее всего, имея конкретную задачу в асинхронном стиле код будет совсем другой, проще и понятнее.

 

Еще можно такой паттерн:

interface Worker {
virtual void work() =0;
};

void func(Worker* worker)
{
while(cond1())
{
   code1();
   worker->work();
   code2();
}
}

Реализация интерфейса Worker может заблокировать нить исполнения в любом месте внутри метода work()

Ну тут аналогично. work должен быть неблокирующий, а зависимость cond1 должна быть определена, а то по коду не понятно, кто влияет на cond1 - code1, code2, worker или вообще кто-то другой, которого мы здесь не видим (какое-нибудь прерывание, например - привет гонки).

Пoдобного рода код очень сложно поддерживать и масштабировать. Он требует глубокого рефакторинга, возможно переделки всей программы с нуля :biggrin:

 

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

Конкретная задача в нон-блокинге решается совсем другим способом, кардинально отличающимся от блокинга.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Правильно, о чем собственно и говорилось. Такой код практически невозможно выразить в SST парадигме (ну или очень сложно). Т.е. объявляем такой код 'неправильным', и 'требующим переделки всей программы с нуля'.

При таком подходе что угодно можно положить на SST, а если оно не ложится - то оно неправильное :)

 

Кстати, то, что SST не требует никаких синхронихаций и блокировок, а так же то, что deadlock'и в нем невозможны, не соотвествует действительности.

 

Синхронизация - это не атрибут реализации потока управления (классические thread'ы или SST), а атрибут разделяемых данных.

 

Если в вашей программе (в SST) нет разделяемых данных, то никакая синхронизация конечно не нужна, а если они есть (например модификация переменной из 2х задач), то она появляется.

И если в thread'ах это решается mutex'ами на обращение к переменной из разных threado'ов, то в SST это решается глобальной блокировкой прерываний в низкоприоритетной задаче на время работы с переменной. Это можно сделать и в thread'ах - завести один mutex на все переменные и захватывать его (полная аналогия функциональности блокировки прерывания ы SST). При таком методе никаких гонок и deadlock'ов в thread'ах не будет, но это очень грубый (я бы даже сказал топорный) метод. Он способен полностью убить производительность любой системы.

 

И deadlock'и и SST возможны. Рассмотрим 2 thread'а (в SST их будет больше), каждая из которых читает по 1 байту из очереди, обрабатывает, и записывает от 0 до 3х байт в другую очередь. Среднее количество записываемых байтов где то 0.5. Задача 1 читает из очереди A и пишет в очередь B. А задача 2 читает из B и пишет в A. (Допустим, что есть еще задача 3 которая иногда пишет что то в обе очереди) Обе очереди имеют ограниченный размер, и если в них нет места для новых данных, то задача поставщик блокируется.

Рассморим ситуацию: в обоих очередях содержится максимально возможное количество данных, и обе задачи хотят прочесть по 1 байту (каждая из своей очереди) и записать по 3. Прлучим deadlock (как в классической thread модели, так и в SST)

 

А то, что в SST нет классических объектов синхронизации, таких как mutex, semaphore, event объясняется не тем, что они не нужны, а тем, что их невозможно представить в классическом виде в модели SST, т.е. програмисту придется их описывать руками (в виде набора задач).

(Хотя event там есть - по сути это постановка задачи в очередь)

 

 

 

Кстати, код в примере совсем не абстрактный/теоритический. Это вполне жизнеспособный код. Он например соответствует обработке какого нибудь пакетного действия с аппаратурой. Например, нам надо передать блок данных в прибор. При этом блок очень большой, и за один раз не умещается. Т.е. его надо нарезать на пакеты, и перед началом (и после окончания) каждого пакета необходимо дождаться готовности от аппаратуры.

 

Первый if - это ожидание готовности, внутренний while - передача блока, 2й if - ожидание готовности после передачи пакета, и внешний while - нарезка всего блока данных на пакеты.

 

 

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

При таком подходе что угодно можно положить на SST, а если оно не ложится - то оно неправильное sm.gif

Любая задача, которая может быть решена на потоках(блокинг-стиль) - может быть решена и на SST(нон-блокинг). Да, код будет совсем другой, и либы нужны свои - асинхронные, с синхронным кодом этот подход не совместим. Но зачастую решение конкретной практической задачи на SST выглядит проще и его проще поддерживать.

 

Точно так же, как, например в функциональное программирование - оно не совместимо с императивным кодом (там нельзя написать i=i+1), но зато на нем можно решать любые задачи, правда код выглядит совсем иначе и императивщику его понять очень трудно - нужно кардинально перестраивать свой мозг. Зато этот код короче, очень легко масштабируется, легко дебажится и имеет кучу других преимуществ.

 

Если в вашей программе (в SST) нет разделяемых данных, то никакая синхронизация конечно не нужна, а если они есть (например модификация переменной из 2х задач), то она появляется.

В современном софте разделяемой памяти быть не должно. Разделяемые должны быть высокоуровневые сущности, и то на худой конец.

Лучше вообще ничего не разделять. Указатель(ссылка) на обьект должен быть один единственный(в один момент времени), а все переменные должны быть const. К этому и стремимся.

 

И если в thread'ах это решается mutex'ами на обращение к переменной из разных threado'ов, то в SST это решается глобальной блокировкой прерываний в низкоприоритетной задаче на время работы с переменной.

....

Он способен полностью убить производительность любой системы.

Это далеко не так. В SST рулят очереди сообщений, а они могут работать без блокировок вовсе(lock-free алгоритмы). Да и если даже и с блокировками(например, когда аппаратная поддержка lock-free слабая или ее нет вовсе), то они очень короткие(на несколько инструкций, обычно до десяти, сама реализация традиционных мютексов и переключение контекста требует гораздо более длинных блокировок) и выполняются строго в недрах движка самой очереди. За пределами очередей блокировок нет. Это и есть то кардинальное отличие блокинга от нон-блокинга,

Для пользователя, тобышь программиста, блокировок нет и быть не может.

 

Обе очереди имеют ограниченный размер, и если в них нет места для новых данных, то задача поставщик блокируется. Рассморим ситуацию: в обоих очередях содержится максимально возможное количество данных, и обе задачи хотят прочесть по 1 байту (каждая из своей очереди) и записать по 3. Прлучим deadlock (как в классической thread модели, так и в SST)

Нет, если нет места, то строчка записи в очередь выкинет исключение или вернет ошибку. Такие ошибки надо обязательно обрабатывать, нормальные высоко-уровневые языки программирования(типа Rust) сами заставляют программиста это делать. Блокировок здесь нет.

 

Да и такого понятия, как чтение из очереди тоже надо опасаться и обходить стороной.

Код должен быть не вида: while((size=read(data)))process(data,size);

а должен быть такого вида: void on_data_available(const Data* data, int size){ process(data, size); }

 

Это совсем другой стиль и мыслить надо здесь иначе.

Но второй код проще - не нужно создавать бесполезный цикл, не нужно выделять место под буфер(data), не нужно проверять ошибки(чтения), не нужно ничего читать, не нужно ждать, не нужно блокироваться.

 

Кстати, код в примере совсем не абстрактный/теоритический. Это вполне жизнеспособный код. Он например соответствует обработке какого нибудь пакетного действия с аппаратурой. Например, нам надо передать блок данных в прибор. При этом блок очень большой, и за один раз не умещается. Т.е. его надо нарезать на пакеты, и перед началом (и после окончания) каждого пакета необходимо дождаться готовности от аппаратуры.

А вот это гораздо ближе к делу. В принципе - типичная задача для embedded, и решение ее должно уже быть готовое в виде некого шаблонного класса, которое нужно просто взять и подключить парой строчек.

Но рассмотрим это решение по-ближе, в неблокирующем стиле ессно. Оно не идеальное, я сам только учусь ;)

Collector collector(&input_queue, Block_size, Max_packet_size); // обьект, который разбивает блоки данных на пакеты.
	//  Реализация может быть разная, зависит от рода задачи, но обычно в нем буфера нет,
	//  просто некая стейт-машина подвязанная под очередь.
	//  событыия прихода данных из очереди он обрабатывает сам, нам не нужно об этом заботится.

// установим наши обработчики событий
collector.on_packet_ready = process_packet; // событие - пакет собран
device.on_ready_to_receive_packet = process_packet; // событие - готовность аппаратуры принимать пакет

// оба события завернуты на 1 обработчик
void process_packet(){
// условие, по сути это проверка состояния очередей, выполняется очень быстро - с десяток инструкций
if(collector.is_packet_ready() && device.is_ready_to_receive_packet()){
	// запускаем отправку пакета
	device.sendPacket(collector.currentPacket(), [](){
		// теперь пакет полностью принят аппаратурой
		// сигнализируем коллектор, что текущий пакет обработан и он нам больше не нужен.
		collector.signal_packet_processed();
	});
}
}

 

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

Device1Queue dev1; // отсюда
Device2Queue dev2;  // пишем сюда

dev1.on_data_ready = [](Data* data, int len){
    dev2.send(data,len, [](){
        dev1.signal_data_processed();
    });
};

 

Побочные эффекты(состояния) хоть и есть, но они сидят глубоко в библиотечных классах. Для пользователя код выглядит хоть и не чисто функциональным, но довольно близким к нему.

 

В теории можно конечно нафантазировать чего хочешь, реализовать которое без блокировок будет практически невозможно. Но необходимость этих блокировок будет вызвана этой самой теоретической задачей :)

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

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

Физический мир сам по себе асинхронный.

Возьмем к примеру простейшую схему - батарейка лампочка и выключатель. Лампочка не ждет пока батарейка будет заряжена или пока нажмут выключатель, это смешно :)

В реальности - лампочка засветится только тогда, когда батарейка заряжена и выключатель включен. Если произойдет событие (сядет батарейка или выключат выключатель) - лампочка погаснет.

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

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

В современном софте разделяемой памяти быть не должно. Разделяемые должны быть высокоуровневые сущности, и то на худой конец.

Лучше вообще ничего не разделять. Указатель(ссылка) на обьект должен быть один единственный(в один момент времени), а все переменные должны быть const. К этому и стремимся.

Можно отсылку на труды классиков? Ну т.е. где бы эта концепция более подробно раскрывалась.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Присоединяйтесь к обсуждению

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

Гость
К сожалению, ваш контент содержит запрещённые слова. Пожалуйста, отредактируйте контент, чтобы удалить выделенные ниже слова.
Ответить в этой теме...

×   Вставлено с форматированием.   Вставить как обычный текст

  Разрешено использовать не более 75 эмодзи.

×   Ваша ссылка была автоматически встроена.   Отображать как обычную ссылку

×   Ваш предыдущий контент был восстановлен.   Очистить редактор

×   Вы не можете вставлять изображения напрямую. Загружайте или вставляйте изображения по ссылке.

×
×
  • Создать...