itmo_conspects

Программирование на C++ с элементами многопоточности

Лекция 1. Идиомы C++

Идиома RAII

С++ - язык с ручным управлением ресурсами, в нем нужно разработчику управлять выделением и освобождение памяти

Поэтому появилась идиома RAII (Resource Acquisition is Initialization) - “Получение ресурса есть инициализация”

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

Принципу RAII соответствуют умные указатели std::unique_ptr и мьютексы std::lock_guard. В случае мьютексов код, не следующии RAII, выглядит так:

{
    mutex1.lock()

    // критическая секция
    // здесь может быть return или throw

    mutex1.unlock()
}

Здесь нужно быть внимательным, чтобы mutex1.unlock() однозначно вызвался. std::lock_guard избавляет от этого:

{
    std::lock_guard lck(mutex1);
    // конструктор делает mutex1.lock() 

    // критическая секция

    // деструктор делает mutex1.unlock()
}

Деструктор вызывается в любом случае при выходе из скоупа, поэтому мьютекс будет однозначно разблокирован

Уникальный указатель std::unique_ptr

Умные указатели std::unique_ptr и пара std::shared_ptr-std::weak_ptr также следуют идиоме RAII и используются для управления памяти

Уникальный указатель std::unique_ptr владеет объектом единолично. Копирование такое указателя запрещено, но разрешено перемещение

std::unique_ptr создается с помощью std::make_unique, а не через конструктор. Рассмотрим такой пример:

Foo(new Bar(), std::unique_ptr(new Boo()));

Стандарт C++ не указывает, в каком порядке должны быть созданы объекты-аргументы, поэтому может случиться такая ситуация:

  1. Создается new Bar()
  2. Создается new Boo(), выделив память, конструктор которого вызывает исключение
  3. Создается std::unique_ptr(new Boo())

Исключение перехватывается на уровне выше по стеку вызовов, но память, выделившаяся в конструкторе new Bar(), не освободиться, поэтому возникнет утечка памяти. Вместо этого лучше использовать std::make_unique:

Foo(std::make_unique<Bar>(), std::make_unique<Boo>());

Разделяемый указатель std::shared_ptr и слабый указатель std::weak_ptr

Разделяемый указатель std::shared_ptr используется, когда объект должен иметь несколько владельцев. Каждый разделяемый указатель увеличивает счетчик ссылок при создании и копировании, уменьшает при уничтожении, а когда счетчик становится равен нулю - объект уничтожается

Рекомендуется создавать std::shared_ptr с помощью std::make_shared по тем же причинам

Вместе с std::shared_ptr в комплекте идет тип слабого указателя. Слабый указатель std::weak_ptr создан для того, чтобы избавиться от кольцевых зависимостей

struct B;

struct A {
    std::shared_ptr<B> b;
};

struct B {
    std::shared_ptr<A> a;
};

Если A и B ссылаются друг на друга через std::shared_ptr, счетчик ссылок никогда не станет нулем, и возникнет утечка памяти

Слабый указатель weak_ptr используется вместе с shared_ptr, но сам по себе ничего не хранит. Вместо этого можно вызвать метод .lock(), который вернет shared_ptr, если объект существует внутри, или nullptr, если он был уничтожен (то есть больше нет живых std::shared_ptr):

std::shared_ptr<Foo> shptr = std::make_shared<Foo>();

// ...

std::weak_ptr<Foo> wptr = shptr;

if (auto sptr = wptr.lock()) {
    // объект еще существует
} else {
    // объект уже уничтожен
}

std::shared_ptr при создании через std::make_shared выделяет один непрерывный блок памяти c самим объектом, счетчиком разделяемых указателей и счетчиком слабых указателей. Это эффективнее по памяти и быстрее по времени

Если счетчик std::shared_ptr становится равен нулю, объект уничтожается, но управляющий блок (в котором хранятся счетчики) остается в памяти, пока существуют std::weak_ptr

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

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

Функтор

Также для создания программ на C++ полезны функторы

Функтор (functor) — это объект, который можно вызывать как функцию. Он реализует оператор operator()

Функторы реализуют паттерн «Команда» — объект, хранящий в себе функцию и ее состояние.

Пример:

struct Add {
    int x;
    Add(int x) : x(x) {}

    int operator()(int y) const {
        return x + y;
    }
};

int main() {
    Add number(10);

    std::cout << number(5); // 15
}

Потоки

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