2 потока, чтение-запись в общий буфер

У меня есть часть кода, которая выполняет 2 функции:

  1. Захватывает кадр с экрана монитора.
  2. Сжимает этот кадр с помощью ffmpeg и пишет в файл.

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

  1. Сделать общий буфер, например, на 8 кадров (Да можно всего лишь и на 2-3),
  2. Поток 1 получает кадр и пишет его в общий буфер.
  3. Поток 2 читает буфер и берет оттуда кадр.
  4. Поток 1 пишет всегда по кругу, если конец, то начинает писать сначала. Главное, он не может перезаписывать кадр, к которому получил доступ Поток 2.
  5. Поток 1 не ждёт когда обработаются все кадры в буфере, он пишет поверх старых. То есть в итоге кадры могут пропускаться.
  6. Общий буфер не должен блокироваться, если к нему получил доступ какой-либо из потоков. То есть он всегда доступен обоим потокам.

Как это реализовать? Кольцевой неблокируемый буфер, как?


Ответы (3 шт):

Автор решения: avp

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

Поток 1 бежит по кольцу кадров и всегда делает только pthread_mutex_trylock для захвата кадра. Найдя кадр в состоянии "пусто", заполняет его, присваивает этим данным новый последовательный номер и оставляет в состоянии "заполнен" (естественно, снимает блокировку с кадра).

Если он не нашел свободного кадра в буфере, то он захватывает и переписывает новыми данными самый старый еще не обработанный кадр.

Поток 2 тоже в основном делает pthread_mutex_tryloc(), найдя заполненный буфер обрабатывает его и переводит в состояние "пусто". Он никогда не должен обрабатывать кадр с последовательным номером меньшим или равным последнему, который он обработал. Если захватили кадр с таким номером, просто освобождайте lock и ищите следующий кадр, переводя этот в состояние "пусто".

Если Поток 2 прошел полный круг без работы, то поняв, что очередной кадр заполняется потоком 1 (т.е. pthread_mutex_trylock() вернул не 0 и errno = EBUSY), он вызывает pthread_mutex_lock и таким образом ждет завершения заполнения кадра, не потребляя впустую такты процессора. Получив в конце-концов lock на этот кадр, он его обрабатывает и опять начинает круг с trylock().

Как-то так я вижу возможный алгоритм.

→ Ссылка
Автор решения: gbg

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

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

На Linux c Intel HD Graphics такой подход дает около 90 FPS, что лично я считаю хорошим результатом.

→ Ссылка
Автор решения: Sergey

Такие задачи встречаются очень часто. Общее их название "Задача производитель/потребитель". По английски "Produser/Consumer". Для решения этих задач разработан алгоритм, который называется "Алгоритм кольцевого буфера". Это абсолютно стандартная вещь, проверенная в тысячах приложений. Обычно используются кольцевые буферы с размером кратным степени 2.

Алгоритм этот описан во многих местах. Например, тут: http://cdeblog.ru/simple-ring-buffer В этой статье не описан режим блокировки буфера через мьютексы, но он достаточно очевиден: охранять мьютексами нужно не сам буфер, а индексы чтения и записи.

→ Ссылка