Замена буферов в потоках с одним записывающим и несколькими читателями - PullRequest
4 голосов
/ 18 октября 2011

История

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

Writer (case 1):
acquire lock 0                        
loop
    write to current buffer
    acquire other lock
    free this lock
    swap buffers
    wait for next period

или

Writer (case 2):
acquire lock 0                        
loop
    acquire other lock
    free this lock
    swap buffers
    write to current buffer
    wait for next period

Проблема

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

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

Проблема заключается в задержке, которую я хочу минимизировать.Представьте себе случай 1:

writer writes to buffer0                reader is reading buffer1
writer can't acquire lock1              because reader is still reading buffer1
|                                       |
|                                       reader finishes reading,
| (writer waiting for next period)      <- **this point**
|
|
writer wakes up, and again writes to buffer0

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

Случай 2 аналогичен:

writer writes to buffer0                reader is idle
|                                       |
|                                       reader finishes reading,
| (writer waiting for next period)
|
|                                       reader starts reading buffer1
writer wakes up                         |
it can't acquire lock0                  because reader is still reading buffer1
overwrites buffer0

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

Writer (case 3):
acquire lock 0                        
loop
    if last buffer swap failed
        acquire other lock
        free this lock
        swap buffers
    write to current buffer
    acquire other lock
    free this lock
    swap buffers
    wait for next period

Теперь проблема с задержкой сохраняется:

writer writes to buffer0                reader is reading buffer1
writer can't acquire lock1              because reader is still reading buffer1
|                                       |
|                                       reader finishes reading,
| (writer waiting for next period)      <- **this point**
|
|
writer wakes up
swaps buffers
writes to buffer1

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

Вопрос

Вопрос в том, как мне справиться с этим?Если я хочу, чтобы записывающее устройство выполнялось точно в нужный период, ему нужно ждать период, используя функцию RTAI, и я не могу сделать это, как

Writer (case 4):
acquire lock 0                        
loop
    write to current buffer
    loop a few times or until the buffer has been swapped
        sleep a little
        acquire other lock
        free this lock
        swap buffers
    wait for next period

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

Просто чтобы быть более ясным, вот что я хочу сделать:

writer writes to buffer0                reader is reading buffer1
|                                       |
|                                       reader finishes reading,
| (writer waiting for next period)      As soon as all readers finish reading,
|                                         the buffer is swapped
|                                       readers start reading buffer0
writer wakes up                         |
writes to buffer1

Что я нашел уже

Я нашел read-copy-update , который, насколько я понял, продолжает выделять память для буферов и освобождает их до тех пор, пока читатели не закончатс ними, что невозможно для меня по многим причинам.Во-первых, потоки распределяются между ядром и пользовательским пространством.Во-вторых, с помощью RTAI вы не можете распределять память в потоке реального времени (потому что тогда ваш поток будет вызывать системные вызовы Linux и, следовательно, нарушать его в реальном времени! (Не говоря уже о том, что использование собственной реализации RCU в Linux бесполезно из-запо тем же причинам)

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

Ответы [ 5 ]

3 голосов
/ 10 ноября 2011

Какой API вы используете для блокировки чтения-записи?Есть ли у вас временная блокировка, например pthread_rwlock_timedwrlock ?Если да, я думаю, что это решение вашей проблемы, как в следующем коде:

void *buf[2];

void
writer ()
{
  int lock = 0, next = 1;

  write_lock (lock);
  while (1)
    {
      abs_time tm = now() + period;

      fill (buf [lock]);
      if (timed_write_lock (next, tm))
        {
          unlock (lock);
          lock = next;
          next = (next + 1) & 1;
        }
      wait_period (tm);
    }
}


void
reader ()
{
  int lock = 0;
  while (1)
    {
      reade_lock (lock);
      process (buf [lock]);
      unlock (lock);
      lock = (lock + 1) & 1;
    }
}

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

1 голос
/ 18 октября 2011
  • Использовать очередь (связанный список FIFO)
  • Оператор записи в реальном времени всегда добавляет (ставит в очередь) конец очереди
  • Считыватели всегда удаляют (удаляют) из начала очереди
  • Считыватели будут блокироваться, если очередь пуста

изменить, чтобы избежать динамического выделения

Я бы, наверное, использовал круговую очередь ... Я бы использовал встроенные __sync атомарные операции. http://gcc.gnu.org/onlinedocs/gcc-4.1.0/gcc/Atomic-Builtins.html#Atomic-Builtins

  • Круговая очередь (массив FIFO 2d)
    • например: byte [] [] Array = новый байт [MAX_SIZE] [BUFFER_SIZE];
    • Указатели начала и конца индекса
  • Writer перезаписывает буфер в массиве [End] []
    • Writer может увеличивать Start, если он завершает цикл вокруг
  • Считыватель получает буфер из массива [Пуск] []
    • Считыватель блокируется, если Start == End
1 голос
/ 18 октября 2011

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

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

Читатели должны быть готовы пропустить что-то время от времени, и должны быть в состоянии обнаружить, когда они пропустили что-то. Я бы связал флаг достоверности и длинный счетчик последовательностей с каждым буфером, а писатель сделал что-то вроде «очистить флаг достоверности, увеличить число последовательностей, синхронизировать, записать в буфер, увеличить число последовательностей, установить флаг действительности, синхронизировать». Если считыватель читает счетчик последовательностей, синхронизирует, видит флаг достоверности true, считывает данные, синхронизирует и повторно считывает тот же счетчик последовательностей, то, возможно, есть некоторая надежда, что он не получил искаженные данные.

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

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

См. Также поиск по алгоритмам без блокировки, таким как http://www.rossbencina.com/code/lockfree

Чтобы пойти с этим, вы, вероятно, хотите, чтобы писатель дал сигнал спящим читателям. Для этого вы можете использовать семафоры Posix - например, Пусть читатель попросит автора вызвать sem_post () для определенного семафора, когда он достигнет заданного порядкового номера или когда буфер станет действительным.

1 голос
/ 18 октября 2011

Разве это не та проблема, которую должна решить тройная буферизация? Итак, у вас есть 3 буфера, давайте назовем их write1, write2 и read. Поток записи чередуется между записью в write1 и write2, гарантируя, что они никогда не блокируются, и что последний полный кадр всегда доступен. Затем в потоках чтения, в некоторой подходящей точке (скажем, непосредственно перед или после чтения кадра), буфер чтения переключается с доступным буфером записи.

Хотя это гарантирует, что записывающие устройства никогда не блокируют (переворачивание буфера можно сделать очень быстро, просто переключив два указателя, возможно, даже с атомарным CAS вместо блокировки), все еще остается проблема читателей, ожидающих других считыватели, чтобы закончить с буфером чтения перед переключением. Я полагаю, что это можно решить слегка RCU-esque с помощью пула буферов чтения, где можно перевернуть доступный.

0 голосов
/ 19 октября 2011

Другой вариант - придерживаться блокировки, но убедитесь, что читатели никогда не зависают слишком долго, удерживая блокировку.Читатели могут сохранять время, затрачиваемое на удержание блокировки, коротким и предсказуемым, не делая ничего другого, пока они удерживают эту блокировку, кроме копирования данных из буфера записывающего устройства.Тогда единственная проблема заключается в том, что считыватель с низким приоритетом может быть прерван задачей с более высоким приоритетом на полпути после записи, и лекарство для этого будет http://en.wikipedia.org/wiki/Priority_ceiling_protocol.

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

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

...