thread rendering что это

Про оптимизацию рендеринга — с оптимизмом

У меня есть мечта, и она утопична: я хочу, чтобы мои веб-приложения работали идеально. JQuery, AngularJs, React, Vue.js — все обещают производительность. Но проблема совсем не во фреймворках и не в JavaScript. Проблема в том, как браузер рендерит страницу. А делает он это очень плохо.

Если бы браузер отлично справлялся с рендерингом, то не появился бы такой инструмент, как React Native. Под капотом React Native всё тот же JavaScript, а View нативное, и разница в производительности между нативным приложением и приложением на React Native не будет заметна для рядового пользователя. Другими словами, проблема не в JavaScript.

Если что-то оптимизировать, то как раз рендеринг. Инструментов, которые нам даёт JavaScript и API браузера, недостаточно. Два года я пытаюсь сделать работу своих продуктов плавной и быстрой, но тщетно. Я почти смирился с тем, что веб останется таким навсегда. В этой статье я собрал всё, что успел узнать об оптимизации рендеринга и применить на проектах, над которыми работал, и рассказываю о своих надеждах на ближайшее будущее. Это будущее, в котором я хочу опираться на устойчивый фундамент стандартов и API браузера, а не CSS-хаки и third-party репозитории для оптимизации производительности.

thread rendering что это. Смотреть фото thread rendering что это. Смотреть картинку thread rendering что это. Картинка про thread rendering что это. Фото thread rendering что это

Гибридные приложения и производительность

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

В разработке гибридных приложений хорошо. JavaScript меня полностью устраивал, полностью устраивали меня и элементы интерфейса, которые щедро предоставляли фреймворки Framework7 и Ionic. Даже плагинов, позволяющих пользоваться нативными функциями, хватало. Пиши одно приложение и получай сразу десять — под все платформы, какие только придумали. Мечта да и только. Но сейчас будет «но», жирное и ставящее крест на всём.

Как только приложение становится заметно сложнее, чем “Hello, world!”, начинаются проблемы с производительностью. Приложение работает лучше, чем мобильная версия сайта, но далеко не так хорошо, как аналогичное нативное приложение.

thread rendering что это. Смотреть фото thread rendering что это. Смотреть картинку thread rendering что это. Картинка про thread rendering что это. Фото thread rendering что это

Если кто-то и готов с этим мириться, то для меня это стало вызовом. Мне нужно было написать гибридное приложение так, чтобы его невозможно было отличить от нативного. Тогда я чуть-чуть покопался и пришёл к одному простому выводу: с js всё хорошо, проблема в рендеринге. Я перепробовал всё css-хаки от “transform: translate3d(0,0,0)” (который вскоре перестал работать) и до замены градиентов png с альфа-каналом. Это, конечно же, не решало проблему, а лишь чуть маскировало её. По ссылкам несколько таких хаков:

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

Medium, у вас проблемы

На сайтах и в приложениях мы видим бесконечные ленты: Instagram, Facebook, Twitter, Medium — из этих примеров, пожалуй, можно составить свою ленту с подгрузкой. И в этом нет ничего плохого. Скролл позволяет перемещаться в пределах одного поста и перемещаться между постами. Можно скроллить быстро, можно медленно. Добавляешь новые элементы в список сколько душе угодно. Я и сам так делал.

thread rendering что это. Смотреть фото thread rendering что это. Смотреть картинку thread rendering что это. Картинка про thread rendering что это. Фото thread rendering что это

Давайте проведем эксперимент. У вас шумный кулер? Откройте Medium.com и мотайте вниз. Как скоро ваш кулер выйдет на максимальные обороты? Мой результат — примерно 45 секунд. И это не Chrome виноват. И даже не то, что моему ноутбуку много лет. Проблема в том, что никто не занимается оптимизацией того, что мы видим во viewport.

Что же происходит, когда мы мотаем ленту? Оказавшись внизу страницы, мы получаем от сервера ещё немного постов, и они добавляются в конец списка. Список растёт бесконечно. Что делает браузер? Ничего. Посты в начале ленты всё ещё существуют и браузер всё еще рендерит их. И “visibility: hidden” тут никак не поможет, даже если мы будем вешать это свойство на каждый пост, который находится за пределами viewport’а. Кстати, такая бесполезная оптимизация была замечена мною в Ionic. Серьезно. Но потом это исправили. Если кому интересно, вот тема на форуме Ionic, которую я создал, чтобы обсудить проблему.

Загадочный мир оптимизации

thread rendering что это. Смотреть фото thread rendering что это. Смотреть картинку thread rendering что это. Картинка про thread rendering что это. Фото thread rendering что это

Что же мешает писать хороший, оптимизированный код? Мешает то, что мы не так много знаем об этом процессе. Большая часть знаний пришла к нам методом проб и ошибок, а статьи с заголовком вроде «Как браузер рендерит страницу» рассказывают нам о том, как HTML совмещается с CSS и как страница разбивается на слои. Мне непонятно, что происходит, когда я добавляю в DOM новый элемент или добавляю элементу новый класс. Какие элементы при этом пройдут пересчёт и рендеринг?

Вот мы добавим новый элемент в список. Что дальше?

Рендеринг в браузере работает иначе. Вот одна из массы статей на эту тему, где автор рассказывает о процессе совмещения DOM-дерева и CSS-дерева и о том, как браузер впоследствии рисует полученную конструкцию. Всё очень здорово, однако не совсем ясно, что может сделать разработчик, чтобы помочь браузеру. Есть неофициальный ресурс CSS Triggers, на котором представлена таблица, позволяющая определить, какие CSS-свойства вызывают Layout/Paint/Composite-процессы в браузере, но так как страницы обычно содержат большое количество стилей и элементов, единственный разумный выход — это постараться исключить всё, что может ударить по производительности.

В общих чертах оптимизация состоит из нескольких пунктов:

Отсечение лишнего

thread rendering что это. Смотреть фото thread rendering что это. Смотреть картинку thread rendering что это. Картинка про thread rendering что это. Фото thread rendering что это

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

Многие знают про Virtual list/Virtual scroll/Grid View/Table View. Названия разные, но суть одна: это компонент для эффективного отображения очень длинных списков на странице. В основном подобные интерфейсные компоненты используют в мобильной разработке.

GitHub полон js-репозиториев а-ля virtual list, virtual grid и т.д. Оптимизация вполне рабочая, это факт. В списке из 10 тысяч элементов можно создать контейнер длинной в 10 000 px, умноженных на высоту одного элемента, далее следить за скроллом и рендерить только видимые пользователю элементы плюс ещё чуть-чуть. Сами элементы позиционируются при помощи “translate: transformY( * px)”. Я недавно изучал Vue.js 2.0 и написал такой компонент за пару часов.

Есть несколько вариантов реализации, и разница между ними лишь в том, как мы позиционируем элементы и разбиваем ли их на группы, но это не столь важно. Проблема в том, что событие scroll при идеальных условиях срабатывает ровно столько раз, сколько пикселей было проскроллено. То есть очень много. Добавьте к этому необходимость производить вычисления при каждом срабатывании события. На мобильных устройствах событие scroll и сама механика scroll работает по-разному. Вывод из этого следует довольно простой: scroll event плохо подходит для таких задач.

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

IntersectionObserver

thread rendering что это. Смотреть фото thread rendering что это. Смотреть картинку thread rendering что это. Картинка про thread rendering что это. Фото thread rendering что это

Вот теперь на сцену выходит IntersectionObserver, первый луч надежды, упавший на описанное мной во вступлении будущее. Фича новая, поэтому вот информация о поддержке браузерами на сайте caniuse.com. А вот немного материалов по ней:

Теперь немного практики. Мне захотелось сделать виртуальный скролл с подгрузкой элементов, используя IntersectionObserver. Задача примерно такая:

Принцип работы

Контент разбивается на части по 12 постов каждая. Когда компонент инициализировался, в DOM находится только одна такая часть. Первая и последняя часть имеют скрытый элемент вверху и внизу соответственно. За видимостью этих элементов мы следим. Когда какой-то из этих элементов попадает на экран, мы добавляем новую часть и удаляем уже ненужную. Таким образом мы имеем в DOM две части по 12 постов одновременно.

Чем это удобнее отслеживания скролла? Если высота постов неизвестна, приходится искать элемент в DOM и узнавать её. Это не очень удобно и не очень производительно.

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

Меньше слов больше дела: вот демо. И я прошу обратить внимание на то, что цель здесь — увидеть работу IntersectionObserver на реальном примере.

Вот тут можно посмотреть на FPS, если скроллить со скоростью беглого просмотра ленты (изображение кликабельно):

thread rendering что это. Смотреть фото thread rendering что это. Смотреть картинку thread rendering что это. Картинка про thread rendering что это. Фото thread rendering что это

И максимально быстро (изображение кликабельно):

thread rendering что это. Смотреть фото thread rendering что это. Смотреть картинку thread rendering что это. Картинка про thread rendering что это. Фото thread rendering что это

FPS очень редко падает ниже 60, но всего на пару кадров и не ниже 45. Хороший результат, учитывая то, что браузер не знает размер картинок и текста заранее.

Источник

Рендеринг в веб

thread rendering что это. Смотреть фото thread rendering что это. Смотреть картинку thread rendering что это. Картинка про thread rendering что это. Фото thread rendering что этоо переводе

Наше понимание в этой области основано на нашей работе с Chrome, и контактировании с большими сайтами в течение последних нескольких лет. В общем, мы хотим вдохновить разработчиков рассмотреть использование серверного рендеринга или статического рендеринга с полноценной регидратацией.

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

Терминология

Рендеринг

Rehydration (регидратация): «загрузка» JavaScript отображениий на клиенте таким образом, чтобы они повторно использовали отрендеренное на сервере DOM-дерево и данные HTML-а

Prerendering (пре-рендеринг): выполнение клиентского приложения во время сборки для захвата его начального состояния в виде статического HTML.

Performance

Server Rendering (Серверный рендеринг)

Серверный рендеринг генерирует полный HTML страницы на сервере в ответ на навигацию. Это позволяет избежать дополнительных проходов для получения данных и шаблонов на клиенте, так как это выполняется до того, как браузер получает ответ..

Серверный рендеринг обычно даёт быстрый First Paint (FP) и First Contentful Paint (FCP). Выполнение логики страницы и её рендеринг на сервере позволяют избежать отправки большого количества JavaScript клиенту, что помогает достичь быстрого Time to Interactive (TTI). Это имеет смысл потому, что при серверном рендеринге вы на самом деле просто посылаете текст и ссылки в браузер пользователя. Такой подход может хорошо работать для широкого спектра устройств и сетевых условий и открывает интересные возможности для оптимизации браузера, например можно выполнять разбор потоковых (streaming) документов.

thread rendering что это. Смотреть фото thread rendering что это. Смотреть картинку thread rendering что это. Картинка про thread rendering что это. Фото thread rendering что это

При серверном рендеринге пользователи вряд ли будут вынуждены ждать, пока CPU-зависимый JavaScript будет выполнен, прежде чем они смогут использовать ваш сайт. Даже когда стороннего JS не избежать, использование серверного рендеринга для уменьшения собственных JS costs (JS затрат) может дать вам больше «budget» (бюжета) для остального. Однако, есть один основной недостаток такого подхода: генерация страниц на сервере занимает время, что часто может привести к замедлению Time to First Byte (TTFB).

Достаточно ли серверного рендеринга для вашего приложения, во многом зависит от того, какое приложение вы строите. Существует давняя дискуссия о правильности применения серверного рендеринга вместо клиентского рендеринга, но важно помнить, что вы можете использовать серверный рендеринг для одних страниц, а для других нет. Некоторые сайты с успехом переняли гибридный рендеринг. Netflix делает серверный рендеринг своих относительно статических страниц, в то время как делает prefetching JS для страниц с тяжелым взаимодействием, давая этим более тяжелым отрендеренным на клиенте страницам больше шансов на быструю загрузку.

Многие современные фреймворки, библиотеки и архитектуры позволяют отрисовывать одно и то же приложение как на клиенте, так и на сервере. Эти инструменты могут быть использованы для Server Rendering, однако важно отметить, что архитектуры, где рендеринг происходит как на сервере, так и на клиенте, являются собственным классом решений с очень различными характеристиками производительности и компромисами. React пользователи могут использовать для серверного рендеринга renderToString() или решения, построенные на нем, такие как Next.js. Пользователи Vue могут ознакомиться с руководством по серверному рендерингу Vue или познакомиться с Nuxt. В Angular есть Universal. Однако большинство популярных решений используют ту или иную форму гидратации (hydration), поэтому перед выбором инструмента следует ознакомиться с используемыми подходами.

Static Rendering (Статический рендеринг)

thread rendering что это. Смотреть фото thread rendering что это. Смотреть картинку thread rendering что это. Картинка про thread rendering что это. Фото thread rendering что это

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

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

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

Серверный рендеринг против статического

Серверный рендеринг генерирует HTML по требованию для каждого URL, но это может быть медленнее, чем просто обслуживание статически отрендереного контента. Если вы готовы сделать дополнительные усилия, то серверный рендеринг + [HTML кеширование] (https://freecontent.manning.com/caching-in-react/) может значительно сократить время серверного рендеринга. Положительной стороной серверного рендеринга является возможность получать более «живые» данные и отвечать на более полный набор запросов, чем это возможно при статическом рендеринге. Страницы, требующие персонализации, являются хорошим примером типа запроса, который плохо работает со статическим рендерингом.

Серверный рендеринг также может представлять интересные решения при построении PWA. Лучше ли использовать full-page service worker кеширование, или просто рендерить на сервере отдельные фрагменты контента?

Client-Side Rendering (CSR)

Рендеринг на стороне клиента (CSR) означает рендеринг страниц непосредственно в браузере с использованием JavaScript. Вся логика, сбор данных, шаблонирование и маршрутизация обрабатываются на клиенте, а не на сервере.

thread rendering что это. Смотреть фото thread rendering что это. Смотреть картинку thread rendering что это. Картинка про thread rendering что это. Фото thread rendering что это

Для тех, кто создает одностраничное приложение, определение основных частей пользовательского интерфейса, разделяемого большинством страниц, означает возможность применить технику Application Shell caching. В сочетании с service workers это может драматически повысить воспринимаемую производительность при повторных визитах.

Комбинация серверного рендеринга и клиентского через регидратацию

Часто называемый Universal Rendering или просто «SSR», этот подход пытается сгладить компромиссы клиентского и серверного редеринга, делая и то, и другое. Навигационные запросы, такие как полная загрузка страницы или перезагрузка, обрабатываются сервером, который рендерит приложение в HTML, затем JavaScript и данные, используемые для рендеринга, встраиваются в результирующий документ. При тщательной реализации, это даёт быстрый FCP (First Contentful Paint) такой же, как Server Rendering, а далее «усиливает это» путем рендеринга опять же на клиенте с помощью техники, называемой (re)hydration ((ре)гидратация). Это новое решение, но оно может иметь некоторые существенные недостатки в производительности.

Основной недостаток SSR с регидратацией (rehydration) заключается в том, что она может оказать значительное негативное влияние на TTI (Time To Interactive), даже если она улучшает FP (First Paint). SSR-страницы часто выглядят обманчиво полностью загруженными и интерактивными, но на самом деле не могут реагировать на ввод, пока не будет выполнен JS на стороне клиента и не будут прикреплены обработчики событий. Это может занять секунды или даже минуты на мобильном устройстве.

= Проблема регидратации: Одно приложение по цене двух

Проблемы с регидратацией часто могут быть хуже, чем задержка интерактивности из-за JS. Для того, чтобы JavaScript на стороне клиента мог точно «определить» («pick up») то место, где остановился сервер, без необходимости повторно запрашивать все данные, использованные сервером для рендеринга этого HTML, текущие SSR решения обычно сериализуют ответ из зависимых данных UI в документ в виде тегов script. Полученный HTML-документ содержит высокий уровень дублирования:

thread rendering что это. Смотреть фото thread rendering что это. Смотреть картинку thread rendering что это. Картинка про thread rendering что это. Фото thread rendering что это

Как вы видите, сервер возвращает описание пользовательского интерфейса приложения в ответ на навигационный запрос, но также возвращает исходные данные, использованные для составления этого интерфейса, и полную копию реализации интерфейса, которая затем загружается на клиенте. Только после того, как bundle.js завершит загрузку и выполнение, этот пользовательский интерфейс станет интерактивным.

Показатели производительности, собранные с реальных веб-сайтов, использующих SSR регидратацию, указывают на то, что их использование должно приводить в уныние. В конце концов, причина сводится к Пользовательскому Опыту: очень легко оставить пользователей в «жуткой долине».

thread rendering что это. Смотреть фото thread rendering что это. Смотреть картинку thread rendering что это. Картинка про thread rendering что это. Фото thread rendering что это

Но всё же надежда на SSR с регидратацией есть. В краткосрочной перспективе, только использование SSR для высоко кешируемого содержимого может уменьшить задержку TTFB (Time to First Byte), давая результаты, схожие с пре-рендерингом. Регидратация инкрементальная, прогрессивная или частичная, может быть ключом к тому, чтобы сделать эту технику более жизнеспособной в будущем.

Потоковый серверный рендеринг и прогрессивная регидратация

Серверный рендеринг за последние несколько лет претерпел ряд изменений.

= Частичная регидратация

Частичная регидратация оказалась трудной для осуществления. Этот подход является продолжением идеи прогрессивной регидратации, когда отдельные части (компоненты / виджеты / деревья), подлежащие прогрессивной регидратации, анализируются, а те, которые обладают низкой интерактивностью или не обладают реактивностью помечаются. Для каждой из этих наиболее статических частей соответствующий код JavaScript затем трансформируется в инертные ссылки и декоративную функциональность, уменьшая их влияние на стороне клиента до почти нулевого уровня. Подход, основанный на частичной гидратации, имеет свои собственные проблемы и компромиссы. Он создает некоторые интересные вызовы для кеширования, а навигация на стороне клиента означает, что мы не можем иметь HTML рендерящийся на сервере для инертных частей приложения и доступный без полной загрузки страницы.

= Трисоморфный рендеринг (Trisomorphic Rendering)

Если service workers, являются подходящим вариантом для вас, то «трисоморфный» рендеринг также может быть вам интересен. Это метод, при котором вы можете использовать потоковый серверный рендеринг для начальных/не-JS навигаций, а затем попросить ваш service worker взять на себя рендеринг HTML для навигации после того как он будет смонтирован. Это может поддерживать кешированные компоненты и шаблоны в актуальном состоянии и позволяет использовать навигацию в стиле SPA для рендеринга новых UI-частей в той же сессии. Такой подход лучше всего работает, когда вы можете поделиться одним и тем же шаблоном и кодом маршрутизации между сервером, клиентской страницей и service worker.

thread rendering что это. Смотреть фото thread rendering что это. Смотреть картинку thread rendering что это. Картинка про thread rendering что это. Фото thread rendering что это

SEO соображения

Команды часто учитывают влияние SEO при выборе стратегии для рендеринга в вебе. Серверный рендеринг часто выбирается для обеспечения поисковым роботам возможности лёгкого «полного поиска». Поисковые роботы могут понимать JavaScript, но часто существуют ограничения, о которых стоит знать в части того как они рендерят. Рендеринг на стороне клиента может работать, но часто не без дополнительного тестирования и трудной работы. В последнее время динамический рендеринг также стал вариантом, заслуживающим внимания, если ваша архитектура в значительной степени ориентирована на клиентский JavaScript.

В случае сомнений, инструмент Mobile Friendly Test бесценен для проверки, что выбранный вами подход делает то, что бы вы хотели. Он показывает визуальный предварительный просмотр того, как какую-либо страницу видет поисковый робот Google, сериализованный HTML контент, найденный (после выполнения JavaScript), и любые ошибки, обнаруженные во время рендеринга.

thread rendering что это. Смотреть фото thread rendering что это. Смотреть картинку thread rendering что это. Картинка про thread rendering что это. Фото thread rendering что это

Заключение.

При принятии решения о подходе к рендерингу, измеряйте и понимайте, каковы ваши «узкие места». Подумайте, может ли статический рендеринг или серверный рендеринг дать вам хотя бы 90% возможностей. Совершенно нормально обычно отправлять HTML с минимальным количеством JS, чтобы получить интерактивный опыт. Вот удобная инфографика, показывающая спектр возможностей в разрезе сервер-клиент:

thread rendering что это. Смотреть фото thread rendering что это. Смотреть картинку thread rendering что это. Картинка про thread rendering что это. Фото thread rendering что это

Благодарности

Спасибо всем этим людям за отзывы и вдохновение:
Jeffrey Posnick, Houssein Djirdeh, Shubhie Panicker, Chris Harrelson, and Sebastian Markbåge

Источник

Правильная работа с потоками в Qt

Qt — чрезвычайно мощный и удобный фреймворк для C++. Но у этого удобства есть и обратная сторона: довольно много вещей в Qt происходят скрыто от пользователя. В большинстве случаев соответствующая функциональность в Qt «магически» работает и это приучает пользователя просто принимать эту магию как данность. Однако когда магия все же ломается то распознать и решить неожиданно возникшую на ровном казалось бы месте проблему оказывается чрезвычайно сложно.

Эта статья — попытка систематизации того как в Qt «под капотом» реализована работа с потоками и о некотором количестве неочевидных подводных камней связанных с ограничениями этой модели.

Основы

Давайте начнем с основ. В Qt любые объекты способные работать с сигналами и слотами является наследниками класса QObject. Эти объекты by design являются некопируемыми и логически представляют из себя некоторые индивидуальные сущности которые «разговаривают» друг с другом — реагируют на те или иные события и могут сами генерировать события. Если говорить другими словами, то QObject в Qt реализует паттерн Actors. При правильной реализации любая программа на Qt по сути представляет из себя не более чем сеть взаимодействующих между собой QObject в которых «живет» вся программная логика.

Помимо набора QObject-ов, программа на Qt может включать в себя объекты с данными. Эти объекты не могут генерировать и принимать сигналы, но могут копироваться. К примеру можно сравнить между собой QStringList и QStringListModel. Один из них является QObject и не копируем, но может напрямую взаимодействовать с UI-объектами, другой — это обычный копируемый контейнер для данных. В свою очередь объекты с данными делятся на «Qt Meta-types» и все остальные. К примеру QStringList — это Qt Meta-type, а std::list (без дополнительных телодвижений) — нет. Первые могут использоваться в любом Qt-шном контексте (передаваться через сигналы, лежать в QVariant и т.д.), но требуют специальной процедуры регистрации и наличия у класса публичных деструктора, конструктора копирования и конструктора по умолчанию. Вторые — это произвольные С++-типы.

Плавно переходим к собственно потокам

Итак, у нас есть условные «данные» и есть условый «код» который с ними работает. Но кто на самом деле будет выполнять этот код? В модели Qt ответ на этот вопрос задается явным образом: каждый QObject строго привязан к какому-то потоку QThread который, собственно, и занимается обслуживанием слотов и прочих событий данного объекта. Один поток может обслуживать сразу множество QObject или вообще ни одного, а вот QObject всегда имеет родительский поток и он всегда ровно один. По сути можно считать что каждый QThread «владеет» каким-то набором QObject. В терминологии Qt это называется Thread Affinity. Попробуем для наглядности визуализировать:

thread rendering что это. Смотреть фото thread rendering что это. Смотреть картинку thread rendering что это. Картинка про thread rendering что это. Фото thread rendering что это

Внутри каждого QThread спрятана очередь сообщений адресованных к объектам которыми данный QThread «владеет». В модели Qt предполагается что если мы хотим чтобы QObject сделал какое-либо действие, то мы «посылаем» данному QObject сообщение QEvent:

В этом потоково-безопасном вызове Qt находит QThread которому принадлежит объект receiver, записывает QEvent в очередь сообщений этого потока и при необходимости «будит» этот поток. При этом ожидается что код работающий в данном QThread в какой-то момент после этого прочитает сообщение из очереди и выполнит соответствующее действие. Чтобы это действительно произошло, код в QThread должен войти в цикл обработки событий QEventLoop, создав соответствующий объект и позвав у него либо метод exec(), либо метод processEvents(). Первый вариант входит в бесконечный цикл обработки сообщений (до получения QEventLoop события quit() ), второй ограничивается тем что обрабатывает сообщения ранее накопившиеся в очереди.

thread rendering что это. Смотреть фото thread rendering что это. Смотреть картинку thread rendering что это. Картинка про thread rendering что это. Фото thread rendering что это

Легко видеть что события для всех объектов принадлежащих одному потоку обрабатываются последовательно. Если обработка какого-то события потоком займет много времени, то все остальные объекты окажутся «заморожены» — их события будут накапливаться в очереди потока, но не будут обрабатываться. Чтобы этого не происходило в Qt предусмотрена возможность cooperative multitasking — обработчики событий в любом месте могут «временно прерваться», создав новый QEventLoop и передав в него управление. Поскольку обработчик события до этого тоже был вызван из QEventLoop в потоке при подобном подходе формируется цепочка «вложенных» друг в друга event loop-ов.

Что входит в понятие «события» обрабатываемого в подобном цикле? Хорошо знакомые всем Qt-шникам «сигналы» — это лишь один из частных примеров, QEvent::MetaCall. Подобный QEvent хранит в себе указатель на информацию необходимую для идентификации функции (слота) которую нужно позвать и ее аргументов. Однако помимо сигналов в Qt существует еще порядка сотни (!) других событий, из которых десяток зарезервирован за специальными Qt-шными событиями (ChildAdded, DeferredDelete, ParentChange) а остальные соответствуют различным сообщениям от операционной системы.

Одним из неочевидных подводных камней здесь является то что в Qt у потока, вообще говоря, может вообще не быть Dispatcher-а и соответственно ни одного EventLoop-а. Объекты принадлежащие этому потоку не будут реагировать на посылаемые им события. Поскольку QThread::run() по умолчанию вызывает QThread::exec() внутри которого как раз реализован стандартный EventLoop, то с этой проблемой часто сталкиваются те кто пытается определить свою собственную версию run() отнаследовавшись от QThread-а. Подобный вариант использования QThread в принципе вполне валиден и даже рекомендуется в документации, но идет вразрез с общей идеей организации кода в Qt описанной выше и частенько работает не так как ожидают того пользователи. Характерной ошибкой при этом является попытка останавливать подобный кастомный QThread путем вызова QThread::exit() или quit(). Обе эти функции направляют сообщение в QEventLoop, но если в потоке этого QEventLoop просто нет, то и обрабатывать их, естественно, некому. В результате неопытные пользователи пытаясь «починить неработающий класс» начинают пытаться использовать «работающий» QThread::terminate, чего делать категорически нельзя. Имейте в виду — в случае если Вы переопределяете run() и не используете стандартный event loop, то механизм выхода из потока придется предусмотреть самостоятельно — к примеру воспользовавшись специально добавленной для этого функцией QThread::requestInterruption(). Правильнее, впрочем, просто не наследоваться от QThread если Вы не собираетесь действительно реализовывать какую-то специальную новую разновидность потоков и либо использовать специально созданный для подобных сценариев QtConcurrent, либо поместить логику в специальный Worker Object отнаследованный от QObject, поместить последний в стандартный QThread и управлять Worker-ом с помощью сигналов.

Thread affinity, инициализация и их ограничения

Итак, как мы уже разобрались, каждый объект в Qt «принадлежит» какому-то потоку. При этом встает закономерный вопрос: а какому, собственно говоря, именно? В Qt приняты следующие соглашения:

1. Все «дети» любого «родителя» всегда живут в том же потоке что и родительский объект

Это пожалуй самое сильное ограничение потоковой модели Qt и попытки его нарушить нередко дают весьма странные для пользователя результаты. К примеру попытка сделать setParent к объекту живущему в другом потоке в Qt просто молча фейлится (в консоль пишется предупреждение). На этот компромисс по всей видимости пошли из-за того что потоковобезопасное удаление «детей» при гибели живущего в другом потоке родителя является очень нетривиальной и склонной к трудно отлавливаемым багам проблемой. Хотите реализовывать иерархию взаимодействующих объектов живущих в разных потоках — придется организовывать удаление самостоятельно.

2. Объект у которого при создании не указан родитель живет в потоке который его создал

Тут все одновременно и просто и в то же время не всегда очевидно. К примеру в силу этого правила QThread (как объект) живет в другом потоке чем собственно тот поток который он контролирует (и в силу правила 1 не может владеть ни одним из объектов созданных в этом потоке). Или, скажем, если Вы переопределите QThread::run и будете внутри создавать какие-либо наследники QObject, то без принятия специальных мер (как разбиралось в предыдущей главе) созданные объекты не будут реагировать на сигналы.

Thread affinity при необходимости можно менять вызовом QObject::moveToThread. В силу правила 1, перемещать можно только верхнеуровневых «родителей» (у которых parent==null), попытка переместить любого «ребенка» будет молча проигнорирована. При перемещении верхнеуровневого «родителя» все его «дети» тоже переедут в новый поток. Любопытно что вызов moveToThread(nullptr) тоже легален и является способом создать объект с «null»-овой thread affinity; подобные объекты не могут получать никаких сообщений.

Получить «текущий» поток исполнения можно через вызов функции QThread::currentThread(), поток с которым ассоциирован объект — через вызов QObject::thread()

Главный поток, QCoreApplication и GUI

В силу правила «созданные объекты наследуют текущий поток», Вы всегда можете спокойно работать не выходя за пределы одного потока. Все созданные объекты автоматически попадут на обслуживание в «главный» поток, где всегда будет event loop и (по причине отсутствия других потоков) никогда не будет проблем с синхронизацией. Даже если Вы работаете с более сложной системой, требующей многопоточности, то в основной поток скорее всего попадет большинство объектов, за исключением тех немногих которые явным образом будут размещены где-то еще. Возможно именно это обстоятельство и порождает кажущуюся «магию» в которой объекты кажутся безо всяких усилий работающими независимо друг от друга (ибо в пределах потока реализуется cooperative multitasking) и при этом не требующими синхронизации, блокировок и тому подобных вещей (ибо все происходит в одном потоке).

Помимо того что «главный» поток является «первым» и содержит в себе основной цикл обработки событий QCoreApplication, еще одним характерным для Qt ограничением является то что в этом потоке должны «жить» все объекты связанные с GUI. Отчасти это является следствием легаси: в силу того что в ряде операционных систем любые операции с GUI могут происходить только в главном потоке, Qt подразделяет все объекты на «виджеты» и «не-виджеты». Widget-type object может жить только в главном потоке, попытка «перевесить» такой объект в любой другой автоматически зафейлится. В силу этого даже существует специальный метод QObject::isWidgetType(), отражающий довольно глубокие внутренние различия в механике работы потоков с такими объектами. Но интересно что и в намного более новом QtQuick, где от костыля с isWidgetType попытались уйти осталась та же самая проблема

Rendering thread

В рамках новой модели Qt5 вся отрисовка объектов происходит в специально выделенном для этого потоке, rendering thread. При этом чтобы это имело смысл и не ограничивалось простым переходом от одного «главного» потока к другому, объекты неявно делятся на «фронт-енд» который видит программист и обычно скрытый от него «бэк-энд» который собственно осуществляет реальную отрисовку. Бэк-энд живет в rendering thread, тогда как фронт-энд, чисто теоретически, может жить в любом другом потоке. Предполагается что полезную работу (если таковая есть) в виде обработки событий выполняет именно фронт-енд тогда как функция бэка ограничена только рендерингом. В теории таким образом получается win-win: бэк периодически «опрашивает» текущее состояние объектов и отрисовывает их на экране, при этом его не может «остановить» то что какой-то из объектов слишком сильно «задумался» обрабатывая событие в силу того что эта медленная обработка происходит в другом потоке. В свою очередь потоку объекта нет нужды дожидаться «ответов» от графического драйвера подтвеждающих завершение отрисовки и разные объекты могут работать в разных потоках.

Но как я уже упомянул в предыдущей главе, раз у нас есть поток «создающий» данные («фронт») и поток который их читает («бэк»), то нам необходимо как-то обеспечивать их синхронизацию. Эта синхронизация в Qt делается блокировками. Поток где живет «фронт» временно приостанавливается, после чего следует специальный вызов функции (QQuickItem::updatePaintNode(), QQuickFramebufferObject::Renderer::synchronize() ) единственной задачей которого является копирование релевантного для визуализации состояния объекта из «фронта» в «бэк». При этом вызов такой функции происходит внутри rendering thread, но благодаря тому что поток где живет объект в этот момент остановлен, пользователь может свободно работать с данными объекта так, как если бы это происходило «как обычно», внутри потока которому принадлежит объект.

Все хорошо, все отлично? К сожалению нет, и здесь начинаются достаточно неочевидные моменты. Если мы будем брать для каждого объекта блокировку по отдельности, то это будет довольно медленно поскольку rendering thread будет вынужден ждать пока эти объекты завершат обработку своих событий. «Повиснет» поток где живет объект — «повиснет» и рендеринг. Вдобавок станет возможен «рассинхрон» когда при одновременном изменении двух объектов один успеет отрисоваться еще в кадре N а другой будет отрисован только в кадре N+1. Предпочтительнее было бы брать блокировку лишь один раз и на все объекты сразу и лишь тогда когда мы уверены что эта блокировка будет успешной.

Что было реализовано для решения этой проблемы в Qt? Во-первых было принято решение что все «графические» объекты одного окна будут жить в одном потоке. Таким образом для отрисовки окна и взятия блокировки на все содержащиеся в нем объекты становится достаточно остановить один этот поток. Во-вторых блокировку для обновления бэк-энда инициирует сам же поток с UI-объектами, посылая сообщение rendering thread о необходимости провести синхронизацию и останавливая сам себя (QSGThreadedRenderLoop::polishAndSync если кому интересно). Это гарантирует что rendering thread никогда не будет «ждать» поток с «фронт-ендом». Если тот вдруг «зависнет» — rendering thread просто продолжит отрисовывать «старое» состояние объектов, не получая сообщений о необходимости обновиться. Это правда порождает довольно забавные баги вида «если рендеринг по какой-то причине не может отрисовать окно сразу, то главный поток зависает», но в целом является разумным компромиссом. Начиная c QtQuick 2.0 ряд «анимационных» объектов можно даже «поселить» имено в render thread чтобы анимация могла тоже продолжать работать если основной поток «задумался».

thread rendering что это. Смотреть фото thread rendering что это. Смотреть картинку thread rendering что это. Картинка про thread rendering что это. Фото thread rendering что это

Однако практическим следствием этого решения является то что все UI-объекты в любом случае должны жить в одном и том же потоке. В случае со старыми виджетами — в «главном» потоке, в случае с новыми Qt Quick-объектами — в потоке объекта QQuickWindow который ими «владеет». Последнее правило довольно изящно обыграно — для того чтобы нарисовать QQuickItem ему нужно сделать setParent к соответствующему QQuickWindow что как уже обсуждалось гарантирует что объект или переедет в соответствующий поток или вызов setParent провалится.

А теперь, увы, ложка дегтя: хотя разные QQuickWindow чисто теоретически могли бы жить в разных потоках, на практике это требует аккуратной пересылки им сообщений от операционной системы и в Qt сегодня таковая не реализована. В Qt 5.13 к примеру QCoreApplication пытается общаться с QQuickWindow через sendEvent требующий чтобы получатель и посылатель были в одном потоке (вместо postEvent который допускает чтобы потоки были разными). Поэтому на практике QQuickWindow правильно работают только в GUI-потоке и как следствие все QtQuick-объекты живут там же. В результате несмотря на наличие rendering thread практически все доступные пользователю объекты связанные с GUI по-прежнему обитают в одном и том же GUI thread. Возможно это изменится в Qt 6.

Помимо вышесказанного стоит так же помнить что поскольку Qt работает на множестве разных платформ (включая те в которых не поддерживается многопоточность), то в фреймворке предусмотрено приличное количество fallback-ов и в некоторых случаях функциональность rendering thread в реальности выполняется тем же самым gui thread-ом. В этом случае весь UI включая рендеринг живет в одном потоке и проблема синхронизации автоматически отпадает. Аналогично обстоят дела и с более старым, Qt4-style UI на основе виджетов. При большом желании можно заставить Qt работать в таком «однопоточном» режиме с помощью установки переменной окружения QSG_RENDER_LOOP в соответвующий вариант.

Заключение

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

Напомню еще раз основные моменты;

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *