C++ ScopeLock
Поговорим о блокировках. Тех, которые блокируют один поток (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!