Какие "вещи нужно знать" при погружении в многопоточное программирование на С++

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

Что должен знать каждый программист при написании многопоточного кода на С++?

Ответ 1

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

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

Ответ 2

Я вообще не эксперт в этом вопросе. Просто какое-то правило:

1) Дизайн для простоты. Ошибки действительно трудно найти в параллельном коде даже в самых простых примерах. 2) С++ предлагает вам очень элегантную парадигму для управления ресурсами (мьютекс, семафор,...): RAII. Я заметил, что работать с boost::thread гораздо легче, чем работать с потоками POSIX.
3) Создайте свой код как потокобезопасный. Если вы этого не сделаете, ваша программа может вести себя странно.

Ответ 3

Я точно в этой ситуации: я написал библиотеку с глобальной блокировкой (многие потоки, но только один запустился за раз в библиотеке), и я реорганизую ее для поддержки concurrency.

Я читал книги по этому предмету, но то, что я узнал, стоит в нескольких точках:

  • думать параллельно: представьте, что толпа проходит через код. Что происходит, когда метод вызывается уже в действии?
  • подумайте об общем: представьте, что многие люди пытаются одновременно читать и изменять общие ресурсы.
  • дизайн: избегайте проблем, которые могут повышать точки 1 и 2.
  • Никогда не думайте, что вы можете игнорировать случаи кросс, они вас укусят.

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

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

  • Mutex (эксклюзивный доступ к ресурсу)
  • Скопированные блокировки (хороший шаблон для блокировки/разблокировки Mutex)
  • Семафоры (передача информации между потоками)
  • ReadWrite Mutex (многие читатели, эксклюзивный доступ к записи)
  • Сигналы (как "убить" поток или отправить ему сигнал прерывания, как его поймать)
  • Параллельные шаблоны проектирования: босс/работник, производитель/потребитель и т.д. (см. schmidt)
  • специальные инструменты платформы: блоки openMP, C и т.д.

Удачи! Concurrency - это весело, просто не торопитесь...

Ответ 4

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

Одно из советов, если ваше приложение имеет какой-либо интерфейс, убедитесь, что вы всегда меняете его из потока пользовательского интерфейса. Большинство инструментов/фреймворков пользовательского интерфейса будут аварийно завершаться (или вести себя непредвиденно), если вы будете обращаться к ним из фонового потока. Обычно они предоставляют некоторую форму диспетчерского метода для выполнения некоторой функции в потоке пользовательского интерфейса.

Ответ 5

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

Следующая точка связана с языком. Помните, что на С++ (в настоящее время) нет четко определенного подхода к потоковому использованию. Компилятор/оптимизатор не знает, может ли код вызываться одновременно. Ключевое слово volatile полезно для предотвращения определенных оптимизаций (например, кэширования полей памяти в регистры CPU) в многопоточных контекстах, но это механизм синхронизации нет.

Я бы рекомендовал boost для примитивов синхронизации. Не связывайтесь с API-интерфейсами платформы. Они затрудняют перенос кода, поскольку они имеют аналогичную функциональность на всех основных платформах, но немного отличаются друг от друга. Boost решает эти проблемы, открывая пользователю только общие функции.

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

Ответ 6

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

Ответ 7

Убедитесь, что вы тестируете свой код в системе с одним процессором и системой с несколькими процессорами.

Основываясь на комментариях: -

  • Один сокет, одноядерный
  • Одиночный разъем, два ядра
  • Единый сокет, более двух ядер
  • Два разъема, одноядерный каждый
  • Два сокета, комбинация одно-, двух- и многоядерных процессоров
  • Сокеты Mulitple, комбинация одно-, двух- и многоядерных процессоров

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

Ответ 8

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

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

EDIT:

Можно использовать свободные от очереди очереди Intel TBB, либо как есть, либо как основу для более общего сообщения, очереди передачи.

Ответ 9

Так как вы новичок, начните просто. Сначала сделайте так, чтобы он работал корректно, а затем подумайте о оптимизации. Я видел, как люди пытались оптимизировать, увеличивая concurrency конкретного раздела кода (часто используя сомнительные трюки), не пытаясь увидеть, было ли вообще какое-либо утверждение.

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

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

Ответ 10

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

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

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

Ответ 11

Мои главные советы для новичков:

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

  • Знайте, как прервать потоки (или в случае TBB, как сделать задачи завершенными раньше, когда вы решите, что не хотите, чтобы результаты были окончательными). Кажется, что новички привлекаются к функциям уничтожения нитей, например, мотылькам. Не делайте этого... Herb Sutter имеет большую короткую статью об этом.

Ответ 12

Удостоверьтесь, что явным образом знаю, какие общие объекты и как они совместно используются.

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

Играет дьявола с вашим кодом. Посмотрите на какой-то код и подумайте, как я могу сломать его с помощью некорректного чередования потоков. В какой-то момент это произойдет.

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

Ответ 13

Держитесь подальше от MFC и многопотоковой библиотеки обмена сообщениями.
Фактически, если вы видите MFC и потоки, идущие к вам - бегите за холмами (*)

(*) Если, конечно, если МФЦ не идет с холмов - в этом случае запустите AWAY с холмов.

Ответ 14

Держите вещи как можно меньше. Лучше иметь более простой дизайн (обслуживание, меньше ошибок), чем более сложное решение, которое может немного улучшить загрузку процессора.

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

Избегайте ложного обмена любой ценой (google этот термин).

Используйте пул потоков, чтобы не часто создавать/уничтожать потоки (что дорого и медленно).

Рассмотрим использование OpenMP, Intel и Microsoft (возможно, другие) поддерживают это расширение на С++.

Если вы делаете хруст числа, подумайте об использовании Intel IPP, который внутренне использует оптимизированные функции SIMD (на самом деле это не многопоточность, а parallelism связанных ролей).

Иметь массу удовольствия.

Ответ 15

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

В многопоточном программировании, с другой стороны, поведение программы не является детерминированным, потому что точное сочетание времени, в течение которого потоки выполняются, для каких периодов времени (относительно друг друга) будет отличаться при каждом запуске программы, Так что просто запускайте многопоточную программу несколько раз (или даже несколько миллионов раз) и говорив: "Это не сбой для меня, отправьте его!" полностью неадекватна.

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

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

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

Нитки сильные juju. Используйте их экономно.

Ответ 16

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

  • Синхронный и асинхронный ввод-вывод (блокировка и неблокирование)
  • Механизмы синхронизации, такие как блокировки и мьютексы
  • Управление потоками на целевой платформе

Ответ 17

Я нашел просмотр вводных лекций по OS и системному программированию здесь от Джона Кубятовича в Беркли.

Ответ 18

Часть моей области обучения выпускников относится к parallelism.

Я прочитал эту книгу и нашел это хорошее резюме подходов на уровне разработки.

На базовом техническом уровне у вас есть 2 основных варианта: потоки или передача сообщений. Резьбовые приложения легче всего сбрасывать с земли, так как pthreads, потоки окон или потоки потока готовы к работе. Однако это приносит с собой сложность разделяемой памяти.

Удобство использования передачи сообщений, по-видимому, в настоящее время ограничено API MPI. Он создает среду, в которой вы можете запускать задания и разбивать свою программу между процессорами. Это больше для суперкомпьютерных/кластерных сред, где нет встроенной общей памяти. Вы можете добиться аналогичных результатов с помощью сокетов и т.д.

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

Классическая проблема - синхронизация здесь; все проблемы в мультипрограммировании исходят из недетерминированного характера мультипрограмм, чего нельзя избежать.

См. методы синхронизации Lamport для дальнейшего обсуждения синхронизации и синхронизации.

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

Ответ 19

Прежде чем давать какие-либо советы о do и dont о многопоточном программировании на С++, я хотел бы задать вопрос. Есть ли какая-то конкретная причина, по которой вы хотите начать писать приложение на С++?

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

Я использую Erlang для своей цели разработки. Он увеличился по производительности не менее чем на 50%. Выполнение кода может быть не таким быстрым, как код, написанный на С++. Но я заметил, что для большей части автономной обработки данных в автономном режиме скорость не так важна, как распределение работы и использование аппаратного обеспечения в максимально возможной степени. Erlang предоставляет простую модель concurrency, где вы можете выполнить одну функцию в нескольких потоках, не беспокоясь о проблеме синхронизации. Написание многопоточного кода легко, но отладка требует много времени. Я сделал многопоточное программирование на С++, но в настоящее время я доволен моделью Erlang concurrency. Это стоит изучить.

Ответ 20

Я нахожусь в той же лодке, что и вы, я только начинаю многопоточность в первый раз как часть проекта, и я просматриваю сеть за ресурсы. Я нашел этот блог, чтобы быть очень информативным. Часть 1 - pthreads, но я связал начало в секции boost.

Ответ 21

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

Серверное приложение имело как серверную, так и клиентскую часть и использовало iocps для управления запросами и ответами. При использовании iocps важно никогда не использовать больше потоков, чем у вас есть ядра. Также я обнаружил, что запросы к серверной части требуют более высокого приоритета, чтобы не потерять лишние запросы. Когда они были "безопасными", я мог бы использовать потоки с более низким приоритетом для создания ответов сервера. Я судил, что клиентская часть может иметь еще более низкий приоритет. Я задал вопросы: "Какие данные я не могу потерять?" и "какие данные я могу разрешить, потому что я всегда могу повторить попытку?" Я также должен был иметь возможность взаимодействовать с настройками приложения через окно, и он должен был быть отзывчивым. Хитрость заключалась в том, что пользовательский интерфейс имел нормальный приоритет, входящие запросы меньше и так далее. Мое рассуждение в этом заключалось в том, что, поскольку я буду использовать интерфейс так редко, он может иметь наивысший приоритет, так что, когда я его использую, он немедленно ответит. Резьба здесь оказалась означающей, что все отдельные части программы в обычном случае будут/могут работать одновременно, но когда система находится под более высокой нагрузкой, вычислительная мощность будет смещена на важные части из-за схемы установления приоритетов.

Мне всегда нравился shellsort, поэтому, пожалуйста, избавьте меня от указателей на quicksort того или другого или blablabla. Или о том, как shellsort плохо подходит для многопоточности. Сказав это, возникла проблема, связанная с сортировкой полумастериста единиц в памяти (для моих тестов я использовал отсортированный по списку список из миллиона единиц по 40 байт каждый). Используя однопоточную оболочку, я мог сортировать их с частотой примерно одна единица каждые два (микросекунды). Моя первая попытка многопоточности состояла из двух потоков (хотя вскоре я понял, что хочу указать количество потоков), и она выполнялась примерно на единицу 3,5 секунды, то есть SLOWER. Использование профилировщика помогло многим, и одно узкое место оказалось статистическим протоколированием (т.е. сравнениями и свопами), где потоки будут сталкиваться друг с другом. Разделение данных между потоками в эффективном путь оказался самой большой проблемой, и я могу сделать больше, чем делить вектор, содержащий индексы, на единицы в размерах адаптированных кэш-линий и, возможно, также сравнивать все индексы в двух строках кэша, прежде чем переходить к следующему линии (по крайней мере, я думаю, что я могу там что-то сделать - алгоритмы усложняются). В конце концов, я достигла скорости по одной единице за каждую микросекунду с тремя одновременными потоками (четыре нити примерно одинаковые, у меня было только четыре ядра).

Что касается первоначального вопроса, то мой совет вам будет

  • Если у вас есть время, изучите механизм резьбы на минимально возможном уровне.
  • Если производительность важна, изучите связанные механизмы, которые предоставляет ОС. Многопоточность сама по себе достаточно редко, чтобы обеспечить полный потенциал приложения.
  • Используйте профилирование, чтобы понять причуды нескольких потоков, работающих в одной и той же памяти.
  • Небрежная архитектурная работа убьет любое приложение, независимо от того, сколько ядер и систем вы его выполняете, и независимо от яркости ваших программистов.
  • Небрежное программирование убьет любое приложение, независимо от блеска архитектурного фундамента.
  • Понимать, что использование библиотек позволяет быстрее достичь цели разработки, но ценой меньшего понимания и (как правило) снижения производительности.

Ответ 22

Убедитесь, что вы знаете, что означает volatile и он использует (что может быть не очевидно вначале).

Кроме того, при разработке многопоточного кода он позволяет предположить, что бесконечное количество процессоров выполняет каждую отдельную строку кода в вашем приложении сразу. (er, каждая строка кода, которая возможна в соответствии с вашей логикой в ​​вашем коде.) И что все, что не отмечено volatile, компилятор делает специальную оптимизацию на нем, так что только поток, который его изменил, может читать/устанавливать его истинное значение и все другие потоки получают мусор.