Как обмениваться данными между задачами/потоками, не связывая их?

Я разрабатываю довольно сложное приложение для микроконтроллера на C, и у меня есть некоторые сомнения относительно того, как «связать» мои общие данные между различными задачами/потоками, не связывая их.

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

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

У нас есть 4 задачи/потока: 3 входных потока, которые считывают некоторые данные датчиков с разных датчиков через аппаратные абстракционные слои (HAL). Собранные данные датчиков хранятся в домене задачи (т.е. они не будут глобальными!!). Теперь у нас также есть 1 выходная задача, назовем ее «Регулятор». Регулятор должен использовать (считывать) данные датчиков, собранные со всех трех датчиков, для получения надлежащего результата.

Вопрос: Как Регулятор будет считывать собранные данные, хранящиеся в разных входных задачах, без привязки к другим задачам?

Регулятор должен знать о входных задачах и их данных только по ссылке (т.е. без #includes, без связи).

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

Я мог бы создать несколько функций getSensorValue(), использующих семафоры, для каждого значения датчика, а затем связать их с регулятором с помощью указателей на функции. Но это займет много памяти!! Есть ли более элегантный способ сделать это? Я просто ищу входы.

Надеюсь все понятно :)


person Jolle    schedule 03.04.2013    source источник
comment
На каком оборудовании вы работаете? Семейство микроконтроллеров имеет большое значение для того, что можно сделать. Например на многих микроконтроллерах нет виртуальной памяти. В других контроллерах «потоки» на самом деле не являются потоками, а просто переставленными сторожевым таймером указателями стека.   -  person Pyrce    schedule 03.04.2013
comment
На данный момент я работаю на PSoC5, позже я планирую портировать/конвертировать в STM32?   -  person Jolle    schedule 03.04.2013
comment
Эти семейства должны иметь настоящие таблицы многопоточности/виртуальной памяти. Когда вы говорите, что защита данных не позволит приложению читать данные, относящиеся к потоку, вы имеете в виду, что эти «потоки» запускаются как процессы, а не как потоки? Поскольку потоки могут читать память другого потока, если вы явно не ограничите эту память локальными ключевыми словами потока, такими как __thread. Что вы считаете много памяти, 3 семафора должны занимать всего несколько байт памяти. Или потоки не могут быть заблокированы в течение длительного времени из-за скорости чтения сенсора?   -  person Pyrce    schedule 03.04.2013
comment
Спасибо за ваш ответ! Я попытаюсь уточнить: скажем, у меня есть только 2 потока, определенных в: Sensor.h и Regulator.h. Sensor соберет некоторые данные и сохранит их в некоторых переменных, определенных в Sensor.c. Теперь Regulator.c должен получить доступ к этим данным БЕЗ привязки к sensor.c (я имею в виду без создания #include Sensor.h).   -  person Jolle    schedule 03.04.2013
comment
Из любопытства, почему вы не можете включить Sensor.h? В этой ситуации вам нужен один главный файл, который включает в себя оба заголовка, запускает все потоки и связывает их вместе, чтобы они могли получить доступ к данным друг друга. Похоже, что из вашего описания Regulator.c не может знать о данных в Sensor.c без включения Sensor.c (не рекомендуется) или Sensor.h?   -  person Pyrce    schedule 03.04.2013
comment
Это потому, что я хочу, чтобы приложение было настолько общим, насколько это возможно. Я сделаю другой пример, объясняющий это лучше. Я снова получил 2 потока: Motionsensor1.h и Regulator.h. Motionsensor1 собирает данные с различных датчиков, таких как гироскопы и акселерометры, и объединяет данные, используя один тип слияния датчиков (например, дополнительный фильтр). Регулятор использует выходные данные от Motionsensor1 для создания какого-либо вывода. Теперь я создаю новый поток: Motionsensor2, который генерирует тот же тип выходных данных, что и Motionsensor1, но использует другое слияние датчиков (например, фильтр Калмана). Продолжение..   -  person Jolle    schedule 03.04.2013
comment
› Теперь Motionsensor1 и Motionsensor2 не обязательно реализуются одной и той же стороной, поэтому переменные, содержащие данные, могут называться по-разному. Поэтому я не могу просто изменить #include ... Здесь было бы разумно с моей стороны просто дать правильную ссылку в инициации Regulator.   -  person Jolle    schedule 03.04.2013
comment
Так что мне не нужно менять какой-либо код в регуляторе, чтобы он использовал в качестве входных данных другие данные. Я нахожу это элегантным :)   -  person Jolle    schedule 03.04.2013
comment
То, что вы описываете, — это просто общий интерфейс между датчиками и регуляторами, что не исключает возможности включения Motionsensor2.h. На самом деле, Motionsensor2.h должен быть включен регулятором (прямо или косвенно), или Motionsensor2.h должен включать регулятор, или другой файл должен включать оба, чтобы эти два файла взаимодействовали. Правильная ссылка в начале регулятора указывает на то, что у вас есть основной файл, который передает ссылку на датчик в регулятор. Я опубликую ответ через некоторое время, чтобы расширить то, что я имею в виду, поэтому мы не будем расширять этот раздел комментариев намного дальше.   -  person Pyrce    schedule 03.04.2013
comment
Я согласен! И то, что вы описываете, это то, как это работает сегодня! Магистраль передает указатели, указывающие на непосредственные данные датчиков, на Регулятор. Проблема в том, что данных датчиков много, поэтому они становятся немного беспорядочными. И поскольку Регулятор получает прямой указатель данных, реализовать защиту данных невозможно. Тогда мне придется сделать getFunctions() для всех данных датчика, а затем передать эти указатели функций в Regulator, но тогда это займет много места :)   -  person Jolle    schedule 03.04.2013
comment
проект, над которым вы работаете, является хорошим кандидатом для планировщика временных интервалов. Я действительно не вижу смысла переходить на потоки.   -  person Kamyar Souri    schedule 04.04.2013


Ответы (1)


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

Поскольку вы работаете в C и не имеете некоторых функций класса C++, которые упростили бы инкапсуляцию посредством наследования, я предлагаю вам создать общий пакет данных из каждого потока датчика, который передается в регуляторы, а не передавать указатель на функцию. Структура формы

struct SensorDataWrap {
    DataType *data;
    LockType *lock;
    ... other attributes such as newData or sensorName ...
};

позволит вам передавать данные регуляторам, где вы можете заблокировать их перед чтением. Точно так же датчики должны быть заблокированы перед записью. Если вы изменили данные на двойной указатель DataType **data, вы можете заставить команду записи блокироваться только на время, необходимое для замены базового указателя. Регулятору тогда просто нужна одна структура SensorDataWrap из каждого потока для обработки информации этого потока независимо от деталей реализации датчика.

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

Детали реализации для связи теперь ограничиваются блокировкой, выполнением операции, разблокировкой и не требуют сложных указателей функций или включения заголовка, специфичного для SensorN. Он должен быть близок к минимальной логике, необходимой для любой многопоточной программы с общими данными. Программа также должна быть перенесена на другие микроконтроллеры без существенных изменений - связь действительно ограничена только наличием/отсутствием потоковой передачи и блокировок.

Другой вариант — передать объект тройного буфера и выполнить переворот буфера, чтобы избежать семафоров и блокировок. Для этого подхода требуется поддержка атомарного целого числа/логического значения (которую вы, скорее всего, предоставили компилятору, если у вас есть семафоры). Руководство по использованию тройных буферов для параллелизма можно найти на этом блог. Этот подход будет использовать немного больше активной памяти, но это очень удобный способ избежать большинства проблем параллелизма.

person Pyrce    schedule 03.04.2013
comment
Ok! Это звучит (немного) как очереди сообщений, и у меня была такая мысль, но я пришел к выводу (без каких-либо веских аргументов), что это излишество. Хотя это не совсем то, о чем вы говорите, верно? Если я правильно понимаю, в Regulator должна быть ссылка на инстанс SensorDataWrap (кстати, какой typedef должен быть объявлен как для Sensor, так и для Regulator), а внутри SensorDataWrap есть ОБА данные и какой-то механизм защиты? Мне нравится идея! Однако часть двойного указателя я не совсем понял. - person Jolle; 04.04.2013
comment
Именно, реализация, которую я написал, представляет собой упрощенную очередь сообщений, в которой регуляторы несут ответственность за чтение данных до того, как они будут перезаписаны датчиком. Реализация очереди сообщений будет работать так же хорошо и не будет излишней. Двойной указатель — это второстепенная нота. Версия с одним указателем требует, чтобы датчики удерживали блокировку, пока они записывают все свои показания в данные. Двойной указатель позволил бы им записывать в новый объект «данные» и блокировать их только тогда, когда они изменяли указатель для обращения к новым считываниям. Однако для этого требуется управление памятью, поэтому это явно не лучше. - person Pyrce; 04.04.2013
comment
Хорошо, я на самом деле думаю, что это лучше, чем очереди сообщений, потому что регулятор работает в режиме реального времени, и поэтому он не обязательно будет потреблять данные так же быстро, как их производит Сенсор. Регулирующий орган должен постоянно получать доступ к новейшим данным. Но большое спасибо, это был именно тот вклад, который я искал! - person Jolle; 04.04.2013