Резюме: ускоренный курс по многопоточным конструкциям C ++ 14 в очень невыносимой форме.

Новые конструкции многопоточности C ++ очень просты в освоении. Если вы знакомы с C или C ++ и хотите начать писать многопоточные программы, эта статья для вас!

Я использую C ++ 14 в качестве справочника, но то, что я описываю, также поддерживается в C ++ 17. Я рассматриваю только общие конструкции. Прочитав это, вы сможете писать свои собственные многопоточные программы.

Обновление (июнь 2020 г.):

Я создал несколько видеороликов о многопоточности C ++. Посмотрите их здесь:

Создание потоков

Поток можно создать несколькими способами:

  1. Использование указателя на функцию
  2. Использование функтора
  3. Использование лямбда-функции

Эти методы очень похожи с небольшими отличиями. Далее я объясню каждый метод и их различия.

Использование указателя на функцию

Рассмотрим следующую функцию, которая принимает векторную ссылку v, ссылку на результат acm и два индекса в векторе v. Функция добавляет все элементы между beginIndex и endIndex.

Теперь предположим, что вы хотите разделить вектор на две части и вычислить общую сумму каждой секции в отдельном потоке t1 и t2:

Что нужно забрать?

  1. std::thread создает новый поток. Первый параметр - это имя указателя функции accumulator_function2. Следовательно, каждый поток будет выполнять эту функцию.
  2. Остальные параметры, переданные конструктору std::thread, являются параметрами, которые нам нужно передать accumulator_function2.
  3. Важно: все параметры, передаваемые в accumulator_function2, передаются по значению, если вы не заключили их в std::ref. Вот почему мы заключили v, acm1 и acm2 в std::ref.
  4. Потоки, созданные std::thread, не имеют возвращаемых значений. Если вы хотите что-то вернуть, вы должны сохранить это в одном из параметров, переданных по ссылке, то есть acm.
  5. Каждый поток запускается, как только он создается.
  6. Мы используем функцию join(), чтобы дождаться завершения потока

Использование функторов

Точно то же самое можно сделать с помощью функторов. Ниже приведен код, в котором используется функтор:

И код, который создает потоки:

Что нужно забрать?

Все очень похоже на указатель на функцию, за исключением того, что:

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

Использование лямбда-функций

В качестве третьей альтернативы мы можем определить каждый поток в лямбда-функции, как показано ниже:

Опять же, все очень похоже на указатель на функцию, за исключением того, что:

  1. В качестве альтернативы передаче параметра мы можем передавать ссылки на лямбда-функции с помощью лямбда-захвата.

Задачи, будущее и обещания

В качестве альтернативы std::thread можно использовать tasks.

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

Ниже представлен тот же пример, написанный с использованием задач:

Что нужно забрать?

  1. Задачи определяются и создаются с использованием std::async, (вместо потоков, которые создаются с использованием std::thread)
  2. Возвращаемое значение из std::async называется std::future. Не пугайтесь его названия. Это просто означает, что t1 и t2 - это переменные, значение которых будет присвоено в будущем. Мы получаем их значения, вызывая t1.get() и t2.get()
  3. Если будущие значения не готовы, при вызове get() основной поток блокируется до тех пор, пока будущее значение не станет готовым (аналогично join()).
  4. Обратите внимание, что функция, которую мы передали std::async, возвращает значение. Это значение передается через тип с именем std::promise. Опять же, не пугайтесь его названия. По большей части вам не нужно знать подробности std::promise или определять какую-либо переменную типа std::promise.. Библиотека C ++ делает это за кулисами.
  5. Каждая задача по умолчанию запускается, как только она создается (есть способ изменить это, я не рассматриваю).

Резюме создания потоков

Вот и все. Создавать потоки так же просто, как я объяснил выше. Вы можете использовать std::thread:

  1. Используйте указатели на функции
  2. Используйте функторы
  3. Используйте лямбда-функции

Или вы можете использовать std::async, чтобы создать задачу и получить возвращаемые значения в std::future. Задачи также могут использовать указатель на функцию, функтор или лямбда-функцию.

Общая память и общие ресурсы

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

C ++ 14 предоставляет несколько конструкций для синхронизации потоков, чтобы избежать таких состояний гонки.

Использование Mutex, lock, () и unlock () (не рекомендуется)

В следующем коде показано, как мы создаем критическую секцию, чтобы каждый поток имел доступ только std::cout:

Что нужно забрать?

  1. Создан мьютекс std::mutex
  2. Критический раздел (т.е. гарантированно запускаемый только одним потоком в каждый момент времени) создается с использованием lock()
  3. Критический раздел заканчивается после вызова unlock()
  4. Каждый поток ожидает lock() и входит в критическую секцию только в том случае, если в этой секции нет другого потока.

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

  1. Это не безопасно для исключений: если код перед блокировкой генерирует исключение, unlock() не будет выполняться, и мы никогда не освобождаем мьютекс, который может вызвать взаимоблокировку.
  2. Мы всегда должны быть осторожны, чтобы не забыть позвонить unlock()

Использование std :: lock_guard (рекомендуется)

Не пугайтесь его названия lock_guard. Это просто более абстрактный способ создания критических разделов.

Ниже приведен такой же критический раздел с использованием lock_guard.

Что нужно забрать?

  1. Код, поступающий после создания std::lock_guard, автоматически блокируется. Нет необходимости в явном вызове функций lock() и unlock().
  2. Критическая секция автоматически завершается, когда std::lock_guard выходит за рамки. Это делает его безопасным в исключительных случаях, а также нам не нужно помнить о звонке unlock()
  3. lock_guard по-прежнему требует использования переменной типа std::mutex в своем конструкторе.

Сколько потоков мы должны создать?

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

Чтобы получить максимальное количество ядер, вы можете позвонить: std::thread::hardware_concurrency(), как показано ниже:

Что я не накрыл

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

  1. std :: move
  2. детали std :: обещания
  3. std :: packaged_task
  4. Условные переменные

Надеюсь, это поможет вам быстро изучить многопоточность C ++.

Если вам понравилась эта статья, нажмите на нее и оставьте отзыв.

Посмотрите мое другое видео об алгоритме поиска с возвратом и его реализации на C ++: