|
|
  |
Система, управляемая событиями и SST(super-simple tasker), Выделено из "ООП. Классы и динамические объекты" |
|
|
|
Sep 15 2016, 13:29
|
Гуру
     
Группа: Свой
Сообщений: 3 123
Регистрация: 7-04-07
Из: Химки
Пользователь №: 26 847

|
'Энтузизизм' это конечно хорошо, но надо знать меру  Был у меня в студенческую пору (где то конец 80х) научный руководитель (который сам года 2 назад еще был студентом), и написал я (от нечего делать) простой оконный менеджер (a-la Windows, но в текстовом режиме), и даже добавил туда thread'ы (все это собиралось на Borland C++ 3.1). В принципе было похоже на Turbo Vision (от того же Borland), но на тот момент он еще не вышел в массы  Принцип построения этой системы так вдохновил моего 'научного руководителя', что он бегал по всему НИИ и с диким воссторгом и пеной у рта рассказывал всем, какая это крутая штука. Кроме того, он начал тщательно исследовать ее внутренности и писать маленькие програмки на ней. При этом он совершенно забил на свои прямые обязанности (времени не осталось) и чуть не вылетел с работы  Что касается SST, то в общем и целом писать на ней сложнее, чем на традиционной RTOS (что бы brag не говорил), т.к. простую монолитную задачу придется разрезать на части, и обеспечить их вызов в правильной последовательности. Это крайне легко сделать для чисто последовательных задач (именно такие мы в основном тут и видели), и сложнее, если flow исходной задачи более сложное. Чисто формально для преобразования монолитной задачи в набор задач для SST необходимо проделать такие шаги: - Найти в задаче все точки, в которых производится ожидание внешних событий
- Сделать граф, в котором точки из п1 являются вершинами, а control flow пути между ними - дугами
- Весь код по дугам оформить в виде отдельных Task'ов
- Полученный набор Task'ов может быть запущен под SST
Это все отлично работает для линейного графа (что мы уже видели), хуже работает для произвольного графа в пределах одной функции, и совершенно не работает если есть вызовы функций с ожиданиями внутри и тем паче если такие вызовы динамические. Что бы заставить этот случай заработать, надо преобразовать вызываемые функции так, что бы точки ожидания оказались за их пределами (что мы и видели на примере printf). Это означает координальную переделку всего и вся Более того, если таких функций много, и граф их вызовов чисто динамический, и если в него еще входят вызовы чего то библиотечного, то такую систему будет проще запустить на класической RTOS, чем на SST. Посмотрите на вышеизложенный алгоритм - количество путей между узлами графа зависит от связанности графа (т.е. от того, из каких точек ожидание в какие мы можем попасть), и в предельном случае (если можно попасть из любой точки в любую) количество дуг будет экспотенциально зависить от количества узлов (связь все ко всем). А само количество узлов может линейно зависить от размера исходной программы. Таким образов при переводе в SST этот кодовый монстр вырастет в экспотенциальной прогрессии Что касается блокировок, синхронизации и пр, то все не так плохо, как тут было обрисованно. В нормально сделанной системе они должны быть локализованны и быть их должно немного. Если у вас это не так, и синхронизация размазана равномерным слоем по всей программе, то это неправильное проектирование архитектуры системы, а не родовой недостаток RTOS
|
|
|
|
|
Sep 15 2016, 15:12
|
Профессионал
    
Группа: Свой
Сообщений: 1 047
Регистрация: 2-12-06
Из: Kyiv, Ukraine
Пользователь №: 23 046

|
Цитата И чего вы лишаетесь переходя на доморощенные решения. Это я в курсе. Совместимости с блокинг-кодом тут нет. А инструментов своих хватает. Да и не работаю я под виндой, так что... Цитата Перегрузку процессора я вижу даже когда она намного превышает 100% А это как, был проц на 72мгц а вдруг стал работать на 300?  Цитата Что бы заставить этот случай заработать, надо преобразовать вызываемые функции так, что бы точки ожидания оказались за их пределами (что мы и видели на примере printf). Да,от блокировок(ожиданий) надо избавлятся полностью, иначе будут заблокированы все задачи равного или более низкого приоритета. Сложнее или нет - не знаю, мне когда-то было сложно, но после хорошей практики стало так же просто. Цитата Это крайне легко сделать для чисто последовательных задач (именно такие мы в основном тут и видели), и сложнее, если flow исходной задачи более сложное. ... Таким образов при переводе в SST этот кодовый монстр вырастет в экспотенциальной прогрессии crying.gif Можно пример кода, а то я не понял в чем собственно проблема?
|
|
|
|
|
Sep 15 2016, 16:44
|

фанат дивана
     
Группа: Свой
Сообщений: 3 387
Регистрация: 9-08-07
Из: Уфа
Пользователь №: 29 684

|
Цитата(XVR @ Sep 15 2016, 18:29)  Был у меня в студенческую пору (где то конец 80х) ... (все это собиралось на Borland C++ 3.1). ... В принципе было похоже на Turbo Vision (от того же Borland), но на тот момент он еще не вышел в массы  Ох, что-то с памятью у вас не то...  Borland C++ 3.1 вышел в 1992 году. И в нём уже был Turbo Vision.
--------------------
Если бы я знал, что такое электричество...
|
|
|
|
|
Sep 16 2016, 09:06
|
Гуру
     
Группа: Свой
Сообщений: 3 123
Регистрация: 7-04-07
Из: Химки
Пользователь №: 26 847

|
Цитата(zltigo @ Sep 15 2016, 22:56)  Да, а то и позже. Вообще первый реально рабочий Борлондячий 1.01 С++ в 91 году появился. С него начинал сишный путь. Первым опытом была оконая библиотека  . В 80х плюсов у борланда не было вообще. Возможно это был не BC 3.1, а нечто более раннее. Было это в 89-90 годах, и TV в нем точно не было (а С++ был) Цитата(brag @ Sep 15 2016, 18:12)  Можно пример кода, а то я не понял в чем собственно проблема? Проблема в том, что это вылезет только на очень болльшом коде, такой пример тут не привести
|
|
|
|
|
Sep 25 2016, 18:13
|
Гуру
     
Группа: Свой
Сообщений: 3 123
Регистрация: 7-04-07
Из: Химки
Пользователь №: 26 847

|
Ну хорошо. Что нгибудь вроде такого (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()
|
|
|
|
Guest_TSerg_*
|
Sep 25 2016, 18:57
|
Guests

|
Цитата(XVR @ Sep 16 2016, 12:06)  а нечто более раннее. Было это в 89-90 годах, и TV в нем точно не было (а С++ был) Turbo C 87 г. Turbo C++ 90 г. Borland C++ 2.0 90 г. Borland C++ 3.1 (OWL + TV) 92 г.
|
|
|
|
|
Sep 26 2016, 12:30
|
Профессионал
    
Группа: Свой
Сообщений: 1 047
Регистрация: 2-12-06
Из: Kyiv, Ukraine
Пользователь №: 23 046

|
XVR, Большое спасибо за примеры. Цитата(XVR @ Sep 25 2016, 21:13)  Ну хорошо. Что нгибудь вроде такого (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 и зависит ли вообще. Современный стиль программирования предполагает избавление от подобных спагетти. Зависимости должны быть явные, побочные эффекты либо вовсе отсутствовать(функциональный стиль), либо их минимизация и приведение в понятный явный вид. Разбиение сложной задачи на более простые итд. Я понятия не имею, какую конкретную задачу должен решать этот код. Скорее всего, имея конкретную задачу в асинхронном стиле код будет совсем другой, проще и понятнее. Цитата(XVR @ Sep 25 2016, 21:13)  Еще можно такой паттерн: Код 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добного рода код очень сложно поддерживать и масштабировать. Он требует глубокого рефакторинга, возможно переделки всей программы с нуля Асинхронный стиль чем и прикольный, что написание подобного рода кода, даже такого простого, очень геморройно и практически невозможно. Он заставляет сразу писать понятный масштабируемый код. Конкретная задача в нон-блокинге решается совсем другим способом, кардинально отличающимся от блокинга.
|
|
|
|
|
Sep 26 2016, 15:09
|
Гуру
     
Группа: Свой
Сообщений: 3 123
Регистрация: 7-04-07
Из: Химки
Пользователь №: 26 847

|
Правильно, о чем собственно и говорилось. Такой код практически невозможно выразить в 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 - нарезка всего блока данных на пакеты.
|
|
|
|
|
Sep 26 2016, 21:41
|
Профессионал
    
Группа: Свой
Сообщений: 1 047
Регистрация: 2-12-06
Из: Kyiv, Ukraine
Пользователь №: 23 046

|
Цитата При таком подходе что угодно можно положить на 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, и решение ее должно уже быть готовое в виде некого шаблонного класса, которое нужно просто взять и подключить парой строчек. Но рассмотрим это решение по-ближе, в неблокирующем стиле ессно. Оно не идеальное, я сам только учусь  CODE 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(); }); }; Побочные эффекты(состояния) хоть и есть, но они сидят глубоко в библиотечных классах. Для пользователя код выглядит хоть и не чисто функциональным, но довольно близким к нему. В теории можно конечно нафантазировать чего хочешь, реализовать которое без блокировок будет практически невозможно. Но необходимость этих блокировок будет вызвана этой самой теоретической задачей  В реальном мире все иначе - любая практическая задача, а тем более работа с аппаратурой ложится на асинхронщину, как влитая. дальше философия, можно не читать, но для программиста философия - это очень важно, нынче задачи очень сложные и обычным методом влоб они не решаются, нужно выдумывать всякие философские абстракции.Физический мир сам по себе асинхронный. Возьмем к примеру простейшую схему - батарейка лампочка и выключатель. Лампочка не ждет пока батарейка будет заряжена или пока нажмут выключатель, это смешно  В реальности - лампочка засветится только тогда, когда батарейка заряжена и выключатель включен. Если произойдет событие (сядет батарейка или выключат выключатель) - лампочка погаснет. Именно так и должна работать асинхронная программа - на событиях, имеющих свой физический аналог(смысл), а не на бессмысленных(а тем более вечных) циклах.
|
|
|
|
|
  |
2 чел. читают эту тему (гостей: 2, скрытых пользователей: 0)
Пользователей: 0
|
|
|