C++ ScopeUnlock

Кто о чем, а я все о блокировках. Или о RAII, кому как больше нравится.

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


Вернемся к коду использующему ScopeLock:

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

Допустим нам нужно расширить логику и добавить туда обращение к еще одному ресурсу.

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

Вроде не сложно. Но оказывается use_other_resource() выполняется долго и держать нашу блокировку все это время уже не нужно. Не вопрос, добавим снятие и возврат блокировки:

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

Ничего не напоминает? Снова эта парочка, которая должна быть сбалансированна. Только теперь они поменялись местами.

Кто-то может подумать, что это не страшно, если из-за исключения до lockobj.lock() управление не дойдет, ведь в данном случае висячей блокировки не появится. Но проблема тут есть.

После того как мы вызвали lockobj.unlock() мы больше не владеем блокировкой и не можем её освобождать повторно (а это произойдет ведь деструктор scope_lock будет вызван и в этом случае). Так вот, если на этот момент блокировку успел захватить второй поток, а мы ее освободим, то ей может завладеть третий поток (или этот же поток при повторном вызове). В этом случае мы окажемся в ситуации, когда второй и третий потоки одновременно обращаются к общему ресурсу. Именно этой ситуации мы и хотели избежать вводя блокировку.

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

Призовем на помощь старого друга — RAII:

class ScopeUnlock {
public:
inline ScopeUnlock(LockObj& lockobj)
: _lockobj(lockobj)
{
_lockobj.unlock(); // тут unlock, а не lock
}

inline ~ScopeUnlock() {
_lockobj.lock(); // тут lock, а не unlock
}

protected:
LockObj& _lockobj;

private:
ScopeUnlock(const ScopeUnlock&);
void operator=(const ScopeUnlock&);
};

Мы взяли ScopeLock и поменяли местами код конструктора и деструктора.

Формально это уже не RAII, ведь мы не захватываем объект в конструкторе, но я думаю что такое буквоедство тут никому не нужно. :)

Переделаем наш код:

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

Вновь код стал короче, читабельнее и безопаснее. Все как мы любим.

Есть тут правда небольшая неэффективность. Если исключение все же произойдет, то будет произведено два ненужных вызова: lockobj.lock() и lockobj.unlock(). И если эта неэффективность действительно критична, то ScopeLock и ScopeUnlock придется заменить на ручное управление блокировкой и ручную обработку исключений. Но в большинстве случаев это мелочь по сравнению с висячей блокировкой или состоянием гонки между потоками.


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

Code responsibly!