В оконных приложениях таких платформ, как Windows, Linux и macOS, скрывается ряд недоработок, которые порой снижают их быстродействие до раздражающе медленного. Сюда относятся нюансы работы в реальном времени, блокирование памяти при доступе к диску и недостаточно эффективное планирование для активного оконного приложения. В статье мы подробно разберём все эти проблемы.

Я программирую уже давно. Под давно я имею в виду не один десяток лет работы. Надеюсь, это достаточно долго. За это время мой опыт в основном заключался в программировании для современных платформ вроде Linux, Windows, macOS для десктопных и серверных архитектур процессоров. Недавно я участвовал в создании MIDI-движка для систем значительно меньшей мощности.

Вскоре после начала я столкнулся с проблемой, которая требовала обеспечить невозможность переполнения очереди событий ввода. По сути, эта задача сводится к тому, чтобы каждый обработчик событий не выполнялся дольше некой максимальной продолжительности времени. Затем меня осенило! Я уже слышал это раньше — «максимальная продолжительность времени», так как занимаюсь разработкой систем, работающих в реальном времени.

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

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

Когда я кликаю кнопкой мыши, нажимаю клавишу клавиатуры, то ожидаю, что система отреагирует в течение ограниченного промежутка времени. Ограниченный промежуток времени? Мы это уже слышали! Оконные приложения тоже являются системами реального времени. А ограниченное количество времени – это сколько, 100 мс или, может, 250мс? Главный смысл здесь в том, что время реакции не должно быть неопределенным. Я никогда не должен видеть вращающийся курсор ожидания. Никогда.

▍ Библиотечные функции не являются real-time


Одна из фундаментальных проблем в том, что многие оконные приложения для Windows, Linux и macOS вызывают функции, которые не предназначены для выполнения в течение ограниченного промежутка времени. Вот простой пример: многие приложения не думают дважды, выполняя ввод-вывод файла в обработчике событий UI. На стандартных дисках в большинстве случаев это приводит к терпимой задержке, но что если файл хранится на сетевом винчестере? Для обработки запроса такого файла может потребоваться гораздо больше секунды. Это приведет к временному зависанию приложения и недопониманию пользователя. Сетевой диск работает корректно, но оконное приложение нет.

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

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

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

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

▍ Виртуальная память


Что касается Windows, Linux и macOS, то эти операционные системы используют виртуальную память. Когда приложения выделяют область памяти, фактически они выделяют не физическую память. Они говорят операционной системе, что будут использовать определённую область памяти для конкретной цели. Это раскрывает богатую функциональность, но, в частности, позволяет операционным системам экономить физическую память, прозрачно сохраняя её страницы на жёстком диске и восстанавливая нужную страницу, когда приложение снова к ней обращается. Это означает, что при доступе к жёсткому диску доступ к памяти может блокироваться.

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

Это не является типичной проблемой, но случаи, когда вся система «исчерпывает память» встречаются довольно часто. Оказавшись в таком состоянии, система начинает спешно подкачивать страницы памяти на жёсткий диск. Это влияет на оконные приложения и ведёт к тому, что система зависает без предупреждения и без какого-либо способа вмешаться в этот процесс, поскольку нажатия клавиш не обрабатываются. С позиции пользователя это выглядит хуже, чем паника ядра. Такой тип сбоя происходил у меня в Linux много раз, поэтому я знаю, что это реальная проблема. Возможно, разработчики Windows и macOS уже её учли, но я в этом сомневаюсь.

Есть ли возможность её исправить? По крайней мере, в Linux есть семейство функций mlock(), которые просят операционную систему помещать и удерживать страницы памяти процесса в RAM. В Windows и macOS наверняка присутствуют аналогичные функции. Естественно, здесь есть и сложности, например, кто отвечает за блокирование страниц памяти – приложение или операционная система? Откуда приложение знает, какие страницы блокировать? Откуда это знает операционная система?

▍ Планирование в реальном времени


Последняя фундаментальная проблема реализации real-time UI поверх современных платформ – это отсутствие планирования для активного оконного приложения. Это системы с разделением времени, то есть выполнение процесса может быть приостановлено на произвольно долгий промежуток времени, если за использование CPU соперничает множество старых процессов.

Представьте, что у вас есть фоновые процессы, занимающие 100% CPU, и тут поступает событие для активного приложения. Операционная система может заблокироваться на 100 мс или более, прежде чем позволит этому приложению обработать поступившее событие. Это может вызвать задержку ответа для пользователя, нарушающую ограничение реального времени (примечание: типичным временным срезом в системах с разделением времени является 10 Гц).

Но для этого тоже есть решение, менеджер окон или нечто подобное, может просить ОС выдать приоритет в расписании требующему того приложению. Это означает, что в период, когда приложение активно и требует CPU, фоновые процессы голодают. Правда, есть сложности с применением этого решения к существующим системам, например, «Что делать, когда активное оконное приложение входит в бесконечный цикл?», «А что насчёт многопроцессных приложений?»

▍ Заключение


Надеюсь, мне удалось убедить вас, что в основе передовых оконных приложений есть недоработки. Работают ли эти приложения? Конечно, бо́льшую часть времени такие приложения исправно работают, но когда дают сбой из-за неудачных допущений относительно реального времени, это прям бесит. В этом случае мы наблюдаем вращающийся курсор ожидания. Для интерактивных систем рабочих станций такое поведение неприемлемо. Я хочу использовать отзывчивые, корректные приложения. В будущем UI будут учитывать ограничения реального времени во всём используемом в них стеке.

Насколько я знаю, исправление этих проблем рациональным способом потребует больших изменений на уровне экосистемы и общего осведомления разработчиков. За десятилетия скопилось много широко распространённых архитектурных решений, в которых эти проблемы игнорируются. Меня тянет отказаться от использования Windows, macOS и Linux в качестве основных платформ, с которыми я взаимодействую.

Telegram-канал с розыгрышами призов, новостями IT и постами о ретроиграх ????️

Комментарии (22)


  1. Tsvetik
    29.09.2023 13:52
    +15

    Ужасный перевод


    1. Bright_Translate Автор
      29.09.2023 13:52
      +3

      Будьте добры поконкретнее.


      1. ogost
        29.09.2023 13:52
        +10

        Пожалуйста:

        Когда я понял, что мне нужно дополнительно учитывать ограничения реального времени, у меня родилось много инженерных решений в определённом направлении

        Это что? По мне так это машинный перевод. А это:

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

        Это что? Нет, перевод в обоих случаях (возможно) корректный, возможно даже немного вами отредактированный, но если бы мы хотели читать машинный перевод, то пошли бы на оригинал и нажали бы кнопку translate в хроме. Можете мамой клясться, что переводили "без помощи машины", но перевод чисто механический, я с таким же успехом могу взять в руки словарь и перевести эту же статью, и у меня получится примерно то же самое, или возможно даже немного лучше, потому что у меня не стоит задачи перевести слово в слово и отработать за день, а есть задача донести мысль автора до читателя.

        И это заметьте, замечание человека, для которого русский не родной язык.

        ПС: хабр, почините уже этот редактор, или возьмите готовый, раз сами не умеете


        1. Bright_Translate Автор
          29.09.2023 13:52
          +2

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


        1. ogost
          29.09.2023 13:52
          +7

          ППС: хабр, почините трекер, и вообще наймите адекватных разработчиков, раз позиционируете себя как ИТ ресурс


      1. 1dNDN
        29.09.2023 13:52

        del


      1. anmipo
        29.09.2023 13:52
        +5

        Лично я поперхнулся вскоре после начала:

        Вскоре после начала я столкнулся с проблемой, которая требовала обеспечить невозможность переполнения очереди событий ввода.

        Канцелярит силён переводе этом в. Хотя надо отдать должное, перевод неплохо передаёт косноязычность оригинала:

        Soon after I started, I ran into the issue of guaranteeing that it was impossible for the queue of input events to overflow.

        Мне кажется, так было бы лучше:

        Уже в начале я столкнулся с проблемой: гарантировать, что очередь событий ввода невозможно будет переполнить.


        1. Bright_Translate Автор
          29.09.2023 13:52

          Спасибо. Но мне ваш вариант тоже как то не очень. Гарантировать, что... Не... Я бы так не написал даже просто по-русски, не в контексте перевода. В этой сфере многое диктуется схемой мышления нашей. Есть, кто очень вольно переводит, к примеру, но уходит от стилистики оригинала сильно при этом.... Стараюсь. Благодарю вас, читателей, за критику, в том числе. Подбадривает


  1. HemulGM
    29.09.2023 13:52
    +1

    Так тут проблема не с программами, а с текущей архитектурой ОС (ну и с подходом некоторых разработчиков)


  1. Einherjar
    29.09.2023 13:52

    Какая то демагогия закоренелого сишника (btw в оригинале "with an S", черт его знает что это, но вряд ли язык C) пишущего на чистом винапи, не знающего слова многопоточность и застрявшего в начале нулевых. В большинстве языков и фреймворков уже есть куча средств для того чтобы просто и быстро делать асинхронные обработчики событий пользовательского ввода. Наглухо UI повисает разве что в каких то совсем дремучих программах, ну или если какой то баг.


    1. lair
      29.09.2023 13:52
      +2

      btw в оригинале "with an S", черт его знает что это, но вряд ли язык C

      Это значит "декадЫ, не декадА".


    1. Ritan
      29.09.2023 13:52

      Ну так фундаментально это действительно проблему не решает - т.к. в случае сильной нехватки физической памяти всё упрётся в pagein-pageout и никакого риалтайма не останется, сколько асинхронных обработчиков не вешай. В Linux я на это натыкаюсь с завидным постоянством.


      Правда решения он никакого и не предлагает, т.к. отказ от виртальной памяти таковым быть не может


      1. Gorthauer87
        29.09.2023 13:52

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

        Так что в итоге факт, что приложение может тормозить при недостатке памяти просто надо принять.


  1. vagon333
    29.09.2023 13:52
    +2

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

    Никто и не спорит.
    С т.з. разработки у разраба всегда дилемма: сделать доступ ресурсу синхронным или асинхронным.
    Синхронный проще кодить, асинхронный удобнее пользователю, но сложнее кодить и траблшутить (troubleshooting).
    В чем открытие?

    Меня тянет отказаться от использования Windows, macOS и Linux в качестве основных платформ, с которыми я взаимодействую.

    Ну, это смелое заявление горячих голов.
    Переводить пользовательские приложения в real time interaction with user - неоправданный кусок работы. Большинство пользователей не заметят.


  1. avdosev
    29.09.2023 13:52
    +2

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


    1. rapidstream
      29.09.2023 13:52
      +2

      Нет никакого решения у автора: "бу-бу-бу, уйду в лес" (откажусь от Windows, MacOS, Linux). Это нытьё и демагогия.


  1. kmatveev
    29.09.2023 13:52

    Нууу противоречиво. Блокирующие вызовы с одной стороны блокируют поток выполнения на неопределённое время, с другой стороны - упрощают структуру кода, он остаётся линейным. С другой стороны, тому потоку, который заблокирован, точно есть что делать, если бы он не был заблокирован? И те же самые вопросы возникают, если брать оконные приложения. Автор с одной стороны ожидает, что когда он кликает, то приложение прореагирует, с другой стороны - не хочет видеть курсор ожидания. Ну дык курсор сменился на ожидание - это приложение прореагировало. Не очень хорошо, что заблокировался UI-поток, в нём происходит отрисовка. Но интерфейс приложения всё равно придётся заблокировать, каким-нибудь модальным диалогом, разве что добавив кнопку "Cancel".


    1. MiraclePtr
      29.09.2023 13:52

      Блокирующие вызовы с одной стороны блокируют поток выполнения на неопределённое время, с другой стороны - упрощают структуру кода, он остаётся линейным

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


      1. Ariox41
        29.09.2023 13:52

        Большинство gui фреймворков не имеют интеграции с async/await и т.п, а разработчики асинхронных экосистем редко думают о gui.

        Я недавно экспериментировал с волокнами (fibers) на c++ и qt. Получилось довольно интересно: в потоке gui можно использовать асинхронные каналы, мьютексы, future и promise, что позволяет явно написать алгоритм вида: "заблокировать кнопку перехода на следующую страницу" - "асинхронно загрузить страницу, в процессе выполняя асинхронные сетевые запросы" - "разблокировать кнопку перехода на следующую станицу". Это действительно удобно, но есть одна проблема: при нажатии кнопки "завершить программу", она не завершается. В асинхронных экосистемах механизма отмены обычно либо нет, либо он слишком неудобен, чтобы считать асинхронный код простым. Конечно, можно найти/написать свой планировщик, более подходящий для gui, но для этого как минимум нужно знать и понимать, какие проблемы он должен решать, какие решить не может, и учитывать нюансы вроде удаления вложенных виджетов при удалении родительского в qt. Простой frontend разработчик обычно это сделать не сможет, по крайней мере так, чтобы код оставался понятным. Нужна согласованная экосистема.


        1. HemulGM
          29.09.2023 13:52

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


        1. MiraclePtr
          29.09.2023 13:52

          В асинхронных экосистемах механизма отмены обычно либо нет, либо он слишком неудобен, чтобы считать асинхронный код простым.

          Ну в шарпе вон уже не первый день на каждом углу есть CancellationToken'ы, там с ними все реально очень удобно, и да, можно сказать у них там почти что согласованная экосистема. В плюсах в новый std::jthread не забыли добавить поддержку stop_source/stop_token (первый шажочек, так сказать). Так что подвижки есть.


  1. Xeldos
    29.09.2023 13:52
    +6

    Заголовок - "всего лишь чуть-чуть недоработанные приложения, а то был бы риалтайм"
    Содержание - "ну тут в общем всю систему надо менять начиная с malloc()"