2 потока, чтение-запись в общий буфер
У меня есть часть кода, которая выполняет 2 функции:
- Захватывает кадр с экрана монитора.
- Сжимает этот кадр с помощью ffmpeg и пишет в файл.
Так как и 1 и 2 операции занимают время, а 2 уж намного больше, то я решил их разнести по 2 разным потокам, чтоб они не ждали друг друга.
- Сделать общий буфер, например, на 8 кадров (Да можно всего лишь и на 2-3),
- Поток 1 получает кадр и пишет его в общий буфер.
- Поток 2 читает буфер и берет оттуда кадр.
- Поток 1 пишет всегда по кругу, если конец, то начинает писать сначала. Главное, он не может перезаписывать кадр, к которому получил доступ Поток 2.
- Поток 1 не ждёт когда обработаются все кадры в буфере, он пишет поверх старых. То есть в итоге кадры могут пропускаться.
- Общий буфер не должен блокироваться, если к нему получил доступ какой-либо из потоков. То есть он всегда доступен обоим потокам.
Как это реализовать? Кольцевой неблокируемый буфер, как?
Ответы (3 шт):
Сделайте у каждого кадра в буфере свой mutex и индикатор состояния, принимающий значения -- "пусто" и "заполнен". Сначала все кадры находятся в состоянии "пусто". Также храните вместе с индикатором состояния последовательный номер, вырабатываемый Потоком 1 при заполнении кадра.
Поток 1 бежит по кольцу кадров и всегда делает только pthread_mutex_trylock для захвата кадра. Найдя кадр в состоянии "пусто", заполняет его, присваивает этим данным новый последовательный номер и оставляет в состоянии "заполнен" (естественно, снимает блокировку с кадра).
Если он не нашел свободного кадра в буфере, то он захватывает и переписывает новыми данными самый старый еще не обработанный кадр.
Поток 2 тоже в основном делает pthread_mutex_tryloc(), найдя заполненный буфер обрабатывает его и переводит в состояние "пусто". Он никогда не должен обрабатывать кадр с последовательным номером меньшим или равным последнему, который он обработал. Если захватили кадр с таким номером, просто освобождайте lock и ищите следующий кадр, переводя этот в состояние "пусто".
Если Поток 2 прошел полный круг без работы, то поняв, что очередной кадр заполняется потоком 1 (т.е. pthread_mutex_trylock() вернул не 0 и errno = EBUSY), он вызывает pthread_mutex_lock и таким образом ждет завершения заполнения кадра, не потребляя впустую такты процессора.
Получив в конце-концов lock на этот кадр, он его обрабатывает и опять начинает круг с trylock().
Как-то так я вижу возможный алгоритм.
Не имеет смысла разносить две взаимоувязанные операции в разные потоки в данном случае, постольку, поскольку во время сжатия кадра вы как раз занимаетесь пропуском других кадров, а сам процесс снятия скриншота будет только отнимать ресурсы ЦП, который и так нагружен сжатием.
Лучше подумать над ускорением самого процесса как снятия скриншотов, так и сжатия. В идеале - использовать аппаратный захват и аппаратное же сжатие, без передачи полного кадра в RAM. С последним может быть туго - разные производители имеют разные API для этого.
На Linux c Intel HD Graphics такой подход дает около 90 FPS, что лично я считаю хорошим результатом.
Такие задачи встречаются очень часто. Общее их название "Задача производитель/потребитель". По английски "Produser/Consumer". Для решения этих задач разработан алгоритм, который называется "Алгоритм кольцевого буфера". Это абсолютно стандартная вещь, проверенная в тысячах приложений. Обычно используются кольцевые буферы с размером кратным степени 2.
Алгоритм этот описан во многих местах. Например, тут: http://cdeblog.ru/simple-ring-buffer В этой статье не описан режим блокировки буфера через мьютексы, но он достаточно очевиден: охранять мьютексами нужно не сам буфер, а индексы чтения и записи.