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

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

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

Основы

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

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

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

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

Разработка вашей первой многопоточной программы

Использовать потоки на языке C действительно просто: есть структура данных (pthread_t), которая представляет 1 поток, который должен выполняться процессором, и есть набор функций, которые выполняют операции над ними. Вот пример:

Как видите, использовались две важные функции: pthread_create и pthread_join. Они позволяют программисту соответственно создавать и ждать результата потока.

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

fn должен возвращать указатель, чтобы компилятор не возвращал ошибки. Второй аргумент в pthread_join может использоваться для получения возвращаемого значения функции, выполненной потоком.

Понимание и устранение проблем с общей памятью

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

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

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

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

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

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

Чтобы мьютекс был хорош, он должен удовлетворять определенным требованиям:

  • Безопасность - только 1 поток одновременно может удерживать мьютекс.
  • Избегайте взаимоблокировок - если несколько потоков пытаются заблокировать мьютекс, то хотя бы один из них должен иметь возможность заблокировать его.
  • Избегайте голодания - если поток пытается заблокировать его, он в конечном итоге его получит (он не будет бесконечно ждать)

И взаимоблокировки, и голодание - общие проблемы при разработке многопоточных приложений.

Как использовать мьютекс

Создать мьютекс так же просто, как создать поток - есть структура и функции, которые сделают это за вас. И теперь мы можем исправить предыдущую проблему:

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

Теперь ваш банк в полной безопасности, и люди больше не могут его использовать из-за ваших дополнительных мер по его защите!

Если вы все еще хотите узнать больше…

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

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

Не стесняйтесь обращаться ко мне, если у вас есть какие-либо вопросы по этой теме, я сделаю все возможное, чтобы помочь вам понять это! Вы можете прокомментировать этот пост, и я отвечу вам, или вы можете найти меня онлайн в Twitter!