Что нужно знать при погружении в многопоточное программирование на C++
В настоящее время я работаю над беспроводным сетевым приложением на C++, и сейчас у меня возникнет необходимость в многопоточном программном обеспечении в рамках одного процесса, а не в том, чтобы все они были в разных процессах. Теоретически я понимаю многопоточность, но мне еще практически не хотелось погружаться.
Что должен знать каждый программист при написании многопоточного кода на C++?
22 ответа
Я бы сконцентрировался на том, чтобы спроектировать объект как можно более секционированным, чтобы у вас было минимальное количество общих вещей в потоках. Если вы убедитесь, что у вас нет статики и других ресурсов, совместно используемых потоками (кроме тех, которыми вы бы поделились, если бы вы разработали это с процессами, а не с потоками), все будет в порядке.
Поэтому, хотя да, вы должны иметь в виду такие понятия, как блокировки, семафоры и т. Д., Но лучший способ решить эту проблему - попытаться избежать их.
Я не эксперт вообще в этой теме. Просто правило большого пальца:
1) Дизайн для простоты, ошибки действительно трудно найти в параллельном коде даже в самых простых примерах.
2) C++ предлагает вам очень элегантную парадигму для управления ресурсами (мьютекс, семафор,...): RAII. Я заметил, что гораздо легче работать с boost::thread
чем работать с POSIX
потоки.
3) Создайте свой код как потокобезопасный. Если вы этого не сделаете, ваша программа может вести себя странно.
Я нахожусь именно в этой ситуации: я написал библиотеку с глобальной блокировкой (много потоков, но только одна работает в библиотеке за один раз) и рефакторинг ее для поддержки параллелизма.
Я читал книги на эту тему, но то, что я узнал, стоит в нескольких пунктах:
- думайте параллельно: представьте толпу, проходящую через код. Что происходит, когда метод вызывается, когда он уже в действии?
- думайте о том, чтобы делиться: представьте себе, что многие люди пытаются читать и изменять общие ресурсы одновременно.
- Дизайн: избегайте проблем, которые могут возникнуть в пунктах 1 и 2.
- никогда не думайте, что вы можете игнорировать крайние случаи, они будут сильно кусаться.
Поскольку вы не можете проверять параллельный дизайн (так как чередование потоков не воспроизводимо), вы должны убедиться, что ваш проект устойчив, тщательно анализируя пути кода и документируя, как код должен использоваться.
Как только вы поймете, как и где вы должны ставить узкое место в своем коде, вы можете прочитать документацию об инструментах, используемых для этой работы:
- Mutex (эксклюзивный доступ к ресурсу)
- Scoped Locks (хороший шаблон для блокировки / разблокировки Mutex)
- Семафоры (передача информации между потоками)
- ReadWrite Mutex (много читателей, эксклюзивный доступ на запись)
- Сигналы (как "убить" поток или отправить ему сигнал прерывания, как его перехватить)
- Модели параллельного проектирования: начальник / работник, производитель / потребитель и т. Д. (См. Шмидта)
- специфичные для платформы инструменты: openMP, блоки C и т. д.
Удачи! Параллелизм это весело, просто не торопитесь...
Вы должны прочитать о блокировках, мьютексах, семафорах и условных переменных.
Один совет: если ваше приложение имеет какую-либо форму пользовательского интерфейса, убедитесь, что вы всегда меняете его из потока пользовательского интерфейса. Большинство наборов инструментов / каркасов пользовательского интерфейса вылетает (или ведет себя неожиданно), если вы обращаетесь к ним из фонового потока. Обычно они предоставляют метод диспетчеризации для выполнения некоторой функции в потоке пользовательского интерфейса.
Никогда не предполагайте, что внешние API являются поточно-ориентированными. Если это явно не указано в их документах, не вызывайте их одновременно из нескольких потоков. Вместо этого ограничьте их использование одним потоком или используйте мьютекс для предотвращения одновременных вызовов (это довольно похоже на вышеупомянутые библиотеки GUI).
Следующий момент связан с языком. Помните, что в C++ (в настоящее время) нет четко определенного подхода к многопоточности. Компилятор / оптимизатор не знает, может ли код вызываться одновременно. volatile
Ключевое слово полезно для предотвращения определенных оптимизаций (например, кэширование полей памяти в регистрах ЦП) в многопоточных контекстах, но это не механизм синхронизации.
Я бы порекомендовал повысить для примитивов синхронизации. Не связывайтесь с API платформы. Они затрудняют перенос вашего кода, потому что они имеют схожую функциональность на всех основных платформах, но немного отличаются поведением деталей. Boost решает эти проблемы, предоставляя пользователю только общие функциональные возможности.
Кроме того, если есть даже минимальная вероятность того, что структура данных может быть записана двумя потоками одновременно, используйте примитив синхронизации для ее защиты. Даже если вы думаете, что это произойдет только раз в миллион лет.
Одна вещь, которую я нашел очень полезной, - это сделать приложение настраиваемым с учетом фактического количества потоков, которые оно использует для различных задач. Например, если у вас есть несколько потоков, обращающихся к базе данных, настройте количество этих потоков с помощью параметра командной строки. Это очень удобно при отладке - вы можете исключить проблемы с многопоточностью, установив число в 1, или принудительно разрешив их, указав большое значение. Это также очень удобно при определении оптимального количества потоков.
Убедитесь, что вы тестируете свой код в системе с одним процессором и системой с несколькими процессорами.
На основании комментариев:-
- Одно гнездо, одноядерный
- Одно гнездо, два ядра
- Один разъем, более двух ядер
- Две розетки, одноядерные каждый
- Две розетки, комбинация одно-, двух- и многоядерных процессоров.
- Множественные розетки, комбинация одно-, двух- и многоядерных процессоров.
Ограничивающим фактором здесь будет стоимость. В идеале, сконцентрируйтесь на типах системы, на которой будет выполняться ваш код.
Поскольку вы новичок, начните с простого. Сначала заставьте это работать правильно, затем беспокойтесь об оптимизации. Я видел, как люди пытаются оптимизировать, увеличивая параллелизм определенного раздела кода (часто используя сомнительные трюки), даже не пытаясь понять, был ли спор вообще.
Во-вторых, вы хотите иметь возможность работать на таком высоком уровне, на котором только можете. Не работайте на уровне блокировок и мьютексов, если вы можете использовать существующую очередь мастер-работник. TBB от Intel выглядит многообещающе, будучи немного выше уровня чистых потоков.
В-третьих, многопоточное программирование сложно. Сократите области вашего кода, где вы должны как можно больше думать об этом. Если вы можете написать класс таким образом, чтобы объекты этого класса работали только в одном потоке, и в нем нет статических данных, это значительно уменьшает то, о чем вам следует беспокоиться в этом классе.
Мои лучшие советы для новичков:
Если возможно, используйте библиотеку параллелизма на основе задач, наиболее очевидным из которых является TBB от Intel. Это изолирует вас от шероховатых, хитрых деталей и более эффективно, чем все, что вы сами соберетесь. Основным недостатком является то, что эта модель не поддерживает все виды многопоточности; он отлично подходит для использования многоядерных вычислений для вычислительной мощности, менее хорош, если вы хотите, чтобы потоки ожидали блокирования ввода-вывода.
Знать, как прервать потоки (или, в случае TBB, как заставить задачи завершаться раньше, когда вы решите, что вам все-таки не нужны результаты). Похоже, что новичков привлекают такие функции, как мотыльки к пламени. Не делай этого... У Херба Саттера есть отличная короткая статья на эту тему.
Несколько ответов касались этого, но я хотел бы подчеркнуть один момент: если вы можете, убедитесь, что как можно больше ваших данных доступно только из одного потока за один раз. Очереди сообщений - очень полезная конструкция для этого.
Мне не приходилось писать много многопоточного кода на C++, но в целом паттерн "производитель-потребитель" может быть очень полезен для эффективного использования нескольких потоков, избегая условий гонки, связанных с одновременным доступом.
Если вы можете использовать чужой уже отлаженный код для взаимодействия с потоками, вы в хорошей форме. Как новичок, есть искушение сделать что-то специальным образом - например, использовать переменную volatile для синхронизации между двумя частями кода. Избегайте этого как можно больше. При наличии конкурирующих потоков очень трудно писать код, который будет пуленепробиваемым, поэтому найдите какой-нибудь код, которому вы можете доверять, и максимально сократите использование низкоуровневых примитивов.
В дополнение к другим упомянутым вещам вы должны узнать об асинхронных очередях сообщений. Они могут элегантно решить проблемы обмена данными и обработки событий. Этот подход хорошо работает, когда у вас есть параллельные конечные автоматы, которые должны взаимодействовать друг с другом.
Я не знаю ни о каких платформах передачи сообщений, предназначенных для работы только на уровне потоков. Я видел только домашние решения. Пожалуйста, прокомментируйте, если вы знаете какие-либо из существующих.
РЕДАКТИРОВАТЬ:
Можно использовать очереди без блокировок из Intel TBB, как есть, или как основу для более общей очереди передачи сообщений.
Убедитесь, что явно знаете, какие объекты являются общими и как они являются общими.
По мере возможности сделайте свои функции чисто функциональными. То есть они имеют входы и выходы и не имеют побочных эффектов. Это значительно упрощает анализ вашего кода. С более простой программой это не так уж важно, но с ростом сложности она станет существенной. Побочные эффекты приводят к проблемам с безопасностью потоков.
Играет адвоката дьявола с вашим кодом. Посмотрите на некоторый код и подумайте, как я мог бы сломать это с помощью какого-то своевременного чередования потоков. В какой-то момент это случится.
Сначала изучите безопасность потоков. Как только вы добьетесь этого, вы перейдете к сложной части: одновременной работе. Именно здесь необходимо отойти от глобальных блокировок. Найти способы минимизации и снятия замков, сохраняя при этом безопасность нитей, сложно.
Держитесь подальше от MFC и его библиотеки многопоточности + обмена сообщениями.
На самом деле, если вы видите MFC и нити, идущие к вам - бегите за холмы (*)
(*) Если, конечно, если MFC идет с холмов - в этом случае убегайте от холмов.
На мой взгляд, самая большая разница между однопоточным и многопоточным программированием заключается в тестировании / проверке. В однопоточном программировании люди часто выдумывают какой-то не совсем продуманный код, запускают его, и, если он кажется работающим, они назовут его хорошим и часто сойдут с рук, используя его в производственной среде.
В многопоточном программировании, с другой стороны, поведение программы недетерминировано, потому что точная комбинация времени выполнения потоков, для которых периоды времени (относительно друг друга) будут отличаться при каждом запуске программы. Так что просто запустите многопоточную программу несколько раз (или даже несколько миллионов раз) и скажите "это не сработало для меня, отправьте!" совершенно неадекватно.
Вместо этого, когда вы выполняете многопоточную программу, вы всегда должны пытаться доказать (по крайней мере, к вашему собственному удовлетворению), что программа не только работает, но что нет способа, которым она могла бы не работать. Это гораздо сложнее, потому что вместо проверки одного пути кода вы фактически пытаетесь проверить почти бесконечное число возможных путей кода.
Единственный реалистичный способ сделать это, не взорвав мозг, - это сделать вещи настолько простыми, насколько это возможно. Если вы можете полностью избежать многопоточности, сделайте это. Если вы должны выполнять многопоточность, делиться как можно меньшим количеством данных между потоками и использовать надлежащие многопоточные примитивы (например, мьютексы, поточно-ориентированные очереди сообщений, условия ожидания) и не пытаться уйти с полумерами (например, пытаясь синхронизировать доступ к общему фрагменту данных с использованием только логических флагов никогда не будет работать надежно, поэтому не пытайтесь это сделать)
Чего вы хотите избежать, так это многопоточного адского сценария: многопоточная программа, которая успешно работает неделями подряд на вашем тестовом компьютере, но происходит случайным образом, примерно раз в год, на площадке заказчика. Такого рода ошибку в состоянии гонки можно почти невозможно воспроизвести, и единственный способ избежать этого - очень тщательно спроектировать код, чтобы гарантировать, что его не произойдет.
Нитки сильные жужу. Используйте их экономно.
Сохраняйте вещи простыми, насколько это возможно. Лучше иметь более простой дизайн (обслуживание, меньше ошибок), чем более сложное решение, которое могло бы иметь немного лучшую загрузку ЦП.
По возможности избегайте общего состояния между потоками, это уменьшает количество мест, которые должны использовать синхронизацию.
Избегайте ложного обмена любой ценой (Google этот термин).
Используйте пул потоков, чтобы не часто создавать / уничтожать потоки (это дорого и медленно).
Рассмотрите возможность использования OpenMP, Intel и Microsoft (возможно, другие) поддерживают это расширение для C++.
Если вы выполняете обработку чисел, рассмотрите возможность использования Intel IPP, который внутренне использует оптимизированные функции SIMD (на самом деле это не многопоточность, а параллелизм смежных типов).
Веселитесь.
Мне показалось полезным просмотр вводных лекций по ОС и системному программированию Джона Кубятовича в Беркли.
Вы должны иметь представление об основных системах программирования, в частности:
- Синхронный и асинхронный ввод-вывод (блокировка против неблокирования)
- Механизмы синхронизации, такие как конструкции блокировки и мьютекса
- Управление потоками на вашей целевой платформе
Прежде чем давать какие-либо советы о том, что можно делать и чего не делать в отношении многопоточного программирования на C++, я хотел бы задать вопрос. Есть ли какая-то конкретная причина, по которой вы хотите начать писать приложение на C++?
Существуют и другие парадигмы программирования, в которых вы используете многоядерные системы, не вдаваясь в многопоточное программирование. Одной из таких парадигм является функциональное программирование. Напишите каждый кусок вашего кода как функции без каких-либо побочных эффектов. Тогда его легко запустить в несколько потоков, не беспокоясь о синхронизации.
Я использую Erlang для моих целей разработки. Это увеличилось производительностью по крайней мере на 50%. Выполнение кода может быть не таким быстрым, как код, написанный на C++. Но я заметил, что для большей части внутренней обработки автономных данных скорость не так важна, как распределение работы и максимально возможное использование аппаратного обеспечения. Erlang предоставляет простую модель параллелизма, в которой вы можете выполнять одну функцию в нескольких потоках, не беспокоясь о проблеме синхронизации. Написание многопоточного кода - это легко, но отладка требует много времени. Я занимался многопоточным программированием на C++, но в настоящее время я доволен моделью параллелизма Erlang. Это стоит посмотреть.
Я в той же лодке, что и вы, я только впервые запускаю многопоточность как часть проекта, и я искал ресурсы в сети. Я нашел этот блог очень информативным. Часть 1 - это pthreads, но я связался, начиная с раздела наддува.
Часть моей аспирантуры связана с параллелизмом.
Я прочитал эту книгу и нашел хорошее резюме подходов на уровне дизайна.
На базовом техническом уровне у вас есть 2 основных варианта: темы или передача сообщений. Многопоточные приложения легче всего начать, так как pthreads, windows-потоки или потоки boost готовы к работе. Однако это приводит к сложности общей памяти.
На данный момент удобство передачи сообщений в основном ограничено API MPI. Он устанавливает среду, в которой вы можете запускать задания и распределять вашу программу между процессорами. Это больше для суперкомпьютерных / кластерных сред, где нет встроенной общей памяти. Вы можете добиться аналогичных результатов с сокетами и пр.
На другом уровне вы можете использовать прагмы языкового типа: популярной сегодня является OpenMP. Я не использовал его, но, похоже, он создает потоки с помощью предварительной обработки или библиотеки времени соединения.
Классическая проблема - синхронизация здесь; Все проблемы мультипрограммирования проистекают из недетерминированной природы мультипрограмм, чего нельзя избежать.
Посмотрите методы синхронизации Lamport для дальнейшего обсуждения синхронизации и синхронизации.
Многопоточность - это не то, что могут сделать только доктора наук и гуру, но вы должны быть достаточно приличными, чтобы делать это, не делая безумных ошибок.
Я написал многопоточное серверное приложение и многопоточную сортировку. Они оба были написаны на C и используют функции потоков в NT "raw", без какой-либо промежуточной библиотеки функций, чтобы запутать вещи. Это были два совершенно разных опыта с разными выводами. Высокая производительность и высокая надежность были главными приоритетами, хотя методы кодирования имели более высокий приоритет, если один из первых двух был оценен как находящийся под угрозой в долгосрочной перспективе.
Серверное приложение имеет как серверную, так и клиентскую части и использует iocps для управления запросами и ответами. При использовании iocps важно никогда не использовать больше потоков, чем у вас есть ядер. Также я обнаружил, что запросы к серверной части нуждаются в более высоком приоритете, чтобы не терять ненужные запросы. Когда они стали "безопасными", я мог использовать потоки с более низким приоритетом для создания ответов сервера. Я решил, что клиентская часть может иметь еще более низкий приоритет. Я задал вопросы "какие данные я не могу потерять?" и "какие данные я могу позволить потерпеть неудачу, потому что я всегда могу повторить попытку?" Мне также нужно было иметь возможность взаимодействовать с настройками приложения через окно, и оно должно было реагировать. Хитрость была в том, что пользовательский интерфейс имел обычный приоритет, входящие запросы были на один меньше и так далее. Я обосновал это тем, что, поскольку я редко использую пользовательский интерфейс, он может иметь наивысший приоритет, чтобы при его использовании он сразу реагировал. Поток здесь, оказалось, означал, что все отдельные части программы в обычном случае могли бы / могли работать одновременно, но когда система находилась под более высокой нагрузкой, вычислительная мощность была бы перенесена на жизненно важные части благодаря схеме расстановки приоритетов.
Мне всегда нравилась сортировка по ракушкам, поэтому, пожалуйста, избавьте меня от указаний о быстрой сортировке того или другого или блаблабла. Или о том, как шеллсорт плохо подходит для многопоточности. Сказав это, проблема, с которой я столкнулся при сортировке полубольшого списка единиц в памяти (для своих тестов я использовал список с обратной сортировкой по одному миллиону единиц по сорок байтов каждый. Используя однопоточную сортировку я мог сортировать они со скоростью примерно одна единица каждые две микросекунды. Моя первая попытка многопоточности была с двумя потоками (хотя я скоро понял, что я хотел быть в состоянии указать количество потоков), и она работала примерно на одну единицу каждый 3,5 секунды, то есть МЕДЛЕННО. Использование профилировщика очень помогло, и одним узким местом оказалось ведение статистики (т. Е. Сравнение и свопинг), где потоки сталкивались друг с другом. оказалось, что это самая большая проблема, и есть еще кое-что, что я могу сделать, например, разделение вектора, содержащего числа в единицах, в единицах, адаптированных к размеру строки кэша, и, возможно, также сравнение всех значений в двух строках кэша перед переходом к следующему. линия (по крайней мере, я думаю, что я могу кое-что сделать там - алгоритмы становятся довольно сложными). В итоге я достиг скорости в одну единицу каждую микросекунду с тремя одновременными потоками (четыре потока примерно одинаковы, у меня было только четыре доступных ядра).
Что касается первоначального вопроса, мой совет вам будет
- Если у вас есть время, изучите механизм потоков на самом низком уровне.
- Если важна производительность, изучите соответствующие механизмы, которые предоставляет ОС. Многопоточности само по себе достаточно редко, чтобы реализовать весь потенциал приложения.
- Используйте профилирование, чтобы понять причуды нескольких потоков, работающих в одной и той же памяти.
- Небрежная архитектурная работа убьет любое приложение, независимо от того, сколько ядер и систем у вас есть, и независимо от блеска ваших программистов.
- Небрежное программирование убьет любое приложение, независимо от блеска архитектурного фундамента.
- Следует понимать, что использование библиотек позволяет быстрее достичь цели разработки, но за счет меньшего понимания и (как правило) более низкой производительности.
Убедитесь, что вы знаете, что volatile
означает, и это использует (что может быть неочевидным на первый взгляд).
Кроме того, при разработке многопоточного кода помогает представить, что бесконечное количество процессоров выполняет каждую строку кода в вашем приложении одновременно. (эээ, каждая строка кода, которая возможна в соответствии с вашей логикой в вашем коде.) И что все, что не помечено как изменчивое, компилятор выполняет специальную оптимизацию для него, так что только поток, который его изменил, может читать / устанавливать его истинное значение и все остальные потоки получают мусор.