0

Есть два потока, один из которых выступает производителем данных, а второй - их потребителем. Так же есть общий пул данных, например, массив структур типа :

struct D{
char fl_busy;
char data[256];
}; 

Поток-производитель выполняет только следующие 3 действия :

  1. просматривает пул на предмет наличия пустого элемента (перебирает элементы до первого, в котором fl_busy = 0);
  2. если такой элемент найден, помечает его занятым (fl_busy = 1), заполняет оставшуюся часть структуры data;
  3. передает указатель на только что заполненный элемент пула потоку-потребителю (например, добавляет указатель в очередь, которую мониторит поток-потребитель).

Поток-потребитель, в свою очередь, выполняет только свои 3 действия :

  1. ожидает, когда в очереди появится указатель на элемент пула, и изымает его из очереди;
  2. получив указатель, как-то обрабатывает данные data в структуре (модифицирует, копирует куда-то);
  3. помечает данные элемент пула свободным (fl_busy = 0);

Вопросы:

  1. Какие вообще механизмы лучше использовать в подобных задачах, чтобы не снижать производительность, но при этом добиться, чтобы значение флагов в элементах пула было видимым и актуальным в обоих потоках и гарантировать правильную последовательность действий в потоках? Volatile в каждом элементе пула использовать совсем не хочется из-за производительности, да и все проблемы он, вероятно, не решит. Кажется, что тут имеет смысл использовать барьеры памяти, но я пока плохо с ними освоилась, и мне не помешал бы пример.
  2. Нужны ли тут атомарные функции и объявление fl_busy как atomic, с учетом что флаг всего байт?
margosh
  • 2,753
  • Один поток ставит флаг, другой его снимает. А нужен ли вообще этот флаг? Производитель получает данные и помещает в потокобезопасную коллекцию. Потребитель выгребает данные из коллекции и обрабатывает. Осталось найти готовую подходящую коллекцию. – Alexander Petrov May 19 '17 at 08:38
  • @AlexanderPetrov, нужен! В коллекцию передаются не данные, а указатель на них, без этого флага первый поток не поймет, когда элемент пула с этим указателем можно использовать повторно – margosh May 19 '17 at 09:00
  • @Abyx, да, конечно, второй поток должен был ожидать на condition variable – margosh May 19 '17 at 10:57
  • @margosh: Тогда без мьютекса никак. Вот вам пример на pthreads: https://ru.stackoverflow.com/a/428867 – VladD May 19 '17 at 11:25
  • @VladD, да это понятно как раз, мьютекс и cond. var. будут защищать очередь, меня другое интересует - мне нужно чтобы не получилось, что поток-потребитель обработал данные в элементе пула, и пометил его как свободный, а поток-производитель не заметил, что элемент освободился. Заходите в чат ;) – margosh May 19 '17 at 11:33
  • @margosh: Я с телефона, так что буду писать медленно :) – VladD May 19 '17 at 11:47
  • @margosh: А в каком чате обсуждение? – VladD May 19 '17 at 12:34
  • @VladD, в С++ \ С – margosh May 19 '17 at 12:41
  • @margosh, (только сейчас добрался до компа) volatile в такой схеме обязателен (и ни с какими memory fence это не связано). Без volatile на элемент структуры (точнее указатель, по которому вы будете его менять) компилятор (скажем с -O2) попросту может оставить новое значение в регистре и не писать его в память. Именно компилятор. Так что, в любом случае компилируйте такие кусочки с -S и смотрите, что есть команда записывающая регистр в память. – avp May 21 '17 at 18:02
  • 1
    @avp этот подход сильно устарел. Просто нужно помечать переменную, в которую лезут многие потоки, как atomic В С++20 так вообще - volatile будет deprecated. – gbg Jul 03 '20 at 07:15

2 Answers2

1

Прямо отвечая на ваши требования - Вам нужно использовать std::atomic_flag.

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

Относительно барьеров памяти. Если вы внимательно почитаете, какие параметры кушают атомарные переменные в C++, вы там эти самые memory orders увидите. По умолчанию они как раз обеспечивают то, что вы просите - когерентность.

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

Потому что это может быть, как минимум

mov память, регистр
add регистр
mov регистр, память

А теперь пришел планировщик, и после первого mov все сломал.

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

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

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

gbg
  • 22,253
0

флаг всего байт - какая разница?! Хоть полбайта! :-) АТОМАРНОСТЬ в многопотоковом программировании означает нечто совсем другое. Конкретно, вот вы пишете:

  1. проверям fl_busy = 0
  2. помечаем fl_busy = 1

Вы понимаете, что в момент МЕЖДУ пунктом 1 и пунктом 2 может произойти прерывание и управление передано будет на другую нить? И вся программа рухнет..

Так вот АТОМАРНОЙ будет операция, объединяющая пункты 1 и 2 и которая не может быть прервана. Эта операция в большинстве ОС реализована с помощью механизма мьютексов. Читайте !!! Это большая и сложная тема.

Sergey
  • 13,474
  • Вы плохо вникли в описанную задачу, так что Ваш ответ с вопросом мало общего имеет – margosh May 19 '17 at 07:42
  • Вы плохо вникли в описанную задачу Может быть... :-( Но вообще-то, задача производитель/потребитель настолько стара и настолько изучена, что вникать тут уже, вроде-бы, некуда. Вот, можете посмотерть студенческую работу на эту тему: http://www.hpcc.unn.ru/multicore/materials/os/MultiCore-OS-Lab1-ProducerConsumer.pdf – Sergey May 19 '17 at 07:50
  • Вы опять упираете на мьютексы, но вообще-то это довольно медленное решение, которое меня не устраивает, меня в данном случае интересуют lock-free варианты и обеспечение когерентности кэша в потоках при помощи менее затратных по сравнению с мьютексами средств – margosh May 19 '17 at 07:55
  • это довольно медленное решение, которое меня не устраивает - странно.... Насколько мне известно, это самаое распространённое решение проблемы, которым пользуются практических во всех программных системах с параллелизмом. Включая самые мощные серверы. Ну, если Вам кажется, что это медленно, попробуйте напрямую использовать семафоры (man 7 sem_overview). Собственно говоря, мьютекс - это обёртка над семафорамами, имеющая всего 2 состояния: 0 и 1. Попробуйте их. Кстати, для задачи производитель/потребитель они очень подходят. – Sergey May 22 '17 at 02:32