Многоядерное программирование: использование преимуществ многоядерности

Опубликовано: 23 Марта, 2023

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

J2EE

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

Использование J2EE для ваших корпоративных приложений позволяет разработчику кодировать приложение, не задумываясь о потоках; это реальная сила J2EE. Приложения J2EE становятся многопоточными за счет изменения настроек, найденных на сервере приложений; поэтому многопоточность управляется контейнером. Как это произошло? Ну, в основном бизнес-логика может быть закодирована, а затем развернута на сервере в логическом контейнере (контейнеры содержат бизнес-логику… понимаете? Эти сумасшедшие Java-коты). Затем параметры контейнера будут определять, сколько одновременных экземпляров этой части бизнес-логики может быть запущено. Контейнер также может определять, сколько подключений к ресурсам, таким как базы данных, можно использовать одновременно. Эти управляемые контейнером свойства, хотя иногда с ними трудно работать, предоставляют средства для отделения многопоточности от кода приложения. Это хорошо, потому что позволяет настраивать многопоточность в зависимости от аппаратного обеспечения, на котором работает приложение, без изменения кода приложения. Корпоративное оборудование довольно часто меняется; машины могут быть добавлены в кластер серверов, или, возможно, процессоры могут быть добавлены или удалены из машин, уже существующих в кластере, каждое из этих изменений влияет на потенциал многопоточности приложений, работающих на оборудовании.

OpenMP

Отделение логики многопоточности от бизнес-логики должно быть ключевой задачей разработчиков, создающих приложения с массовым параллелизмом. Есть много причин для этого; простота разработки, простота отладки и легкость изменения приложения. При разработке на C/C++ или FORTRAN популярной реализацией для этого является OpenMP. OpenMP — это API, используемый для эффективного и действенного создания многопоточного кода.

По сути, OpenMP используется путем включения набора инструкций в код в виде комментариев или аннотаций. Код пишется последовательно, а затем в соответствующих местах добавляются аннотации. Когда затем код компилируется компилятором, совместимым с OpenMP, аннотации считываются, и код компилируется таким образом, чтобы использовать потоки в соответствии с указаниями аннотаций.

При подготовке этой статьи я наткнулся на JOMP. JOMP — это технология, подобная OpenMP, которую можно использовать с Java. Мне не ясно, каковы официальные отношения между JOMP и OpenMP, если таковые имеются; если кто-то, читающий это, знает больше об этом, пожалуйста, пришлите мне по электронной почте. Мне было бы очень интересно узнать об этом больше. Несмотря на то, что в Java есть встроенный API для использования потоков и управления ими, использование такой реализации по-прежнему полезно.

Этот метод параллельного программирования очень выгоден. Поскольку программа написана для последовательного выполнения, а аннотации являются просто комментариями к коду, то, если код скомпилирован традиционным компилятором, компилятор просто проигнорирует аннотации OpenMP. Затем тот же код можно было скомпилировать с помощью компилятора OpenMP и запустить с несколькими потоками параллельно. Это означает, что разработчику не пришлось бы изменять код, если бы программа работала на двух разных аппаратных архитектурах: одна поддерживает многопоточность, а другая не поощряет многопоточность.

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

Изображение 20319

Рис. 1. Пример потока многопоточного приложения. (Источник: www.acm.org)

Опасности многопоточности

Одна из распространенных ошибок, которую допускают разработчики при многопоточности своих приложений, связана с многопоточностью циклов. Например, если нужно выполнить итерацию по всему массиву целых чисел и просто добавить единицу к каждому значению, то это можно сделать многопоточным, чтобы выполнять каждую итерацию в отдельном потоке одновременно. OpenMP выполняет это (в C/C++) с помощью аннотации #pragma omp parallel для. Но, возможно, кто-то захочет перебрать тот же массив, но изменить каждое значение, добавив значение предыдущего элемента. В этом случае было бы ошибкой делать этот цикл многопоточным; если все итерации цикла происходят одновременно, вы не можете гарантировать, что предыдущий элемент был изменен, чтобы содержать новое значение. Выполнение такого цикла в нескольких потоках приведет к совершенно другому массиву целых чисел по сравнению с последовательным выполнением этого цикла в одном цикле.

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

Производительность

При тщательном подходе многопоточные приложения могут привести к значительному повышению производительности. Конечно, для этого требуется, чтобы приложение запускалось на оборудовании, способном эффективно поддерживать множество потоков. Но даже при наличии соответствующего оборудования существуют ограничения на то, сколько улучшений можно достичь с помощью многопоточности. Как я упоминал ранее (в моем примере с циклом for), есть блоки кода, которые не могут быть многопоточными. Следовательно, если есть большие части кода, которые не могут быть многопоточными, то время выполнения этих частей будет ограничивающим фактором в общем времени выполнения программы. Закон Амдала — это теория, часто используемая для оценки максимально возможного выигрыша в производительности от многопоточности приложения.

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