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!