Помощь - Поиск - Пользователи - Календарь
Полная версия этой страницы: Система, управляемая событиями и SST(super-simple tasker)
Форум разработчиков электроники ELECTRONIX.ru > Сайт и форум > В помощь начинающему > Программирование
Страницы: 1, 2, 3, 4, 5
XVR
Цитата(brag @ Sep 27 2016, 17:51) *
Да, это все огрызки. С ними возимся не из за того, что не можем дорасти до нормального ФП, а из за того, что железо, с которым работаем -императивное и слишком слабое и нормальные ФП-языки на нем запустить пока не удается.

С этим согласен на все 100%. Вот только не уверен, что когда нибудь появится такое железо, где можно будет напрямую ФП запустить sm.gif

Вспоминается история с PicoJAVA. SUN решил, что если исполнение JAVA байт кода сделать напрямую в железе, то такая штука всех порвет. И сделал. А потом выяснилось, что обычный SPARC этот самый байт код успевает интерпретировать быстрее, чем спец процессор исполнять sad.gif
Провал был грандиозный cranky.gif Не все можно эффективно на железо положить smile3046.gif
brag
Та мощный проц запросто под ФП идет, но я работаю в основном на embedded, cortex-ы там всякие, под них erlang хоть и есть, но ресурсов сжирает много, а под avr-образные вообще такого нет.
XVR
Цитата(brag @ Sep 27 2016, 17:51) *
Нет, в sst так писать нелься. Не должен counter меняться в 2х задачах.
Тогда так и надо писать - в SST не нужны никакие примитивы синхронизации потому, что он не умеет работать с такими структурами данных, для которых в thread программах нужны примитивы синхронизации.

Цитата
Пример ситуации можно, когда будет dead-lock?
Почему это он повиснет? Он либо запишет 3 байта, либо не запишет. Как быть в случаи провала записи в очередь решает уже программист, обрабатывая ошибку записи.
Так, расписываю подробно (думал, что и так понятно, но видимо нет)
Код
process(input_queue, output_queue)
{
  input_queue.on_data_avail = function()
  {
    data = input_queue.get();
    var out[3], index=0;
    ...
    output_queue.on_space_avail = function()
    {
      if (index<3) output_queue.push(out[index++]);
      else output_queue.on_space_avail = NULL;
    }
  }
}

queue A, B;
process(A,B);
process(B,A);
A.push(<some data>)
Можем получить deadlock

Цитата
Это да. но хотелось бы реальную задачу, где именно сохранение контекста необходимо или сильно облегчало бы жизнь.
Возьмите любую MT задачу размером более 10000 строк С/С++
brag
Цитата
Возьмите любую MT задачу размером более 10000 строк С/С++

И переписать на SST? Так я таким и занимался, вышло отлично.

Цитата
Тогда так и надо писать - в SST не нужны никакие примитивы синхронизации потому, что он не умеет работать с такими структурами данных, для которых в thread программах нужны примитивы синхронизации.

Этих примитив в современном коде нужно избегать, хотите верьте - хотите нет, но в каждой современной книге об этом написано и расписано почему.
Да, SST не умеет работать с такими примитивами, он заставляет программиста обходится без них, на то он и нужен wink.gif

Цитата
Можем получить deadlock

Код
process(input_queue, output_queue)
{
  input_queue.on_data_avail = function() // ок, установили обработчик на очередь B
  {
    data = input_queue.get(); // получили указатель на данные B. лучше бы он передавался в обработчик автоматически, ну да ладно.
    var out[3], index=0;
    ...
    output_queue.on_space_avail = function() // а так не делается.
// on_space_avail срабатывает один раз когда очередь переходит из состояния полностью заполненной к состоянию не полностью заполненной.
// этот код требует рефакторинга
// а если это событие будет возникать всякий раз, когда очередь не пуста, то получим 100%ную бесполезную загрузку процессора, так тоже делать нельзя.
    {
      if (index<3) output_queue.push(out[index++]); // тут недо было бы проверить на наличие ошибки, поскольку очередь A - конкурентная, кто-то другой мог уже туда что-то записать и там может не оказаться места.
      else output_queue.on_space_avail = NULL; // удалили обработчик - будет пропуск события. хорошо это или плохо - зависит от программиста
    }
  }
}

queue A, B;
process(A,B); // это бессмысленная строчка, тк input_queue.on_data_avail будет тут же переопределен следующей строкой. в данном примере эта строка не делает ничего
process(B,A); // а тут можно уже заглянуть по-глубже в код ф-ции process, там комменты для этой сторчки
A.push(<some data>) // конкурентная запись в очередь A(process -> output_queue) - норм, наши очереди это поддерживают.

Ну этот код вообще по моему не корректен для SST. трудно по нему сказать, как он должен был бы работать по задумке его автора.
Но deadlock-а здесь нет, хоть и будут пропуски событий из за неправильно построенного кода, но события будут и дальше приходить и их можно дальше обрабатывать.
XVR
Цитата(brag @ Sep 27 2016, 18:57) *
Этих примитив в современном коде нужно избегать, хотите верьте - хотите нет, но в каждой современной книге об этом написано и расписано почему.
Да, SST не умеет работать с такими примитивами, он заставляет программиста обходится без них, на то он и нужен wink.gif
Т.е. SST это такая смирительная рубашка, которая ограничивает возможности програмиста?

Цитата
Ну этот код вообще по моему не корректен для SST. трудно по нему сказать, как он должен был бы работать по задумке его автора.
Но deadlock-а здесь нет, хоть и будут пропуски событий из за неправильно построенного кода, но события будут и дальше приходить и их можно дальше обрабатывать.
То, что вы не видите потенциального deadlock'а, не значит, что его нет. В этом и проблема.
Deadlock будет при заполненных очередях. Когда в событии on_space_avail каждый процесс будет пытаться положить данные в очередь. Т.к. места в очереди нет (после 1го срабатывания on_space_avail) оба процесса будут ожидать пока освободится место для 2го и 3го байта. Но это место никогда не освободится, т.к. только эти самые процессы могут освободить место, читая из очередей. А они не могут, т.к. они не доработали до конца - не все данные помещены в очереди. Deadlock

brag
Цитата
Т.е. SST это такая смирительная рубашка, которая ограничивает возможности програмиста?

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

Цитата
То, что вы не видите потенциального deadlock'а, не значит, что его нет. В этом и проблема.
Deadlock будет при заполненных очередях. Когда в событии on_space_avail каждый процесс будет пытаться положить данные в очередь. Т.к. места в очереди нет (после 1го срабатывания on_space_avail) оба процесса будут ожидать пока освободится место для 2го и 3го байта. Но это место никогда не освободится, т.к. только эти самые процессы могут освободить место, читая из очередей. А они не могут, т.к. они не доработали до конца - не все данные помещены в очереди. Deadlock

Так процесс всего один - process(B,А ) , и то какой-то странный. Первый(process(A, B ) ) не делает ничего, его можно смело закомментировать, как по мне.
Какую задачу должен решать этот код? Я попытаюсь сделать правильное решение.
deadlock можно и в одном потоке получить sm.gif только какой в этом смысл?
XVR
Цитата(brag @ Sep 27 2016, 19:26) *
Равно, как и любой высокоуровневый язык.
Ага, но такое ограничение - это черезчур sm.gif

Цитата
Так процесс всего один - process(B,А ) , и то какой-то странный. Первый(process(A, B ) ) не делает ничего, его можно смело закомментировать, как по мне.
Почему это? Работают оба процесса. Попробую написать в виде thread кода, в SST переведете сами
Код
void process(queue& inp, queue& out)
{
  for(;;)
  {
   char data = inp.get(); // Can block until data is available
   .... // calculate data to send
   char a1, a2, a3;
   out.put(a1); // Can block until space is available
   out.put(a2); // Can block until space is available
   out.put(a3); // Can block until space is available
  }
}

queue A, B;

run_task( [] (process(A,B);} );
run_task( [] (process(B,A);} );
A.put(1);

brag
Ну такое переливание делается как-то так. deadlock-a нет, есть ошибка записи, которую нужно обработать.
Код
void process(queue& inp, queue& out){
    inp.on_ready_read = [](const Data* data){
        // process the data
        // ....
        Data data2(...);
        // ....
        if(!out.push(data2, [](){
            inp.signal_data_processed();
        })){
            throw queue_full_error("Queue full");
            // or process an error in another way
            // resolve signal_data_processed on inp
        }
    };
}

queue A, B;

process(A,B);
process(B,A);
A.push(1);

Если этого не сделать, то сообщения перестанут обрабатываться, но эту ситуацию можно исправить на горячую, в отличии от dead-locka на мютексах.
XVR
Цитата
// or process an error in another way
// resolve signal_data_processed on inp
Именно, что 'another way'. Нужно поставить событие на освобождение места в out и в нем пытаться записать (именно это делает оригинальный код)

Цитата(brag @ Sep 27 2016, 22:40) *
Если этого не сделать, то сообщения перестанут обрабатываться,
Это и есть deadlock rolleyes.gif
Цитата
но эту ситуацию можно исправить на горячую, в отличии от dead-locka на мютексах.
deadlock на мьютексах тоже можно разбить на горячую, проблема в том, что система уже находится в некорректном состоянии (что в этом примере, что в мьютексах). Для избежания deadlock'ов нужно перепроектировать систему, а не ломать их 'на горячую'. И это относится как к обычным thread'ам, так и к SST

Так что SST от deadlock'ов не спасает rolleyes.gif

NB. Уточнение во избежание недопонимания - dedlock это не только взаимная блокировка мьютексов, но и любая взяимная блокировка на каких то ресурсах. От обычной блокировки (например, если пытаться писать в очередь из которой никто не читает) отличается тем, что возникает очень спорадически и невоспроизводим.
NNB. Еще уточнение - 'спасает от dedlockов' обозначает принципиальную невозможность возникновения dedlock'ов в любом случае, а не их отсутвие при правильно написанной программе.

brag
Цитата
Именно, что 'another way'. Нужно поставить событие на освобождение места в out и в нем пытаться записать (именно это делает оригинальный код)

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

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

Цитата
Еще уточнение - 'спасает от dedlockов' обозначает принципиальную невозможность возникновения dedlock'ов в любом случае, а не их отсутвие при правильно написанной программе.

Ну такой deadlock(когда система перестает обрабатывать события) можно получить и в одном потоке и даже в функциональном программировании.
XVR
Цитата(brag @ Sep 28 2016, 10:36) *
При проектировании SST - заведомо достаточно места в очереди. То есть избегать их переполнения.
Если уж переполнение произошло - нужно сделать восстановление - обычно это потеря данных, запись этого события в лог, и повторная их передача либо просто skip.
А это уже означает перепроектирование программы. Оригинальная программа даст deadlock. Т.е. сам по себе SST не в состоянии его предотвратить.
Что и означает, что заявление 'в SST никаких dedlockов быть не может' несколько не соотвествует действительности, не так ли?

Цитата
Ну такой deadlock(когда система перестает обрабатывать события) можно получить и в одном потоке и даже в функциональном программировании.
deadlock должен быть спорадическим, иначе это просто обычная бага в программе. Добится спорадического (и невоспроизводимого) поведения в одном потоке невозможно.
sigmaN
Я думал любой deadlock есть результат бага в программе.
brag
Цитата
Оригинальная программа даст deadlock. Т.е. сам по себе SST не в состоянии его предотвратить.

Оригинальная программа на SST даст исключение.

Цитата
Что и означает, что заявление 'в SST никаких dedlockов быть не может' несколько не соотвествует действительности, не так ли?

Смотря что подразумевать под deadlock-ом.

Цитата
deadlock должен быть спорадическим, иначе это просто обычная бага в программе. Добится спорадического (и невоспроизводимого) поведения в одном потоке невозможно.

Смотря что подразумевать под потоком sm.gif
Если параллельное исполнение кода (тру поток) - тогда можно добиться и в одном потоке. Если поток классической ОС (часто его называют Thread-ом) - тогда тоже можно и в одном потоке получить такой трудновоспроизводимый lock.

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

Любое прерывание, любую модифицируемую извне память(например IO-регистры) уже можно назвать отдельным потоком. Каждый IO-регистр - это уже отдельный маленький поток, эти регистры могут изменятся параллельно, независимо друг от друга и от основного кода программы.
XVR
Цитата(brag @ Sep 28 2016, 12:00) *
Оригинальная программа на SST даст исключение.
Значит это не оригинальная программа. Она (оригинальная, на thread'ах) будет ждать освобождения очереди, и никаких исключений. Более того, если SST программа будет давать исключение на любое переполнение очереди, то это еще хуже, чем deadlock - она вылетит по исключению тогда, когда оригинальная будет продолжать работать (т.к. для deadlock'а нужно переполнение обоих очередей, а в SST версии чтобы умереть хватит переполнения и одной очереди)

Цитата
Смотря что подразумевать под deadlock-ом.
Я уже давал определение, давайте послушаем ваше.

Цитата
Смотря что подразумевать под потоком sm.gif
Один поток исполнения без всякой параллельности, типа прерываний и пр.
Цитата
Его невозможно получить только в тру-одном потоке, то есть там, где кроме инструкций процессора нет больше ничего - никаких прерываний, никаких других внешних устройств, то есть там, где все доступное адресное пространство может быть модифицировано только кодом и никем больше.
Именно такое
Цитата
Но такая система является бесполезной на практике, она кроме выполнения бесполезных вычислений больше ничего не может.
Это почему не может? А работа по поллингу вместо прерываний? Или если у процессора выключить прерывания, то он сразу превратится в кирпич?
Цитата(sigmaN @ Sep 28 2016, 11:23) *
Я думал любой deadlock есть результат бага в программе.

Да, но не любая бага приводит к deadlock'у
brag
Цитата
Это почему не может? А работа по поллингу вместо прерываний? Или если у процессора выключить прерывания, то он сразу превратится в кирпич?

если есть хоть один аппаратный регистр, значение которого меняет не проц(или не только проц), значит это уже 2 потока, не важно поллинг это или прерывание.
С мютексами тоже можно работать по поллингу и тоже можно получить deadlock sm.gif
Ну или кривая FSM(тоже по сути polling) тоже может перестать обрабатывать сообщения - при стечении определенных обстоятельств.

Цитата
Более того, если SST программа будет давать исключение на любое переполнение очереди, то это еще хуже, чем deadlock - она вылетит по исключению

Никуда она не вылетит, исключение можно подловить и восстановиться или обработать ошибку сразу без использования исключения. При deadlock-e на мютексах/блокировках такое сделать не получится, разве что ради этого есть поддержка в ОС.
Редко когда переполнение очереди - это нормальная работа, обычно это исключительные ситуации(по сути баги), которые обрабатываются отдельно и записываются в лог для дальнейшей правки. Да и юнит-тестирование быстро такие места(переполнение очередей) выявляет, в отличии от блокирующей модели, где просто так тест написать очень трудно, по сути надо писать свой планировщик, который будет перебирать всевозможные варианты переключения контекста, аналогичный планировщик я делал для быстрого выявления гонок.
XVR
Цитата(brag @ Sep 28 2016, 16:24) *
если есть хоть один аппаратный регистр, значение которого меняет не проц(или не только проц), значит это уже 2 потока, не важно поллинг это или прерывание.
Это абсолютно не такю Чтение этого регистра происходит в четко определенные моменты времени, а это значит, что аппаратный поток (который меняет значение регистра) жестко засинхронизирован с основным потоком. Основной поток может увидеть разные значения регистра в разных точках, но прочтенное значение не может самопроизвольно измениться между чтениями.
Цитата
С мютексами тоже можно работать по поллингу и тоже можно получить deadlock sm.gif
Очень хочу посмотреть на 'мьютексы на поллинге' rolleyes.gif
Цитата
Никуда она не вылетит, исключение можно подловить и восстановиться или обработать ошибку сразу без использования исключения.
В вашей реализации она бросит исключение тогда, когда никаких ошибок нет, и когда никаких исключений быть не должно вообще! И никаких deadlock'ов в этот момент тоже нет
Цитата
При deadlock-e на мютексах/блокировках такое сделать не получится, разве что ради этого есть поддержка в ОС.
В некоторых есть
Цитата
Редко когда переполнение очереди - это нормальная работа, обычно это исключительные ситуации(по сути баги), которые обрабатываются отдельно и записываются в лог для дальнейшей правки.
Вы опять подменяете оригинальную задачу на свои пожелания. В оригинальной задаче полное заполнение одной очереди возможно, и не является исключительной ситуацией или ошибкой.
Цитата
Да и юнит-тестирование быстро такие места(переполнение очередей) выявляет,
В оригинальной задаче никакое юнит тестирование это не выявит. Более того, никакие размеры очередей (кроме бесконечных) не дадут гарантии отсуствия dedalock'ов. И это все остается в силе и при реализации на SST (но не вашей, а оригинальной. Вы реализовали другую задачу)
Цитата
в отличии от блокирующей модели, где просто так тест написать очень трудно, по сути надо писать свой планировщик, который будет перебирать всевозможные варианты переключения контекста, аналогичный планировщик я делал для быстрого выявления гонок.
Для таких анализов существуют специальные тулы. А спец планировщик этот deadlock не обнаружит, он зависит не от переключения контекстов а от содержимого очередей и алгоритмов внутри процессов 'перелива' данных между очередями.
brag
Цитата
Это абсолютно не такю Чтение этого регистра происходит в четко определенные моменты времени, а это значит, что аппаратный поток (который меняет значение регистра) жестко засинхронизирован с основным потоком. Основной поток может увидеть разные значения регистра в разных точках, но прочтенное значение не может самопроизвольно измениться между чтениями.

То же самое происходит и при работе с очередями sm.gif

Цитата
Очень хочу посмотреть на 'мьютексы на поллинге' rolleyes.gif

Та что там смотреть, классика по сути
Код
struct Mutex{
  void lock(){
    while(true){ // poll variable 'm'
      disable_irq();
      if(!m){
        m = true;
        enable_irq();
        return;
      }
      enable_irq();
      // we can do something else here, run FSM for example
    }
  }

  void unlock(){ m = false; }
  
  Mutex(): m(false){}

private:
    bool m;
}

Такой мютекс можно применять и без RTOS(а можно и с), например в классической FSM, я когда-то так делал, пока не пришел к SST-модели.

Что такое потоки в однопроцессорной системе? Это те же самые FSM с асинхронными прерываниями. Даже без асинхронных прерываний на FSM можно получить эффект deadlock-a, лок будет зависеть не от кода, а от стечения обстоятельств, точно так же, как это происходит в OS-thread-ах. В тредах лок зависит от хода переключения контекста(тоже по сути от событий-прерываний), в примере от XVR - от состояния очередей, в обычной FSM - от ее состояния и внешних или внутренних событий(прерываний, таймеров, регистров итп).

Я привык оперировать не синтетическими задачами, а реальными. В моем понимании классический deadlock это что-то типа этого:
Код
class Account {
  double balance;

  void withdraw(double amount){
     balance -= amount;
  }

  void deposit(double amount){
     balance += amount;
  }

   void transfer(Account from, Account to, double amount){
        sync(from);
        sync(to);
           from.withdraw(amount);
           to.deposit(amount);
        release(to);
        release(from);
    }
}

Задача решена как-бы правильно и бага или не/недо-проверки ошибок здесь как бы нет. Но..
Реальная ситуация: клиент А отправляет клиенту B 100 рублей, в этот же момент клиент В отправляет клиенту А 50 рублей - и все, это может привести к deadlock-у. Я такие ситуации не раз ловил, очень трудно найти и исправить, особенно, если код гораздо сложнее.

Хотелось бы увидеть реальный(а не синтетический) пример на неблокирующих очередях, который привел бы к подобной ситуации.
XVR
Цитата(brag @ Sep 28 2016, 19:52) *
То же самое происходит и при работе с очередями sm.gif
Если у вас прерывание может запустить более приоритетную задачу из очереди немедленно (не дожидаясь какого либо вызова SST интерфейса), то это уже параллельность. И это отличается от рассматриваемого случая. А если не может, то зачем такой SST нужен?

Цитата
Та что там смотреть, классика по сути
Это называется Spin Lock, и он не является 'Мьютексом на поллинге' sm.gif (Точнее это не тот 'поллинг', который применялся в вышеописанном случае)

Цитата
Что такое потоки в однопроцессорной системе? Это те же самые FSM с асинхронными прерываниями. Даже без асинхронных прерываний на FSM можно получить эффект deadlock-a, лок будет зависеть не от кода, а от стечения обстоятельств, точно так же, как это происходит в OS-thread-ах. В тредах лок зависит от хода переключения контекста(тоже по сути от событий-прерываний), в примере от XVR - от состояния очередей, в обычной FSM - от ее состояния и внешних или внутренних событий(прерываний, таймеров, регистров итп).
Ага. И в SST тоже самое. Что мой пример и показал.

В SST можно получить dedlock и в классическом понимании, но для этого там придется сначала реализовать как минимум мьютексы (которых там нет не потому что они не нужны, а потому что SST предоставляет другой примитив синхронизации, хотя и более ограниченный. А мьютекс можно сделать, но это будет 'закат солнца вручную')

Цитата
Я привык оперировать не синтетическими задачами, а реальными. В моем понимании классический deadlock это что-то типа этого:
Это один из вариантов dedlock'ов. Он не единственный. Кстати, в данном примере dedlock'а не будет sm.gif

Цитата
Хотелось бы увидеть реальный(а не синтетический) пример на неблокирующих очередях, который привел бы к подобной ситуации.
Именно такую ситуацию сделать будет сложно. сначала придется сделать мьютекс в SST (это можно сделать), потом захватьт пару мьютексов в разном порядке в разных задачах.
Вариант с очередями (не очередями самого SST, а пользовательскими, как в примере) гораздо проще. И он далеко не синтетический. Например, представьте, что вы делаете real time систему обработки звука, и в очередях у вас хранятся отсчеты. Вы хотите получить эффект эха с каким нибудь дополнительным эффектом (например изменение тембра и темпа голоса). В этом случае ваши 2 процесса - это эффект эха и обработка тембра/скорости. 1 процесс (эхо) не будет размножать данные, а вот второй будет. И какие ниюудь особенно грамкие вопли могут повесить такую систему sm.gif

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

brag
Цитата
Я понимаю, что эхо с изменением тембра можно сделать без такой изощренной схемы

Вот это главное - SST заставляет делать вещи просто sm.gif там, где нужны были куча потоков, в SST это один поток и зачастую даже сам SST не нужен
SST нужен для приоритизации задач, например когда есть риал-тайм звук и что-то тяжелое, типа декодировки jpeg. Надо чтобы jpeg не приводил к замиранию звука, тут и помогают приоритеты SST. Для обычных прикладных задач обычно хватает 1-3 приоритета.

Цитата
Кстати, в данном примере dedlock'а не будет sm.gif

Будет:
Код
transfer A->B:
  sync(A);  // from
  // context switch (on time quantum for example)

transfer B->A:
  sync(B); // from
  // context switch

transfer A->B: // continue
  sync(B); // to.  DeadLock !
  // context switch

transfer B->A: // continue
  sync(A); // to. Strong DeadLock finally

Редко возникающий, тяжело находимый deadlock.

Ну и контр-пример на SST, вернее без SST, один поток, async IO:
Код
   void transfer(Account from, Account to, double amount){
        from.withdraw(amount);
        to.deposit(amount);
    }

Поскольку все Account-ы равноприоритетные, никакой синхронизации не требуется вовсе.

Синхронизация на очередях избавляет от очень капризных и тяжело находимых багов - гонок при переключении контекста/прерываниях. Я делал свой планировщик для перебора всевозможных комбинаций переключения контекста, типа Relacy http://www.1024cores.net/home/relacy-race-detector . Последний не использую потому что он не смог найти даже такую простую гонку: https://github.com/dvyukov/relacy/issues/3
Так вот этим своим планировщиком я понаходил немало гонок в чужом(да и своем тоже) продакшн-коде, при чем эти гонки ни разу не вылезли за несколько лет использования этого кода.
Это я к тому, что синхронизация на мютексах(и семафорах) требует очень больших временных и мозговых затрат. При работе с очередями все гораздо проще.
brag
При синхронизации на мютексах не столько страшен deadlock, сколько гонки. Наглядный пример я уже приводил https://github.com/dvyukov/relacy/issues/3 - хоть и ff и nexec защищены (тк они atomic), да и по ходу исполнения на них по отдельности гонок нет, но есть так называемая гонка высшего порядка, то есть 2 ресурса защищены по отдельности, но не защищены в совокупности и результат работы программы будет зависеть не от программиста, а от варианта хода переключения контекста.
Чтобы гарантировано(и то не факт) избавится от таких гонок нужно все разделяемые ресурсы покрывать одним глобальным мютексом, но такая система будет крайне тормознутая и никому не нужная.
При общении обьектов через очереди сообщений таких проблем нет.

При синхронизации на мютексах, кроме гонок, есть еще одна важная проблема - resource starvation. Это когда много потоков интенсивно используют один и тот же ресурс. Но кому-то из потоков удается его получать, а кто-то так и не дожидается его освобождения.
По аналогии с лифтом - лифт ездит между первым и 4м этажом, тк там очень много народу, а один человек на 9м этаже так и не смог дождаться лифта.
Это вторая проблема после гонок.
С такой проблемой я тоже очень часто стыкался. Чаще всего это когда на шине SPI висит одно медленное устройство(типа термометра), а второе быстрое(скажем АЦП), занимающее 95% SPI. Потоки, работающие с этим быстрым устройством хаотично делятся мютексом шины, а потоку, работающему с термометром мютекс так и не достается. Хуже, если на шине не 2 устройства, а штук 5 разных.
При работе с SPI через очереди такой проблемы нет - все получают доступ к шине строго в порядке очереди.
XVR
Цитата(brag @ Sep 29 2016, 00:08) *
Вот это главное - SST заставляет делать вещи просто sm.gif

Вот именно - заставляет. Это механизм принудительного выкручивания рук у програмиста.

Цитата
Будет:
Код
transfer A->B:
  sync(A);  // from
  // context switch (on time quantum for example)

transfer B->A:
  sync(B); // from
  // context switch

В transfer B->A первым выполняется sync(A). Так что dedlock'а не будет sm.gif

Цитата
Это я к тому, что синхронизация на мютексах(и семафорах) требует очень больших временных и мозговых затрат. При работе с очередями все гораздо проще.

С этим никто не спорит. Синхронизация на очереди (она же сериализация, она же mailbox) является примитивом синхронизации более высокого уровня, чем голые мьютексы, семафоры и критические секции. Но за это преимущество приходится платить эффективностью. А в обычных (thread based) системах и способом использования - очередь требует event driven стиля (как в SST). Так что тут SST рулит однозначно.

К сожалению сам SST требует этот самый event driven способ построения программы, и принципиально не может использовать обычную flow модель. Для того, что бы безболезненно использовать очереди, это слишком высокая цена rolleyes.gif

Я вижу одно неоспоримое преимущество SST - скромные требования к памяти (стеку).

Отсюда следует, что ниша чистого SST - это мелкие МК (но не слишком мелкие, т.к. на совсем маленьких МК не может быть очень сложных программ, и обычный Super Loop позволит сделать то же самое, что и SST, но без какого либо управляющего кода/tasker'а вообще)

Еще вижу, что можно использовать комбинированный подход - SST поверх обычных thread'ов. Эта комбинация мне кажется супер удобной. То, что просто ложится на SST будет запущено на нем, а все остальное - обычная программа. Например, если надо в процессе какой то сложной обработки (параллельной и запутанной) выдать результат в виде нескольких байтов в SPI железку, то запуск цепочки вывода в SST части будет очень простым и логичным действием. Сама цепочка вывода тоже будет достаточно простой и логичной.
Если же это делать порождением отдельного thread'а, то с одной стороны это будет явный overkill, а с другой это может оказаться менее прозрачным (в коде), чем запуск SST цепочки.

PS. Кстати, ваша реализация SST умеет запускать приоритетные задачи поверх менее приоритетных по аппаратному прерыванию? Если да, то как это реализованно?



Цитата(brag @ Sep 29 2016, 15:31) *
При синхронизации на мютексах не столько страшен deadlock, сколько гонки. Наглядный пример я уже приводил - хоть и ff и nexec защищены (тк они atomic), да и по ходу исполнения на них по отдельности гонок нет, но есть так называемая гонка высшего порядка, то есть 2 ресурса защищены по отдельности, но не защищены в совокупности и результат работы программы будет зависеть не от программиста, а от варианта хода переключения контекста.


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

Код
            if(ff($)==0){
                ff($)++;


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

Цитата
Чтобы гарантировано(и то не факт) избавится от таких гонок нужно все разделяемые ресурсы покрывать одним глобальным мютексом,


Ни в коем случае. В данном случае нужно взять всю работу с ff и nexec в критическую секцию (это самое простое решение).

Цитата
При общении обьектов через очереди сообщений таких проблем нет.


Разумеется, если програмист не умеет работать с разделяемыми ресурсами с помощью низкоуровневых примитивов синхронизации, ему лучше поручить это кому нибудь другому (очереди например). Или вообще завязать с програмированием и идти продавать пирожки smile3046.gif

Цитата
При синхронизации на мютексах, кроме гонок, есть еще одна важная проблема - resource starvation. Это когда много потоков интенсивно используют один и тот же ресурс.


Это проблема, но не проблема мьютексов, а проблема честного планирования доступа к ресурсу.

Цитата
При работе с SPI через очереди такой проблемы нет - все получают доступ к шине строго в порядке очереди.


Да, очередь это один из возможных вариантов такого разделения. Но она тоже не покрывает всех возможных вариантов.
Например - у вас на SPI шине висит 1000 контролеров лампочек (сами лампочки например на пульте управления АЭС), и мотор, управляющий положением графитовых стержней в самом ядерном реакторе. Ваша программа управления определила, что с реактором все Ок, и выдала команду зажечь 500 зеленых лампочек, показывающих, что все Ок. В это время пролетающий мимо протон попал в урановый стержень в реакторе, и цепная реакция пошла в разнос. Программа это определила, и выдала экстренную команду мотору опустить стержни-поглотители. Но вот беда, в очереди SPI уже стоит 500 команд на зажигание лампочек, и пока дойдет очередь до мотора, опускать уже будет нечего - реактор расплавится crying.gif

И в принципе такая задача (об разделении доступа к ресурсу, а не об печальной участи АЭС) очень похожа на задачу планирования task switch в современных ОС, и для ее решения применяют как чистую очередь (round robin алгоритм переключения), так и гораздо более изощренные алгоритмы (с приоритетми, классами приоритетов, priority boost'ами, наследованием приоритетов и пр)
brag
Цитата
Вот именно - заставляет. Это механизм принудительного выкручивания рук у програмиста.

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

Цитата
transfer B->A первым выполняется sync(A). Так что dedlock'а не будет sm.gif

С чего бы это? Первым идет лок from, затем to.
transfer A->B : from=A, to=B
transfer B->A : from=B, to=A
И все, deadlock (раз в год).

Цитата
К сожалению сам SST требует этот самый event driven способ построения программы, и принципиально не может использовать обычную flow модель. Для того, что бы безболезненно использовать очереди, это слишком высокая цена rolleyes.gif

Не спорю, цена высокая и платить нужно временем привыкания к event-driven-модели, ну и несовместимостью с flow-model кодом.

Код
PS. Кстати, ваша реализация SST умеет запускать приоритетные задачи поверх менее приоритетных по аппаратному прерыванию? Если да, то как это реализованно?

Конечно. Без этого это уже не SST, а обычный одно-приоритетный код (типа NodeJS). Реализовано по разному на разных платформах. На Cortex через связку PendSV/SVC (где-то в этой ветке приводил ассемблерный код).
На других камнях через вложенные прерывания - при входе в обработчик разрешаются другие прерывания, а при выходе - запускается планировщик, который достает из очереди самую приоритетную задачу, приоритет которой выше текущей и выполняет ее (если такая имеется). В принципе так же, как в оригинальной статье по SST http://www.embedded.com/design/prototyping...r-Simple-Tasker

Цитата
будет работать не правильно. Нельзя сделать атомарный тест а потом атомарный инкремент и считать, что это все будет по прежнему атомарным

Этот код я специально оставил без защиты, чтобы relacy хоть там нашел ошибку, но он и на это оказался не способен(или я не сумел его научить).
Я акцентировал внимание на двух переменных. Этот код(if в оригинале был защищен) из реальной практической задачи, где я спустя месяца 2 поймал гонку. А хуже того, если переменных/ресурсов не 2, а больше. Работать с таким вообще невозможно(годы уйдут на отладку) выход один - покрывать все одним локом, но это тормоз жуткий.

Цитата
Ни в коем случае. В данном случае нужно взять всю работу с ff и nexec в критическую секцию (это самое простое решение).

Ну так критическая секция(запрет прерываний) - это и есть глобальнейший мютекс. А что, если эти ff и exec не просто переменные, а сложные обьекты, хуже - работа с аппаратурой? Сами они защищены по отдельности, но не защищены в совокупности.

Цитата
Разумеется, если програмист не умеет работать с разделяемыми ресурсами с помощью низкоуровневых примитивов синхронизации, ему лучше поручить это кому нибудь другому (очереди например). Или вообще завязать с програмированием и идти продавать пирожки smile3046.gif

А что если программист не умеет программировать в машинных кодах, вернее умеет, но это у него занимает очень много времени? Переходить на высокоуровневый язык или идти продавать пирожки?
Почти любая сложная программа на мютексах имеет гонки, при чем такие запутанные, что ручным анализом кода их выявить невозможно. Да и инструментов не так много(можно сказать их нет), и сама симуляция сложной программы(перебор всевозможных вариантов переключения контекста) может занимать месяц времени на 4-ядерном 3ггц Core-i7 процессоре. Да и то это еще надо уметь описать assert-ы для симулятора, это тоже очень не просто.
Поэтому и используют сейчас очереди(каналы, пайпы итд, называйте как хотите) взамен мютексов. И код стараются делать простым, без запутанных связей. Да и работа с очередями тоже делается не как вздумается, а по определенным шаблонам проектирования.

Цитата
Но вот беда, в очереди SPI уже стоит 500 команд на зажигание лампочек, и пока дойдет очередь до мотора, опускать уже будет нечего - реактор расплавится crying.gif

Для этого есть очередь с приоритетами - сообщения в ней сортируются сначала в порядке приоритета, затем в порядке поступления. Реализация не сильно сложная, делается один раз и работает быстро.
Именно SST решает эту проблему без изощренных алгоритмов планировки задач ОС.
В то время как сериализация на мютексах рано или поздно приведет к взрыву реактора - 500 лампочек начнут хватать мютекс хаотично, а времени для движка не останется, и наследование приоритетов не поможет. sm.gif

Имхо, в нынешнее время единственное применение мютексам - защита данных внутри самой очереди, и то на системах, где нет другого механизма sm.gif
brag
Цитата
Отсюда следует, что ниша чистого SST - это мелкие МК (но не слишком мелкие, т.к. на совсем маленьких МК не может быть очень сложных программ, и обычный Super Loop позволит сделать то же самое, что и SST, но без какого либо управляющего кода/tasker'а вообще)

Как по мне, SST подходит под контроллеры от уровня Atmega8 до STM32F407, а так же практически на все DSP. На что-то более крупное нужно что-то стандартное(например linux)
Реализация SST настолько проста, что даже если у вас 1 приоритет(как в NodeJS/Javascript), то выкидывать его нет смысла - всегда работаешь с одним инструментом.

Цитата
Еще вижу, что можно использовать комбинированный подход - SST поверх обычных thread'ов. Эта комбинация мне кажется супер удобной. То, что просто ложится на SST будет запущено на нем, а все остальное - обычная программа. Например, если надо в процессе какой то сложной обработки (параллельной и запутанной) выдать результат в виде нескольких байтов в SPI железку, то запуск цепочки вывода в SST части будет очень простым и логичным действием. Сама цепочка вывода тоже будет достаточно простой и логичной.
Если же это делать порождением отдельного thread'а, то с одной стороны это будет явный overkill, а с другой это может оказаться менее прозрачным (в коде), чем запуск SST цепочки.

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

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

Я думаю, если использовать смесь блокинг(тот же обычный printf) и нон-блокинг кода, то нужно это делать в классических тредах, возможно через active object. Но ни в коем случаи не использовать мютексы, а использовать высокоуровневые обьекты, а мютексы пусть используют разработчики этих обьектов внутри их.

Конечно, на первый взгляд в SST неудобно работать, если нужно последовательно выполнять несколько асинхронных действий
Код
void Client::do_socket_connect(){
    settings->readSetting(&settings_union.host, [this](){
        settings->readSetting(&settings_union.port, [this](){
            socket->connect(settings_union.host.data, settings_union.port.data);
        });
    });
}

Нужно заботится о том, где и как хранить обьекты host и port, немного некрасивый вид(callback hell назревает).

В блокинг модели это бы выглядело так:
Код
void Client::do_socket_connect(){
    Settings::Item<Settings::ID_Host> host;        
    settings->readSetting(&settings_union.host);

    Settings::Item<Settings::ID_Port> port;
    settings->readSetting(&port);

    socket->connect(host.data, port.data);
}


Но если копнуть глубже... В первом варианте мы однозначно знаем сколько RAM потребуется выделить, а это большой + для embedded. В случаи с хранением этoго дела на стеке ситуация гораздо хуже (но удобнее).
Потом если это не embedded или есть памяти по-больше - можно выделять память в куче(или из пула фиксированных блоков - очень быстро и constant time), а удалится это все автоматически, через smart pointer - удобства не меньше, чем со стеком и затраты производительности копеечные.
Но это не так важно.
Важно другое - что, если нам на этот блок действий надо нацепить таймаут? Скажем, все эти 4 действия должны выполниться за 5 секунд(именно так и есть в оригинальной задаче, из которой я этот код выдрал). Если не успело - отменить и выдать ошибку.
На SST это сделать очень просто - в конце(или в начале, это вовсе не важно) просто запустить таймер
Код
void Client::do_socket_connect(){
// ...

   timer.start([this](){
       socket->abort();
       settings->abort();
       printf("do_socket_connect timeout\n");
   });
}

Реализовать такое на потоках - это довольно жутко, можете попробовать sm.gif
Если abort в SST - это по сути просто замена callbacka(или нескольких) на пустышку, то как абортануть поток, если он завис в ожидании мютекса? Ведь settings->read - довольно тяжелая и сложная задача - поиск в флеш-памяти файла, поиск в файле строчки с данным settingom, куча проверок итп. Аналогично и с socket->connect.
Да и потоков ради этого всего понадобится как минимум 2, если не больше.
XVR
Цитата(brag @ Sep 29 2016, 19:12) *
С чего бы это? Первым идет лок from, затем to.
transfer A->B : from=A, to=B
transfer B->A : from=B, to=A
И все, deadlock (раз в год).
пардон, не обратил внимание, что from и to это формальные параметры. Посыпаю голову пеплом, вы правы - будет dedlock

Цитата
Этот код я специально оставил без защиты, чтобы relacy хоть там нашел ошибку, но он и на это оказался не способен(или я не сумел его научить).
Я акцентировал внимание на двух переменных. Этот код(if в оригинале был защищен) из реальной практической задачи, где я спустя месяца 2 поймал гонку.
Да, код жутковатый cranky.gif

Цитата
Ну так критическая секция(запрет прерываний) - это и есть глобальнейший мютекс.
Не обязательно запрет прерывания. Это может быть запрет входить в блок кода (как на Windows CRITICAL_SECION). Но она должна быть поставлена на все места, где работают с этими переменными. И да, может получиться тормоз rolleyes.gif

Цитата
А что, если эти ff и exec не просто переменные, а сложные обьекты, хуже - работа с аппаратурой? Сами они защищены по отдельности, но не защищены в совокупности.
Защищать надо именно совокупность. Какими методами это делается не важно, главное результат

Цитата
А что если программист не умеет программировать в машинных кодах, вернее умеет, но это у него занимает очень много времени? Переходить на высокоуровневый язык или идти продавать пирожки?
Учить язык, если не получилось - идти продавать пирожки.

Цитата
Почти любая сложная программа на мютексах имеет гонки, при чем такие запутанные, что ручным анализом кода их выявить невозможно. Да и инструментов не так много(можно сказать их нет), и сама симуляция сложной программы(перебор всевозможных вариантов переключения контекста) может занимать месяц времени на 4-ядерном 3ггц Core-i7 процессоре. Да и то это еще надо уметь описать assert-ы для симулятора, это тоже очень не просто.
Если в программе много мьютексов и все они повязанны друг за друга, то в этом случае нужно переходить на более высокоуровневые примитивы (очереди например). Но только в этом случае. Совсем не обязательно всех и вся в принудительном порядке загонять под эти очереди

Цитата
Для этого есть очередь с приоритетами - сообщения в ней сортируются сначала в порядке приоритета, затем в порядке поступления. Реализация не сильно сложная, делается один раз и работает быстро.
Именно SST решает эту проблему без изощренных алгоритмов планировки задач ОС.
Вы опять путаете причину со следствием. Динамические приоритеты появились не потому что планировщик в ОС такой сложный, а именно такой сложный планировщик появился потому, что возникла потребность в динамических приоритетах.
Кстати, очередь с приоритетами не аналогична приоритетам задач в планировщике ОС. Если в очередь поставить задачу с меньшим приоритетом, а потом постоянно добавлять задачи с большим, то задача с меньшим приоритетом никогда не исполнится. А ОС все же будет исполнять задачу с меньшим приоритетом.

Цитата
В то время как сериализация на мютексах рано или поздно приведет к взрыву реактора - 500 лампочек начнут хватать мютекс хаотично, а времени для движка не останется, и наследование приоритетов не поможет. sm.gif
Сериалзация на мьютексах не делается

Цитата
Как по мне, SST подходит под контроллеры от уровня Atmega8 до STM32F407, а так же практически на все DSP.
Для STM32F407 уже можно и обычную ОС (типа scmRTOS или freertos), памяти у него хватит, а заниматься переучиванием себя под SST выйдет дороже. Использование SST в этом случае оправданно если вы эксперт в ней, и у вас уже есть большие наработки, и event drive стиль для вас привычен. Но на этом форуме похоже вы один такой laughing.gif

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

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

Цитата
Но ни в коем случаи не использовать мютексы, а использовать высокоуровневые обьекты, а мютексы пусть используют разработчики этих обьектов внутри их.
Ну как то так и делается. Лично я голые мьютексы не использую вообще, а как минимум делаю на них scoped критические секции. Захвата 2х и более мьютексов стараюсь избегать всеми силами и переходить на более высокоуровневы примитивы синхронизации. В 99% хватает scoped критической секции, так что к очереди мне приходилось использовать не более пары раз (а захват 2х мьютексов кажется вообще не приходилось)

Цитата
Реализовать такое на потоках - это довольно жутко, можете попробовать
Даже пробовать не буду sm.gif

Цитата
то как абортануть поток, если он завис в ожидании мютекса?
Для этого обычно используют примитивы захвата с timeout'ом
amaora
Цитата(XVR @ Sep 30 2016, 12:00) *
Ну как то так и делается. Лично я голые мьютексы не использую вообще, а как минимум делаю на них scoped критические секции. Захвата 2х и более мьютексов стараюсь избегать всеми силами и переходить на более высокоуровневы примитивы синхронизации. В 99% хватает scoped критической секции, так что к очереди мне приходилось использовать не более пары раз (а захват 2х мьютексов кажется вообще не приходилось)


Я где-то здесь недавно задавал вопрос. Нужно было сделать запрос и подождать готовности ответа, в частности функция i2c_read. Ничего лучше двух мьютексов не нашлось. На захвате первого ждем готовности интерфейса, на втором готовности ответа. Только второй примитив не мьютекс, а семафор. Можно как-то лучше? На очередях получается, еще сложнее.
XVR
Цитата(amaora @ Sep 30 2016, 17:39) *
Нужно было сделать запрос и подождать готовности ответа, в частности функция i2c_read. Ничего лучше двух мьютексов не нашлось. На захвате первого ждем готовности интерфейса, на втором готовности ответа. Только второй примитив не мьютекс, а семафор. Можно как-то лучше?
Это зависит от того как вы в дальнейшем используете результат (в частности нужен ли он в этом потоке, или можно сделать асинхронный callback), и используется ли еще где то работа с этой самой i2c шиной (или с теми ресурсами, которые нужны для такой работы). И если да, то можно ли собрать всю эту работу в одно место?


brag
Цитата
Для этого обычно используют примитивы захвата с timeout'ом

A если надо таймаут не на захват одного ресурса, а на несколько? Не спорю, на потоках тоже можно реализовать, на на SST это проще.
Та же задача с атомным реактором - считываем группу датчиков, если не успели за 5 сек - срабатывает высокоприоритетное событие, которое задвинет стержни и потушит реакцию. С очередями аналогично - вдруг где-то что-то в очередь не влезло - срабатывает событие, которое гарантировано(при любых раскладах) задвинет стержни. Для таких ответственных задач нужна статическая память(то есть гарантия ее наличия еще на этапе компиляции) - никаких стеков и куч и event-ы - никаких сложных планировщиков. И проверенные временем высокоурвневые обьекты - без ручной работы с памятью, иначе будет высокая вероятность схватить segfault - от человеческого фактора никто не застрахован.

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

Ну да, в SST аналогично - если будет флуд высокоприоритетных событий - низкоприоритетные никогда не запустятся. Если такое поведение не подходит - тогда нужна ОС со сложным планировщиком. Обычно в таких ОС о приоритетах никто уже не задумывается - приоритеты динамически расставляет сама ОС

Цитата
Для STM32F407 уже можно и обычную ОС (типа scmRTOS или freertos), памяти у него хватит, а заниматься переучиванием себя под SST выйдет дороже. Использование SST в этом случае оправданно если вы эксперт в ней, и у вас уже есть большие наработки, и event drive стиль для вас привычен. Но на этом форуме похоже вы один такой

Да, нужно иметь опыт в even-driven. Много памяти - это очень удобно для SST - можно смело использовать динамическую память (кучи или пулы блоков фиксированных длин), там, где это уместно - тогда начинаешь чувствовать себя, как в Javascript-е sm.gif

Цитата
Я где-то здесь недавно задавал вопрос. Нужно было сделать запрос и подождать готовности ответа, в частности функция i2c_read. Ничего лучше двух мьютексов не нашлось. На захвате первого ждем готовности интерфейса, на втором готовности ответа. Только второй примитив не мьютекс, а семафор. Можно как-то лучше? На очередях получается, еще сложнее.

А зачем ждать готовности интерфейса? Добавили запрос в очередь. Как только дойдет до него дело(а раз дошло, значит шина уже свободная) - он выполнится и по завершению выдаст соответствующий event. В любом контроллере есть для этого соответствующие прерывания.
Та даже если и нет, а работаем с шиной вручную дрыганием ног - все точно так же - достали из очереди запрос, выполнили, выкинули соответствующий event. Почему просто не выполнить это без всяких очередей - потому что с SST можно выполнять работу с дрыганием ног в своем приоритете, чтобы и другим не мешать, и чтобы глюков(задержек клока итп) на шине не было, из за того, что кто-то прервал нашу работу.
Такие задачи работы с аппаратурой на SST и очереди ложатся, как влитые.
amaora
Цитата(XVR @ Sep 30 2016, 22:48) *
Это зависит от того как вы в дальнейшем используете результат (в частности нужен ли он в этом потоке, или можно сделать асинхронный callback), и используется ли еще где то работа с этой самой i2c шиной (или с теми ресурсами, которые нужны для такой работы). И если да, то можно ли собрать всю эту работу в одно место?


Нужен в этом потоке. Вызов к i2c может произойти из 2-3 потоков, с разной частотой или нерегулярно. В одно место собрать можно, если захотеть. Но вопрос именно в том как делать подобные блокирующие вызовы а не избавится от них. Например, при чтении/записи файлов не те же проблемы возникнут?

Цитата(brag @ Sep 30 2016, 23:21) *
А зачем ждать готовности интерфейса? Добавили запрос в очередь. Как только дойдет до него дело(а раз дошло, значит шина уже свободная) - он выполнится и по завершению выдаст соответствующий event. В любом контроллере есть для этого соответствующие прерывания.
Та даже если и нет, а работаем с шиной вручную дрыганием ног - все точно так же - достали из очереди запрос, выполнили, выкинули соответствующий event. Почему просто не выполнить это без всяких очередей - потому что с SST можно выполнять работу с дрыганием ног в своем приоритете, чтобы и другим не мешать, и чтобы глюков(задержек клока итп) на шине не было, из за того, что кто-то прервал нашу работу.
Такие задачи работы с аппаратурой на SST и очереди ложатся, как влитые.


Вопрос был не про SST, на нем задача решится, не сомневаюсь. Проблема только в том, что в C это будет callback hell, а на более другие языки пока нет желания переходить.

А каким образом вы делаете выход из PendSV обработчика на Cortex-M* когда надо после него вызвать обработку события? Где-то упоминали, я не понял.
brag
Цитата
Вопрос был не про SST, на нем задача решится, не сомневаюсь. Проблема только в том, что в C это будет callback hell, а на более другие языки пока нет желания переходить.

Если грамотно разложить на функции, то hell-a не будет. Наоборот в C есть вложенные функции, а в C++ их нет sad.gif Правда есть лямбды, недавно появились относительно.
Сахарку можно добавить в С с помощью макросов, если нужно.
А можно(я так иногда делаю) прогонять исходник через скрипт на Python-е(или что больше по душе) - расширять сишный синтаксис своими фантазиями wink.gif Иногда мучаюсь с шаблонами, макросами неделю, потом плюю на все стандарты, беру питон и решаю задачу за 10 минут.
Но согласен, без мощного языка работать с event-ами напряжно и лень пробирает sm.gif

Цитата
А каким образом вы делаете выход из PendSV обработчика на Cortex-M* когда надо после него вызвать обработку события? Где-то упоминали, я не понял.

На ассемблере, довольно хитрым, хотя может и не совсем sm.gif
CODE
__attribute__((naked)) void ePendSV(){
asm volatile(
// push = ldmia; pop = stmdb
// "push {r0,r4-r11, lr} \n" // not necessary, callee saved
// create new stack frame for scheduler
"mov r2, %0 \n" // PSR
"movw r1, #:lower16:SST_sync_scheduler \n" // PC
"movt r1, #:upper16:SST_sync_scheduler \n"
"movw r0, #:lower16:async_scheduler_return \n" // LR
"movt r0, #:upper16:async_scheduler_return \n"
"push {r0, r1, r2} \n" // push {lr, pc, xPSR}
"sub sp, #5*4 \n" // push {r0,r1,r2,r3,r12} - undefined values
// return from exception to scheduler
"bx lr \n"
: :"i"(xPSR_RESET_VALUE));
}

// return to preemtded task through SVC
extern "C" __attribute__((naked)) void async_scheduler_return(){
asm volatile("svc #0");
}

__attribute__((naked)) void eSVCall(){
asm volatile(
//"mov r0, sp \n"
//"bl dump_stack \n"
// kill current stack frame
"add sp, #8*4 \n"
// "pop {r4-r11, lr} \n" // not necessary
// perform an EXC_RETURN (unstacking)
"bx lr \n"
);
}

Смысл в чем - при входе в PendSV создаем кадр стека, кидаем туда адрес функции-планировщика и адрес возврата из этой функции(туда попадем, когда планировщик отработает).
По адресу возврата лежит единственная инструкция - SVC, которая кинет нас в другой обработчик, там нам нужно убрать после себя - удалить наш кадр стека, и таким образом при выходе из этого обработчика мы попадем туда, откуда ранее попали в pendSV.
Этот механизм нужен только для асинхронного прерывания задачи, то есть при добавлении задачи в очередь из прерываний.
Чтобы вызвать планировщик из прерывания(а это делается автоматически после добавления задачи в очередь) достаточно установить PendSV, и по завершению обработки всех прерываний мы попадем в наш хитрый pendsv. Сам pendsv тоже может быть прерван в любой момент - это абсолютно безопасно.
Синхронное прерывание задачи - это просто вызов функции sm.gif

На кортексе я добавил рантайм-сахара, чтобы не задумываться откуда вызывается планировщик - из прерывания или Thread-mode, тк для меня, что прерывание, что любой другой код - это одно и то же - прерывание точно такая же задача, как и все остальные, имеет свой приоритет, только этот приоритет всегда выше приоритетов обычных задач(не-прерываний).
Код
// invoke sync or async scheduler
void SST::choose_and_invoke_scheduler(){
    uint32_t psr;
    __asm volatile("mrs %0, IEPSR" :"=r"(psr));

    if((psr&0x1FF) == 0){ // Thread mode
        SST_sync_scheduler(); // тупо вызов функции
    }else{ // Handler mode
        PendSVset(); // установка флага pendSV
    }
}



Без очередей сериализация доступа к аппаратуре может превратиться в мрак. Допустим есть шина I2C.
Ок, для сериализации нам нужен мютекс, назовем его i2c_mutex.
На этой шине висит 5 устройств. Для сериализации доступа к ним нам опять нужно 5 мютексов device_mutex_X.
Дальше одно из устройств - память, часть из которой просто буфер, а другая часть хранит настройки(тн. Settings), для сериализации нам опять нужно 2 мютекса flash_buffer_mutex и settings_mutex.

Получается, чтобы прочитать какой-нибудь setting нам нужно:
1. схватить settings_mutex
2. схватить device_mutex_memory
3. схватить i2c_mutex
4. что-то сделать
5. отпустить i2c_mutex
6. отпустить device_mutex_memory
7. если нужно - перейти к п2.
8. отпустить settings_mutex
Мало того, работа с i2c будет скорее всего выполняться(хотя бы частично) из прерываний, а для этого нам понадобится еще и семафор.

Это еще довольно простой случай. До dead-locka тут еще далеко(хотя не факт), но вот resource starvation при таком обилии локов будет однозначно, если шина нагружена. Ну и оверхед на мютексы, в реализации самих мютексов по любому будут очереди(гыsm.gif и блокировки.
А если вдруг захочется прочитать setting при работе с устройством X на i2c шине, что будет? Правильно, можно схватить deadlock(раз в неделю/месяц/год).
Можно, конечно, всю работу устройств на i2c покрыть одним жирным мютексом(а не это кучей мютексов), но это во первых ослабляет инкапсуляцию, а во вторых - это тормоза, шина будет простаивать во время обработки данных с флешки.

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

К стати такая динамическая ОС тоже может быть чисто асинхронной (event-driven), но это будет уже не SST, a SCT - Super Complex Tasker sm.gif
Но не на столько complex, как обычная ОС. Если потоки будут без блокировок(но с тайм-квантами и динамическими приоритетами), то такой планировщик проще, тк поток никогда не блокируется, он может быть только вытеснен другим потоком, так, как это происходит в SST.

ps. Изначально на Javascript асинхронный стиль был вынужденной мерой. WEB-приложения становились все сложнее и сложнее. Потом народ просек, что этот стиль довольно крут и придумал NodeJS, чтобы писать в том же стиле и серверы(да и просто прикладной софт), которые раньше делались на блокирующем PHP sm.gif
XVR
Цитата(amaora @ Sep 30 2016, 23:48) *
Нужен в этом потоке. Вызов к i2c может произойти из 2-3 потоков, с разной частотой или нерегулярно. В одно место собрать можно, если захотеть. Но вопрос именно в том как делать подобные блокирующие вызовы а не избавится от них.
brag прав - тут напрашивается сериализация (например с помощью очереди). Если работу с самой I2C шиной можно собрать в одну процедуру (или набор процедур), то сериализацию можно встроить в них. Таким образом на уровне интерфейса ее видно не будет. Еще можно саму работу с I2C разбить на автомат и поместить в прерывания. Вариант в 2мя мьютексами рожалуй самый простой, но его надо очень внимательно изучить на предмет гонок, и в особенности в части синхронизации проверок состояния I2C аппаратуры и ожидания на мьютексах - можно словить deadlock.
Цитата
Например, при чтении/записи файлов не те же проблемы возникнут?
Возникнут. Вариантов решений много - начиная от запрета одновременных обращений из разных потоков к файлам, и заканчивая асинхронным вводом/выводом и сериализацией на уровне ОС

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


Цитата(brag @ Oct 1 2016, 00:46) *
Если грамотно разложить на функции, то hell-a не будет. Наоборот в C есть вложенные функции, а в C++ их нет sad.gif
В С их нет. Они есть в gcc, но это его собственное расширение (для С)

brag
Цитата
В С их нет. Они есть в gcc, но это его собственное расширение (для С)

Да, действительно, с C их нет. как-то довелось их применять и компилятор схавал, поэтому и подумал, что они есть в C.
Тогда энкапсулировать можно на уровне файлов. i2c движок не сложный, использовать указатели на функции и typedef, на C норм код получиться.
amaora
Цитата(brag @ Oct 1 2016, 00:46) *
Смысл в чем - при входе в PendSV создаем кадр стека, кидаем туда адрес функции-планировщика и адрес возврата из этой функции(туда попадем, когда планировщик отработает).
По адресу возврата лежит единственная инструкция - SVC, которая кинет нас в другой обработчик, там нам нужно убрать после себя - удалить наш кадр стека, и таким образом при выходе из этого обработчика мы попадем туда, откуда ранее попали в pendSV.
Этот механизм нужен только для асинхронного прерывания задачи, то есть при добавлении задачи в очередь из прерываний.
Чтобы вызвать планировщик из прерывания(а это делается автоматически после добавления задачи в очередь) достаточно установить PendSV, и по завершению обработки всех прерываний мы попадем в наш хитрый pendsv. Сам pendsv тоже может быть прерван в любой момент - это абсолютно безопасно.
Синхронное прерывание задачи - это просто вызов функции sm.gif


Почитал programming manual, стало ясно, но сложно, хочется проще.

Цитата
brag прав - тут напрашивается сериализация (например с помощью очереди). Если работу с самой I2C шиной можно собрать в одну процедуру (или набор процедур), то сериализацию можно встроить в них. Таким образом на уровне интерфейса ее видно не будет. Еще можно саму работу с I2C разбить на автомат и поместить в прерывания. Вариант в 2мя мьютексами рожалуй самый простой, но его надо очень внимательно изучить на предмет гонок, и в особенности в части синхронизации проверок состояния I2C аппаратуры и ожидания на мьютексах - можно словить deadlock.


Сама работа с i2c уже на автомате в прерывании. Снаружи только запуск передачи и получение результата. Проверки занятости i2c нет, если мьютекс не взят то предполагается, что i2c не занят, ставим указатель на структуру i2c_request и запускаем прерывание, ждем семафор.

Можно спланировать использование i2c так, чтобы запросы группировались и шли регулярно с некоторой частотой. К некоторым устройствам на каждый такт, к другим лишь иногда. Но это усложнит код использующий i2c и возможно повысит его связность с кодом i2c, в зависимости от деталей. Будут запросы на чтение, а потом рассылка результатов. Обработать все результаты в одной задаче нельзя, разная частота, разные приоритеты.
brag
С мютексом можно работать, но если начнутся вложенные мютексы - пора переходить на очереди...
К стати как с семафором из прерывания работа выполняется? Через очередь или через блокировку всех прерываний?

Цитата
Почитал programming manual, стало ясно, но сложно, хочется проще.

Проще не будет, кортекс не поддерживает рекурсивные прерывания, поддерживает только вложенные. А для SST нужна рекурсия: задача->планировщик->задача->планировщик->задача итд.
amaora
Цитата(brag @ Oct 1 2016, 22:15) *
С мютексом можно работать, но если начнутся вложенные мютексы - пора переходить на очереди...
К стати как с семафором из прерывания работа выполняется? Через очередь или через блокировку всех прерываний?

Проще не будет, кортекс не поддерживает рекурсивные прерывания, поддерживает только вложенные. А для SST нужна рекурсия: задача->планировщик->задача->планировщик->задача итд.


Во FreeRTOS есть xSemaphoreGiveFromISR, она внутри реализована через очередь (базовый примитив, через него все остальные) с одним элементом нулевого размера, а работа с очередью скорее всего содержит в себе запрет прерываний до некоторого приоритета.
brag
На кортексе можно обойтись без запрета прерываний, у меня именно так планировщик был сделан в моей блокирующей ОС(которой я сейчас не пользуюсь, тк перешел на неблокирующий SST).

Вот процедура добавления syscall-a из прерывания в очередь. Стиль еще старый сишный sm.gif
CODE
int srqEnqueue(uint32_t svc, uint32_t r0, uint32_t r1){
// конкурентное резервирование места под syscall в очереди - без блокировок
uint32_t t, newtail;
do{
uint32_t h = dsrQueue.head;
t = __ldrexh(&dsrQueue.tail); // по этому индексу t будем писать данные, если получиться
newtail = (t+1)&(Srq::size-1);
if(newtail==h)return Error::QUEUE_FULL;
}while(__strexh(newtail, &dsrQueue.tail) );

// все, место удалось зарезервировать, теперь пишем туда данные
Srq::qitem *qi = &dsrQueue.data[t];
qi->svc = svc;
qi->r0 = r0;
qi->r1 = r1;

// выгрузка и выполнение syscall-ов из очереди происходит в PendSV
// у него самый низкий приоритет изо всех прерываний, поэтому в нем тоже блокировок нет
PendSVset();
return 0;
}

// Сама выгрузка и выполнение выглядит вот так.
void Srq::DequeueRun(){
while(1){
uint32_ h = head; // эта head переменная volatile, это важно
if(tail==h)break;
svcTable[data[h].svc](reinterpret_cast<ExceptionStackFrame*>(&data[h])); // собственно виполнение
head = (h+1)&(size-1); // make item available for writing
}
}
// Блокировки тут не нужны, тк запись head происходит только здесь и она не конкурентная

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

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


С event-driven мы уже научились работать sm.gif, теперь нужно двигаться дальше, тк асинхронщина - это путь к функциональному стилю. Чисто ФП-язык(Haskell, Erlang,...) на мк уровня LPC175x/STM32F10x использовать для практических RT-задач вряд ли получится, МК слишком слабые для этого, поэтому придется пользоваться тем, что есть. Это на данный момент C++14 и Rust.
Введение в ФП-асинхронщину тут https://www.fpcomplete.com/blog/2012/06/asy...tinuation-monad
Но для начала на Haskell пописать все таки придется, без этого никак. Вот по этому туториалу учился(и учусь) я http://learnyouahaskell.com/introduction#about-this-tutorial
А тут, интересные, на мой взгляд, практические рекомендации, как применять ФП на C++(14):
http://cpptruths.blogspot.com/2014/03/fun-...yle-part-1.html
http://cpptruths.blogspot.com/2014/05/fun-...yle-part-2.html
http://cpptruths.blogspot.com/2014/08/fun-...yle-part-3.html
brag
В процессе написания очередного проекта столкнулся с частыми пропусками ошибок добавления в очередь (человеческий фактор однако).
Код
void f(...,const delegate<void()>& cbk){
// ...
   SST::postMessage(cbk);
}

Вместо:
Код
bool f(...,const delegate<void()>& cbk){
//...
   if(SST::postMessage(cbk)){
      return true;
   }else{
//...
      return false;
   }
}

Ну и последующей обработки этих ошибок.

Поэтому изменил паттерн исполняемого обьекта, добавляемого в очередь.
Код
typedef VdelegateFIFO<void(Error)> TEventQueue;

Движок очереди автоматически вызовет callback(Error::QueueFull) если нет места в очереди.

Сам класс Error:
CODE
class Error{
public:
enum E{
NoError = 0,
QueueFull = 1<<(sizeof(int)*8-1),

last_
};

Error(int e): value_(static_cast<E>(e)){}

bool isError()const{ return value_ < 0; }
operator bool()const { return isError(); }

private:
E value_;
};


От Error можно наследоваться, хоть и кривовато, но что поделаешь, C++ однако еще тот sad.gif
CODE
class DerivedError : public Error{
public:
enum E{
first_ = last_-1,

Error1,
Error2,

last_
};

using Error::Error;
};


Ну и добавляется сообщение в очередь таким способом:
Код
void f(...,const delegate<void(Error)>& cbk){
//...
   SST::postMessage(cbk);
}


Ошибку подлавливаем уже в обработчиках
Код
f([this](Error e){
   if(e){ handle_error(e); return; }
});

Если ошибку не обработать - компилятор об этом напомнит, это очень сильно помогает. Мало того, поскольку в SST нет блокировок - раскрутка стека происходит автоматически и бесплатно, в отличии от синхронных исключений C++. А обработка ошибок практически такая же, как catch в C++ - подлавливаем группу ошибок в одном месте (в данном примере это функция handle_error

Да, не совсем удобно из за неприспособленности C++ к асинхронщине.
Можно сделать по-удобнее, двумя вариантами:
1. либо написать препроцессор(например на питоне) - все зашибись, но это нестандарт.
2. либо выучить haskell (научиться на нем программировать), написать на нем обертку, потом перевести на шаблоны C++ (на шаблонах можно программировать только в чисто функциональном стиле, а чтобы это уметь - надо иметь опыт в haskell, там все куда проще и круче). - но синтаксис может получиться не совсем короткий, зато решение стандартное.

Пока мне хватает ручного handle_error. Если будут проблемы - скорее всего буду делать по второму варианту.
DASM
А автор тем временем вытяснющую блокирующую в комлект включил.
AHTOXA
Дайте, пожалуйста, ссылочку, чтобы по всей теме не искать.
DASM
https://www.state-machine.com
AHTOXA
Благодарю.
DASM
Я потихоньку начинаю проникаться сабжем, начну на днях проектик. Правда думаю допилить до 32 уровней приоритета. И чего то неуверен, что мне вытеснение вообще нужно, скорее только мешать будет
DASM
brag, а Вы дин. памятью пользуетесь? Выкинув эти топорные ждущие оси в связи с появлением памяти в предскауемых объемах задумался, а так ли страшна фрагментация как ее малюют? Жутко надоело на каждый чих типа контйенера и чего нить стандартного из boost / stl придумывать свои костыли глючные. 21 век на дворе, компилер для Silabs мой даж лямбды понимает и int qq = 0xb10111; (мечта железячника) sm.gif
Для просмотра полной версии этой страницы, пожалуйста, пройдите по ссылке.
Invision Power Board © 2001-2025 Invision Power Services, Inc.