какие приложения используют многопоточность процессора
Исследование оптимизаций и многопоточного выполнения приложений на CPU. Теория и практика.
Нет, это статья не о харкоде или ассемблере.
Решая вопрос о необходимости апгрейда, я исследовал некоторые игры и бенчмарки. Нашел кое-что интересное, чем и хочу поделиться с общественностью.
Текста и лирики будет много, но вы сами научитесь анализировать не только игру, но и любое приложение на предмет многопоточности.
Нет, это статья не о харкоде или ассемблере.
Решая вопрос о необходимости апгрейда, я исследовал некоторые игры и бенчмарки. Нашел кое-что интересное, чем и хочу поделиться с общественностью.
Текста и лирики будет много, но вы сами научитесь анализировать не только игру, но и любое приложение на предмет многопоточности.
Суровые реалии.
113 кадров. Казалось бы, бенчмарк это синтетика, должна уметь ВСЕ выжимать из ПК.
Но обратите внимание, что загрузка ЦПУ около 50%, причем заняты все ядра.
MSI Afterburner показывает, что видеокарты загружены на
40%.
ЦПУ простаивает, ГПУ простаивает… где мои 100500 FPS?
Окай. Снижаю частоту ЦПУ множителем до 2000 МГц с помощью TurboV от вендора матплаты, прямо в рантайме — получаю падение FPS до 90 и падение нагрузки на GPU до 30%. Загрузка ЦПУ в процентах не изменилась вообще! Какого черта?
Возвращаю 3500 МГц и отбираю у процесса бенчмарка одно ядро — FPS почти без изменений.
Отбираю еще одно ядро, остается ему два ядра, — падение до 5-10 FPS, загрузка двух ядер ЦПУ 100%, ГПУ разгрузилось до 1-2%.
Скриншоты к сожалению делал не все, а остался только один. Но суть и так ясна.
А ясно то, что ничего не ясно — ЦПУ простаивает, ГПУ простаивает, а FPS падает, да и вообще его должно зашкаливать в таких разрешениях и при таком качестве.
В поисках ответов я отправился в google, форумы, jabber`ы.
Спустя пару дней вечернего чтения и экспериментов виновные были найдены. Их оказалось двое.
1. Windows.
2. Разработчик приложения.
Да, да, внезапно, но Windows опять виноват. Нет, это не кривая установка, поломанная ОСь или что-то подобное.
Это «бага», которая даже не «бага», а вовсе даже «фича».
Здесь банальное введение пользователя в заблуждение диспетчером задач и планировщиком процессов.
Сейчас расскажу и покажу, что происходит на самом деле. Разберем ситуацию на примере бенчмарка Heaven.
Что такое многопоточное приложение?
Это приложение, процесс, который может инициировать несколько потоков обработки данных. Эти потоки называются thread (нить, поток).
Потоки эти не из параллельного мира и их вполне можно пощупать и увидеть с помощью специальных инструментов.
Нам понадобится:
20-23% времени ЦПУ, чем съедали около 65-70% его ресурсов, и два-три мелких потока по 2-5%. Последние за потоки можно было бы и не считать, но три потока по 5% это 15%, а на ядро у нас приходится 25%. В итоге можно сказать, что Crysis 3 и Battlefield 3 хорошо оптимизированы для 4х ядер и способных с хорошей эффективностью их нагрузить, а вот большее количество ядер им поможет слабо.Темп прироста FPS от количества ядер свыше 4 — будет снижен. Собственно тесты Core 2gen/3gen vs FX Vishera это и показывают.
Спасибо, что дочитали до конца. Удачи Вам.
100% загрузил все 4 ядра ЦПУ.
Ранее он такое поведение не обнаруживал.
Таким образом, вывод об оптимизации в статье следует сделать несколько иной:
Crysis 3 способен загрузить работой как минимум 6 ядер ЦПУ, а, учитывая мелкие треды, скорее всего работы хватит и 8 ядрам. Единственное что — такие броски и нагрузки случаются не всегда и в этой игре встречаются только в местах с травой. В корридорах нагрузка на ЦПУ слабая.
Недавно Лаборатория провела тест ЦПУ и ГПУ как раз в этой же локации.
Как видим Vishera ее 8 ядер не сильно и помогли. Она может тягаться только на равных с i5 без индекса К.
А когда i5 с индексом К соревнуется с ней на равной частоте, то i5 уходит в заметный отрыв.
Phenom II x4 частотой 3.5 ГГц не достаточно для комфортной игры потому, что нагрузка на ЦПУ запредельная, а в случае игрового «замеса» в этой локации ФПС упадет еще сильнее, ниже 20. Можно немного снизить зависимость от ЦПУ, если выставить дальность анимации травы через консоль — e_MergedMeshesInstanceDist 2 (по-умолчанию 4.5 / 8 ). Это поднимет FPS на десяток кадров.
ps. На скришотах OSD информация в игре о ЦПУ и ГПУ получена с помощью Play Claw 4.
Создана тема по сбору статистики в играх!!
UPD 16.07.2015: Из статьи была удалена информация и домыслы о компиляторах.
Потоки и работа с ними
Многопоточность позволяет увеличивать скорость реагирования приложения и, если приложение работает в многопроцессорной или многоядерной системе, его пропускную способность.
Процессы и потоки
Процесс — это исполнение программы. Операционная система использует процессы для разделения исполняемых приложений. Поток — это основная единица, которой операционная система выделяет время процессора. Каждый поток имеет приоритет планирования и набор структур, в которых система сохраняет контекст потока, когда выполнение потока приостановлено. Контекст потока содержит все сведения, позволяющие потоку безболезненно возобновить выполнение, в том числе набор регистров процессора и стек потока. Несколько потоков могут выполняться в контексте процесса. Все потоки процесса используют общий диапазон виртуальных адресов. Поток может исполнять любую часть программного кода, включая части, выполняемые в данный момент другим потоком.
Цели применения нескольких потоков
Используйте несколько потоков, чтобы увеличить скорость реагирования приложения и воспользоваться преимуществами многопроцессорной или многоядерной системы, чтобы увеличить пропускную способность приложения.
Представьте себе классическое приложение, в котором основной поток отвечает за элементы пользовательского интерфейса и реагирует на действия пользователя. Используйте рабочие потоки для выполнения длительных операций, которые, в противном случае будут занимать основной поток, в результате чего пользовательский интерфейс будет недоступен. Для более оперативной реакции на входящие сообщения или события также можно использовать выделенный поток связи с сетью или устройством.
Если программа выполняет операции, которые могут выполняться параллельно, можно уменьшить общее время выполнения путем выполнения этих операций в отдельных потоках и запуска программы в многопроцессорной или многоядерной системе. В такой системе использование многопоточности может увеличить пропускную способность, а также повысить скорость реагирования.
Наконец, можно использовать класс System.Threading.Thread, который представляет управляемый поток. Дополнительные сведения см. в разделе Использование потоков и работа с потоками.
Исключения следует обрабатывать в потоках. Необработанные исключения в потоках, как правило, приводят к завершению процесса. Дополнительные сведения см. в статье Исключения в управляемых потоках.
Внутри процесса: многопоточность и пинг-понг mutex’ом
Какая тема вызывает больше всего вопросов и затруднений у начинающих? Когда я спросила об этом преподавателя и Java-программиста Александра Пряхина, он сразу ответил: «Многопоточность». Спасибо ему за идею и помощь в подготовке этой статьи!
Мы заглянем во внутренний мир приложения и его процессов, разберёмся, в чём суть многопоточности, когда она полезна и как её реализовать — на примере Java. Если учите другой язык ООП, не огорчайтесь: базовые принципы одни и те же.
О потоках и их истоках
Чтобы понять многопоточность, сначала вникнем, что такое процесс. Процесс – это часть виртуальной памяти и ресурсов, которую ОС выделяет для выполнения программы. Если открыть несколько экземпляров одного приложения, под каждый система выделит по процессу. В современных браузерах за каждую вкладку может отвечать отдельный процесс.
Вы наверняка сталкивались с «Диспетчером задач» Windows (в Linux это — «Системный монитор») и знаете, что лишние запущенные процессы грузят систему, а самые «тяжёлые» из них часто зависают, так что их приходится завершать принудительно.
Но пользователи любят многозадачность: хлебом не корми — дай открыть с десяток окон и попрыгать туда-сюда. Налицо дилемма: нужно обеспечить одновременную работу приложений и при этом снизить нагрузку на систему, чтобы она не тормозила. Допустим, «железу» не угнаться за потребностями владельцев — нужно решать вопрос на программном уровне.
Мы хотим, чтобы в единицу времени процессор успевал выполнить больше команд и обработать больше данных. То есть нам надо уместить в каждом кванте времени больше выполненного кода. Представьте единицу выполнения кода в виде объекта — это и есть поток.
К сложному делу легче подступиться, если разбить его на несколько простых. Так и при работе с памятью: «тяжёлый» процесс делят на потоки, которые занимают меньше ресурсов и скорее доносят код до вычислителя (как именно — см. ниже).
У каждого приложения есть как минимум один процесс, а у каждого процесса — минимум один поток, который называют главным и из которого при необходимости запускают новые.
Разница между потоками и процессами
Потоки используют память, выделенную под процесс, а процессы требуют себе отдельное место в памяти. Поэтому потоки создаются и завершаются быстрее: системе не нужно каждый раз выделять им новое адресное пространство, а потом высвобождать его.
Процессы работают каждый со своими данными — обмениваться чем-то они могут только через механизм межпроцессного взаимодействия. Потоки обращаются к данным и ресурсам друг друга напрямую: что изменил один — сразу доступно всем. Поток может контролировать «собратьев» по процессу, в то время как процесс контролирует исключительно своих «дочек». Поэтому переключаться между потоками быстрее и коммуникация между ними организована проще.
Какой отсюда вывод? Если вам нужно как можно быстрее обработать большой объём данных, разбейте его на куски, которые можно обрабатывать отдельными потоками, а затем соберите результат воедино. Это лучше, чем плодить жадные до ресурсов процессы.
Но почему такое популярное приложение как Firefox идёт по пути создания нескольких процессов? Потому что именно для браузера изолированная работа вкладок — это надёжно и гибко. Если с одним процессом что-то не так, не обязательно завершать программу целиком — есть возможность сохранить хотя бы часть данных.
Что такое многопоточность
Вот мы и подошли к главному. Многопоточность — это когда процесс приложения разбит на потоки, которые параллельно — в одну единицу времени — обрабатываются процессором.
Вычислительная нагрузка распределяется между двумя или более ядрами, так что интерфейс и другие компоненты программы не замедляют работу друг друга.
Многопоточные приложения можно запускать и на одноядерных процессорах, но тогда потоки выполняются по очереди: первый поработал, его состояние сохранили — дали поработать второму, сохранили — вернулись к первому или запустили третий, и т.д.
Занятые люди жалуются, что у них всего две руки. Процессы и программы могут иметь столько рук, сколько нужно для скорейшего выполнения задачи.
Жди сигнала: синхронизация в многопоточных приложениях
Представьте, что несколько потоков пытаются одновременно изменить одну и ту же область данных. Чьи изменения будут в итоге приняты, а чьи — отменены? Чтобы работа с общими ресурсами не приводила к путанице, потокам нужно координировать свои действия. Для этого они обмениваются информацией с помощью сигналов. Каждый поток сообщает другим, что он сейчас делает и каких изменений ждать. Так данные всех потоков о текущем состоянии ресурсов синхронизируются.
В категориях объектно-ориентированного программирования сигналы — это объекты синхронизации. У каждого из них — своя роль во взаимодействии.
Основные средства синхронизации
Взаимоисключение (mutual exclusion, сокращённо — mutex) — «флажок», переходящий к потоку, который в данный момент имеет право работать с общими ресурсами. Исключает доступ остальных потоков к занятому участку памяти. Мьютексов в приложении может быть несколько, и они могут разделяться между процессами. Есть подвох: mutex заставляет приложение каждый раз обращаться к ядру операционной системы, что накладно.
Семафор — позволяет вам ограничить число потоков, имеющих доступ к ресурсу в конкретный момент. Так вы снизите нагрузку на процессор при выполнении кода, где есть узкие места. Проблема в том, что оптимальное число потоков зависит от машины пользователя.
Событие — вы определяете условие, при наступлении которого управление передаётся нужному потоку. Данными о событиях потоки обмениваются, чтобы развивать и логически продолжать действия друг друга. Один получил данные, другой проверил их корректность, третий — сохранил на жёсткий диск. События различаются по способу отмены сигнала о них. Если нужно уведомить о событии несколько потоков, для остановки сигнала придётся вручную ставить функцию отмены. Если же целевой поток только один, можно создать событие с автоматическим сбросом. Оно само остановит сигнал, после того как он дойдёт до потока. Для гибкого управления потоками события можно выстраивать в очередь.
Критическая секция — более сложный механизм, который объединяет в себе счётчик цикла и семафор. Счётчик позволяет отложить запуск семафора на нужное время. Преимущество в том, что ядро задействуется лишь в случае, если секция занята и нужно включать семафор. В остальное время поток выполняется в пользовательском режиме. Увы, секцию можно использовать только внутри одного процесса.
Как реализовать многопоточность в Java
За работу с потоками в Java отвечает класс Thread. Создать новый поток для выполнения задачи — значит создать экземпляр класса Thread и связать его с нужным кодом. Сделать это можно двумя путями:
образовать от Thread подкласс;
имплементировать в своём классе интерфейс Runnable, после чего передавать экземпляры класса в конструктор Thread.
Пока мы не будем затрагивать тему тупиковых ситуаций (deadlock’ов), когда потоки блокируют работу друг друга и зависают — оставим это для следующей статьи. А сейчас перейдём к практике.
Пример многопоточности в Java: пинг-понг мьютексами
Если вы думаете, что сейчас будет что-то страшное — выдохните. Работу с объектами синхронизации мы рассмотрим почти в игровой форме: два потока будут перебрасываться mutex’ом. Но по сути вы увидите реальное приложение, где в один момент времени только один поток может обрабатывать общедоступные данные.
Сначала создадим класс, наследующий свойства уже известного нам Thread, и напишем метод «удара по мячу» (kickBall):
Теперь позаботимся о мячике. Будет он у нас не простой, а памятливый: чтоб мог рассказать, кто по нему ударил, с какой стороны и сколько раз. Для этого используем mutex: он будет собирать информацию о работе каждого из потоков — это позволит изолированным потокам общаться друг с другом. После 15-го удара выведем мяч из игры, чтоб его сильно не травмировать.
А теперь на сцену выходят два потока-игрока. Назовём их, не мудрствуя лукаво, Пинг и Понг:
«Полный стадион народа — время начинать матч». Объявим об открытии встречи официально — в главном классе приложения:
Как видите, ничего зубодробительного здесь нет. Это пока только введение в многопоточность, но вы уже представляете, как это работает, и можете экспериментировать — ограничивать длительность игры не числом ударов, а по времени, например. Мы ещё вернёмся к теме многопоточности — рассмотрим пакет java.util.concurrent, библиотеку Akka и механизм volatile. А еще поговорим о реализации многопоточности на Python.
Какая тема вызывает больше всего вопросов и затруднений у начинающих? Когда я спросила об этом преподавателя и Java-программиста Александра Пряхина, он сразу ответил: «Многопоточность». Спасибо ему за идею и помощь в подготовке этой статьи!
Мы заглянем во внутренний мир приложения и его процессов, разберёмся, в чём суть многопоточности, когда она полезна и как её реализовать — на примере Java. Если учите другой язык ООП, не огорчайтесь: базовые принципы одни и те же.
О потоках и их истоках
Чтобы понять многопоточность, сначала вникнем, что такое процесс. Процесс – это часть виртуальной памяти и ресурсов, которую ОС выделяет для выполнения программы. Если открыть несколько экземпляров одного приложения, под каждый система выделит по процессу. В современных браузерах за каждую вкладку может отвечать отдельный процесс.
Вы наверняка сталкивались с «Диспетчером задач» Windows (в Linux это — «Системный монитор») и знаете, что лишние запущенные процессы грузят систему, а самые «тяжёлые» из них часто зависают, так что их приходится завершать принудительно.
Но пользователи любят многозадачность: хлебом не корми — дай открыть с десяток окон и попрыгать туда-сюда. Налицо дилемма: нужно обеспечить одновременную работу приложений и при этом снизить нагрузку на систему, чтобы она не тормозила. Допустим, «железу» не угнаться за потребностями владельцев — нужно решать вопрос на программном уровне.
Мы хотим, чтобы в единицу времени процессор успевал выполнить больше команд и обработать больше данных. То есть нам надо уместить в каждом кванте времени больше выполненного кода. Представьте единицу выполнения кода в виде объекта — это и есть поток.
К сложному делу легче подступиться, если разбить его на несколько простых. Так и при работе с памятью: «тяжёлый» процесс делят на потоки, которые занимают меньше ресурсов и скорее доносят код до вычислителя (как именно — см. ниже).
У каждого приложения есть как минимум один процесс, а у каждого процесса — минимум один поток, который называют главным и из которого при необходимости запускают новые.
Разница между потоками и процессами
Потоки используют память, выделенную под процесс, а процессы требуют себе отдельное место в памяти. Поэтому потоки создаются и завершаются быстрее: системе не нужно каждый раз выделять им новое адресное пространство, а потом высвобождать его.
Процессы работают каждый со своими данными — обмениваться чем-то они могут только через механизм межпроцессного взаимодействия. Потоки обращаются к данным и ресурсам друг друга напрямую: что изменил один — сразу доступно всем. Поток может контролировать «собратьев» по процессу, в то время как процесс контролирует исключительно своих «дочек». Поэтому переключаться между потоками быстрее и коммуникация между ними организована проще.
Какой отсюда вывод? Если вам нужно как можно быстрее обработать большой объём данных, разбейте его на куски, которые можно обрабатывать отдельными потоками, а затем соберите результат воедино. Это лучше, чем плодить жадные до ресурсов процессы.
Но почему такое популярное приложение как Firefox идёт по пути создания нескольких процессов? Потому что именно для браузера изолированная работа вкладок — это надёжно и гибко. Если с одним процессом что-то не так, не обязательно завершать программу целиком — есть возможность сохранить хотя бы часть данных.
Что такое многопоточность
Вот мы и подошли к главному. Многопоточность — это когда процесс приложения разбит на потоки, которые параллельно — в одну единицу времени — обрабатываются процессором.
Вычислительная нагрузка распределяется между двумя или более ядрами, так что интерфейс и другие компоненты программы не замедляют работу друг друга.
Многопоточные приложения можно запускать и на одноядерных процессорах, но тогда потоки выполняются по очереди: первый поработал, его состояние сохранили — дали поработать второму, сохранили — вернулись к первому или запустили третий, и т.д.
Занятые люди жалуются, что у них всего две руки. Процессы и программы могут иметь столько рук, сколько нужно для скорейшего выполнения задачи.
Жди сигнала: синхронизация в многопоточных приложениях
Представьте, что несколько потоков пытаются одновременно изменить одну и ту же область данных. Чьи изменения будут в итоге приняты, а чьи — отменены? Чтобы работа с общими ресурсами не приводила к путанице, потокам нужно координировать свои действия. Для этого они обмениваются информацией с помощью сигналов. Каждый поток сообщает другим, что он сейчас делает и каких изменений ждать. Так данные всех потоков о текущем состоянии ресурсов синхронизируются.
В категориях объектно-ориентированного программирования сигналы — это объекты синхронизации. У каждого из них — своя роль во взаимодействии.
Основные средства синхронизации
Взаимоисключение (mutual exclusion, сокращённо — mutex) — «флажок», переходящий к потоку, который в данный момент имеет право работать с общими ресурсами. Исключает доступ остальных потоков к занятому участку памяти. Мьютексов в приложении может быть несколько, и они могут разделяться между процессами. Есть подвох: mutex заставляет приложение каждый раз обращаться к ядру операционной системы, что накладно.
Семафор — позволяет вам ограничить число потоков, имеющих доступ к ресурсу в конкретный момент. Так вы снизите нагрузку на процессор при выполнении кода, где есть узкие места. Проблема в том, что оптимальное число потоков зависит от машины пользователя.
Событие — вы определяете условие, при наступлении которого управление передаётся нужному потоку. Данными о событиях потоки обмениваются, чтобы развивать и логически продолжать действия друг друга. Один получил данные, другой проверил их корректность, третий — сохранил на жёсткий диск. События различаются по способу отмены сигнала о них. Если нужно уведомить о событии несколько потоков, для остановки сигнала придётся вручную ставить функцию отмены. Если же целевой поток только один, можно создать событие с автоматическим сбросом. Оно само остановит сигнал, после того как он дойдёт до потока. Для гибкого управления потоками события можно выстраивать в очередь.
Критическая секция — более сложный механизм, который объединяет в себе счётчик цикла и семафор. Счётчик позволяет отложить запуск семафора на нужное время. Преимущество в том, что ядро задействуется лишь в случае, если секция занята и нужно включать семафор. В остальное время поток выполняется в пользовательском режиме. Увы, секцию можно использовать только внутри одного процесса.
Как реализовать многопоточность в Java
За работу с потоками в Java отвечает класс Thread. Создать новый поток для выполнения задачи — значит создать экземпляр класса Thread и связать его с нужным кодом. Сделать это можно двумя путями:
образовать от Thread подкласс;
имплементировать в своём классе интерфейс Runnable, после чего передавать экземпляры класса в конструктор Thread.
Пока мы не будем затрагивать тему тупиковых ситуаций (deadlock’ов), когда потоки блокируют работу друг друга и зависают — оставим это для следующей статьи. А сейчас перейдём к практике.
Пример многопоточности в Java: пинг-понг мьютексами
Если вы думаете, что сейчас будет что-то страшное — выдохните. Работу с объектами синхронизации мы рассмотрим почти в игровой форме: два потока будут перебрасываться mutex’ом. Но по сути вы увидите реальное приложение, где в один момент времени только один поток может обрабатывать общедоступные данные.
Сначала создадим класс, наследующий свойства уже известного нам Thread, и напишем метод «удара по мячу» (kickBall):
Теперь позаботимся о мячике. Будет он у нас не простой, а памятливый: чтоб мог рассказать, кто по нему ударил, с какой стороны и сколько раз. Для этого используем mutex: он будет собирать информацию о работе каждого из потоков — это позволит изолированным потокам общаться друг с другом. После 15-го удара выведем мяч из игры, чтоб его сильно не травмировать.
А теперь на сцену выходят два потока-игрока. Назовём их, не мудрствуя лукаво, Пинг и Понг:
«Полный стадион народа — время начинать матч». Объявим об открытии встречи официально — в главном классе приложения: