Пример использования QtConcurrent

Qt Script

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

Постановка задачи: обесцвечивание изображения

Предположим, что у нас есть обычное изображение QImage. И мы хотим сделать его черно-белым. К сожалению, на момент написания заметки в стандартной библиотеке Qt такой возможности еще не предусмотрено. Но не расстраивайтесь. Ее достаточно легко реализовать.

Однопоточная реализация

Начнем с простой однопоточной версии:

Попробуем разобраться с тем, как работает этот код. Первым делом мы определяем вектор colorTable, который заполняется в начале функции main(). Он просто содержит 256 оттенков серого расположенных по порядку от черного qRgb( 0, 0, 0 ) до белого qRgb( 255, 255, 255 ). Далее следует определение структуры GrayscaleTaskInput. Признаюсь, что это заготовка для перехода к многопоточной реализации. В этой структуре содержится исходное изображение, которое мы хотим сделать черно-белым; диапазоны координат x и y, для которых требуется произвести обесцвечивание; и наконец указатель на выходное изображение, в который будем записывать черно-белые пиксели.

Объект структуры GrayscaleTaskInput ожидает получить функция makeRegionGrayscale(). Она и будет делать всю основную работу. Здесь мы в двойном цикле проходим по каждому пикселю из указанного диапазона. Чтобы получить более высокую скорость работы для доступа к пикселям, мы используем scanLine(). То есть работаем напрямую с буфером в памяти, в котором хранится изображение. Мы также могли воспользоваться функцией-членом setPixel(), но она менее эффективна и в среднем может замедлить скорость работы в два раза. Поэтому в цикле для каждой строки выходного изображения destImg мы запрашиваем указатель на буфер. Считываем значение пикселя на позиции (x; y) для исходного изображения srcImg с помощью функции-члена pixel(). Затем получаем указатель на пиксель выходного изображения на позиции x в нашем буфере. А потом просто переводим пиксель исходного изображения в градации серого с помощью qGray() и полученное значение присваиваем пикселю выходного изображения.

Однопоточный пример, который использует функцию makeRegionGrayscale(), выглядит довольно просто. В первой строке мы создаем изображение с размером, равным исходному изображению. Обратите внимание на формат Format_Indexed8, который мы передали в конструктор объекта grayImg. Он указывает на то, что на каждый пиксель будет приходиться по 1 байту, то есть 8 бит. И этот 1 байт мы индексируем с помощью нашей цветовой таблицы colorTable, которую определили немного выше. После этого нам остается лишь вызывать makeRegionGrayscale() с соответствующими параметрами и вернуть получившееся черно-белое изображение.

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

Замер производительности

Чтобы сравнить однопоточную и многопточную (скоро мы до нее доберемся) реализации, напишем простенький бенчмарк для оценки времени работы функций:

Функция benchmark() использует шаблоны с переменным числом параметров, поэтому не забудьте включить режим c++11, если ваш компилятор этого требует. Про шаблоны с переменным числом параметров я достаточно подробно рассказал в заметке, посвященной паттерну Наблюдатель.

На вход benchmark() принимает 3 параметра: количество итераций iterCount, которые нужно сделать для составления статистики; функцию func(), производительность которой мы собрались замерять; аргументы args, которые нужно передать функции func(). Реализация этой шаблонной функции весьма тривиальна. В цикле мы запускаем iterCount раз функцию func() с аргументами args. Для каждого запуска мы делаем замер времени с помощью объекта QTime. Результаты каждого замера заносятся в вектор. Затем для вектора с замерами времени мы определяем минимальную статистику: находим максимальное, минимальное и среднее время выполнения func(). И выводим эти результаты на консоль.

Для convertImageToGrayscaleEasy() вызов бенчмарка имеет следующий вид:

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

В среднем около 0,57 секунд. Не так уж плохо. Посмотрим, что можно выжать из моего компьютера, задействовав потоки.

Многопоточная реализация на основе [crayon-662aac65e983c473459892-i/]

Посмотрим на «быстрое» решение нашей задачи:

Начало функции ничем не отличается от того, что мы уже видели в однопоточной реализации. Создаем новое изображение, задаем его формат и таблицу цветов. А вот дальше уже начинаются некоторые отличия. Изображение мы делим на горизонтальные полосы. Количество полос будет совпадать с количеством потоков THREAD_COUNT, значение которого мы делаем по умолчанию равным QThread::idealThreadCount(). Функция idealThreadCount() просто возвращает количество ядер процессора, что является наиболее логичным вариантом при выборе числа потоков. Затем мы создаем вектор, в который будем собирать объекты GrayscaleTaskInput для каждой полосы, кроме последней. Последнюю полосу мы обрабатываем в вызывающем потоке. Но обратите внимание, что перед вызовом makeRegionGrayscale() для крайней полосы, мы сделали следующее:

Функция map() для каждого элемента вектора tasks запускает функцию makeRegionGrayscale() в своем потоке. Тогда чтобы дождаться результата и вернуть законченное черно-белое изображение, мы принимаем возвращаемый объект QFuture< void >. А перед тем, как вернуть изображение, вызываем waitForFinished(), чтобы гарантировать, что каждая полоса изображения обработана.

Таким образом, основная идея использования QtConcurrent заключается в простом выполнении асинхронных действий для синхронных задач. То есть мы разбиваем сложную задачу на части и отправляем их на обработку в разные потоки. Но не возвращаем управление вызывающей подпрограмме сразу, а ждем, пока все потоки не закончат свою работу. Рассмотренный выше пример является хорошей демонстрацией этого принципа. Но что там у нас со скоростью работу? Пора добавлять бенчмарк:

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

В среднем около 0,14 секунд. Конечно, не мгновенно, но по сравнению с простой однопоточной реализацией это и правда быстро. Мы увеличили скорость работы алгоритма более, чем в 4 раза.

Заключение

Вот мы и посмотрели на типичный пример использования QtConcurrent для реализации многопоточного кода. Такой подход имеет свои преимущества и недостатки. С одной стороны, для слишком большого объема входных данных вызов подобных функций может привести к «тормозам» в GUI. Но во многих случаях простота использования перекрывает этот недостаток, который может никогда и не всплыть. Так нам достаточно реализовать одну функцию и без забот ее использовать, не думая о внутренней многопоточной природе. Но для реализации сложного асинхронного взаимодействия потоков нам придется использовать сигналы и слоты, либо какие-то еще более заумные схемы на базе очередей, мьютексов и прочей гадости. Но все зависит от задачи, которая перед вами стоит. В каких-то ситуациях одно решение окажется идеальным, а где-то просто не подойдет.

Понравилась статья? Поделиться с друзьями:
Добавить комментарий