Что нужно знать о многопоточности в C ++

  • 14 сентября, 15:04
  • 4396
  • 0

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

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

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

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

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

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

void accumulator_function2(const std::vector<int> &v, unsigned long long &acm, 
                            unsigned int beginIndex, unsigned int endIndex)
{
    acm = 0;
    for (unsigned int i = beginIndex; i < endIndex; ++i)
    {
        acm += v[i];
    }
}

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

//Pointer to function
    {
        unsigned long long acm1 = 0;
        unsigned long long acm2 = 0;
        std::thread t1(accumulator_function2, std::ref(v), 
                        std::ref(acm1), 0, v.size() / 2);
        std::thread t2(accumulator_function2, std::ref(v), 
                        std::ref(acm2), v.size() / 2, v.size());
        t1.join();
        t2.join();


        std::cout << "acm1: " << acm1 << endl;
        std::cout << "acm2: " << acm2 << endl;
        std::cout << "acm1 + acm2: " << acm1 + acm2 << endl;
    }

Что нужно отнять?

  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() функцию ожидания завершения потока

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

Вы можете сделать то же самое, используя функторы. Ниже приведен код, который его использует:

class CAccumulatorFunctor3
{
  public:
    void operator()(const std::vector<int> &v, 
                    unsigned int beginIndex, unsigned int endIndex)
    {
        _acm = 0;
        for (unsigned int i = beginIndex; i < endIndex; ++i)
        {
            _acm += v[i];
        }
    }
    unsigned long long _acm;
}

Functor Definition

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

//Creating Thread using Functor
    {

        CAccumulatorFunctor3 accumulator1 = CAccumulatorFunctor3();
        CAccumulatorFunctor3 accumulator2 = CAccumulatorFunctor3();
        std::thread t1(std::ref(accumulator1), 
            std::ref(v), 0, v.size() / 2);
        std::thread t2(std::ref(accumulator2), 
            std::ref(v), v.size() / 2, v.size());
        t1.join();
        t2.join();

        std::cout << "acm1: " << accumulator1._acm << endl;
        std::cout << "acm2: " << accumulator2._acm << endl;
        std::cout << "accumulator1._acm + accumulator2._acm : " << 
            accumulator1._acm + accumulator2._acm << endl;
    }

Что нужно отнять?

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

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

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

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

{
        unsigned long long acm1 = 0;
        unsigned long long acm2 = 0;
        std::thread t1([&acm1, &v] {
            for (unsigned int i = 0; i < v.size() / 2; ++i)
            {
                acm1 += v[i];
            }
        });
        std::thread t2([&acm2, &v] {
            for (unsigned int i = v.size() / 2; i < v.size(); ++i)
            {
                acm2 += v[i];
            }
        });
        t1.join();
        t2.join();

        std::cout << "acm1: " << acm1 << endl;
        std::cout << "acm2: " << acm2 << endl;
        std::cout << "acm1 + acm2: " << acm1 + acm2 << endl;
    }

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

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

Задачи, Фьючерсы и Обещания

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

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

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

#include <future>
//Tasks, Future, and Promises
    {
        auto f1 = [](std::vector<int> &v, 
            unsigned int left, unsigned int right) {
            unsigned long long acm = 0;
            for (unsigned int i = left; i < right; ++i)
            {
                acm += v[i];
            }

            return acm;
        };

        auto t1 = std::async(f1, std::ref(v), 
            0, v.size() / 2);
        auto t2 = std::async(f1, std::ref(v), 
            v.size() / 2, v.size());

        //You can do other things here!
        unsigned long long acm1 = t1.get();
        unsigned long long acm2 = t2.get();

        std::cout << "acm1: " << acm1 << endl;
        std::cout << "acm2: " << acm2 << endl;
        std::cout << "acm1 + acm2: " << acm1 + acm2 << endl;
    }

Что нужно отнять?

  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. Каждая задача по умолчанию запускается сразу после ее создания.

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

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

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

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

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

std::mutex g_display_mutex;
thread_function()
{

    g_display_mutex.lock();
    std::thread::id this_id = std::this_thread::get_id();
    std::cout << "My thread id is: " << this_id  << endl;
    g_display_mutex.unlock();
}

Что нужно отнять?

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

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

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

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

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

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

std::mutex g_display_mutex;
thread_function()
{
    std::lock_guard<std::mutex> guard(g_display_mutex);
    std::thread::id this_id = std::this_thread::get_id();
    std::cout << "From thread " << this_id  << endl;
}

Что нужно отнять?

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

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

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

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

{
    unsigned int c = std::thread::hardware_concurrency();
    std::cout << " number of cores: " << c << endl;;
}

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

  1. std::move
  2. details of std::promise
  3. std::packaged_task
  4. условные переменные

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


0 комментариев
Сортировка:
Добавить комментарий

IT Новости

Смотреть все