C++ ScopeLock

Nikita Zubkov
2 min readJul 11, 2015

--

Поговорим о блокировках. Тех, которые блокируют один поток (thread), пока другой что-то делает с общим ресурсом. И не о самих блокировках, а о коде, в котором они присутствуют.

Сам тип объекта блокировки (mutex, spinlock или что-то другое) не важен, главное, что бы его можно было захватить (lock) и освободить (unlock).

Наивный код использующий блокировку выглядит примерно так:

bool use_shared_resource_concurrently() {
lockobj.lock();
use_shared_resource();
lockobj.unlock();
return true;
}

Просто и понятно, но есть проблемы.

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

bool use_shared_resource_concurrently() {
lockobj.lock();
use_shared_resource();
lockobj.lock(); // должен быть unlock
return true;
}

или вообще забыть про разблокировку:

bool use_shared_resource_concurrently() {
lockobj.lock();
use_shared_resource();
// забыл про unlock
return true;
}

Вторая проблема появляется если мы добавим немного бизнес логики. Например:

bool use_shared_resource_concurrently() {
lockobj.lock();
if (!can_use_shared_resource()) return false;
use_shared_resource();
lockobj.unlock();
return true;
}

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

Эта проблема решаема:

bool use_shared_resource_concurrently() {
lockobj.lock();
if (!can_use_shared_resource()) {
lockobj.unlock();
return false;
}
use_shared_resource();
lockobj.unlock();
return true;
}

Но код сразу становиться запутаннее и сложнее.

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

void use_shared_resource() {
if (something_went_wrong)
throw Exception();
}
...bool use_shared_resource_concurrently() {
lockobj.lock();
if (!can_use_shared_resource()) {
lockobj.unlock();
return false;
}
use_shared_resource();
lockobj.unlock();
return true;
}

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

Можно, конечно, обработать это исключение:

bool use_shared_resource_concurrently() {
lockobj.lock();
try {
if (!can_use_shared_resource()) {
lockobj.unlock();
return false;
}
use_shared_resource();
}
catch (...) {
lockobj.unlock();
throw;
}
return true;
}

но такое лучше не показывать беременным. Хотя беременным лучше вообще код на C++ не показывать.

Что делать, если хочется и мутекс залочить, и кода поменьше написать? Использовать RAII! Что это такое описывать не буду, все есть по ссылке на Википедию.

Если коротко, то это принцип, что захват ресурса происходит в конструкторе объекта, а освобождение в деструкторе.

В этом случае наш код будет выглядеть так:

bool use_shared_resource_concurrently() {
ScopeLock scope_lock(lockobj);
if (!can_use_shared_resource()) return false;
use_shared_resource();
return true;
}

При создании объекта происходит вызов конструктора и в нем захват блокировки. Далее, по правилам C++ при выходе из функции (не важно как) все объекты, которые объявлены в ней, будут уничтожены посредством вызова их деструкторов. В этот момент произойдет освобождение блокировки.

Это работает для любой области видимости. Например, внутри if:

void use_shared_resource_concurrently(bool concurrently = true) {
if (concurrently) {
ScopeLock scope_lock(lockobj);
use_shared_resource();
}
else {
use_shared_resource();
}
}

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

Сама реализация ScopeLock тривиальна до нельзя:

class ScopeLock {
public:
inline ScopeLock(LockObj& lockobj)
: _lockobj(lockobj)
{
_lockobj.lock();
}

inline ~ScopeLock() {
_lockobj.unlock();
}

protected:
LockObj& _lockobj;

private:
// Запрет на копирование объекта.
ScopeLock(const ScopeLock&);
void operator=(const ScopeLock&);
};

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

Happy coding!

--

--