Long live QSemaphoreReleaser!

This is a simple RAII class that makes semaphore releasing
reliable in the face of exceptions and early returns.

This code originates from KDTools' KDSemaphoreReleaser[1], but
has been extensively reworked to support C++11 move semantics.

[1] https://docs.kdab.com/kdtools/2.3.0/class_k_d_semaphore_releaser.html

[ChangeLog][QtCore][QSemaphore] Added a new RAII class, QSemaphoreReleaser,
to reliably perform release() calls.

Change-Id: I6aff64d37cc0882b17c4419817bde60b542f34d9
Reviewed-by: Mitch Curtis <mitch.curtis@qt.io>
Reviewed-by: David Faure <david.faure@kdab.com>
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
This commit is contained in:
Marc Mutz 2017-02-11 10:10:18 +01:00
parent 9c765522d1
commit f0ee4ed0a2
3 changed files with 240 additions and 2 deletions

View File

@ -94,7 +94,7 @@ QT_BEGIN_NAMESPACE
seated (taking the available seats to 5, making the party of 10
people wait longer).
\sa QMutex, QWaitCondition, QThread, {Semaphores Example}
\sa QSemaphoreReleaser, QMutex, QWaitCondition, QThread, {Semaphores Example}
*/
class QSemaphorePrivate {
@ -152,7 +152,10 @@ void QSemaphore::acquire(int n)
\snippet code/src_corelib_thread_qsemaphore.cpp 1
\sa acquire(), available()
QSemaphoreReleaser is a \l{http://en.cppreference.com/w/cpp/language/raii}{RAII}
wrapper around this function.
\sa acquire(), available(), QSemaphoreReleaser
*/
void QSemaphore::release(int n)
{
@ -234,6 +237,152 @@ bool QSemaphore::tryAcquire(int n, int timeout)
}
/*!
\class QSemaphoreReleaser
\brief The QSemaphoreReleaser class provides exception-safe deferral of a QSemaphore::release() call
\since 5.10
\ingroup thread
\inmodule QtCore
\reentrant
QSemaphoreReleaser can be used wherever you would otherwise use
QSemaphore::release(). Constructing a QSemaphoreReleaser defers the
release() call on the semaphore until the QSemaphoreReleaser is
destroyed (see
\l{http://en.cppreference.com/w/cpp/language/raii}{RAII pattern}).
You can use this to reliably release a semaphore to avoid dead-lock
in the face of exceptions or early returns:
\code
// ... do something that may throw or return early
sem.release();
\endcode
If an early return is taken or an exception is thrown before the
\c{sem.release()} call is reached, the semaphore is not released,
possibly preventing the thread waiting in the corresponding
\c{sem.acquire()} call from ever continuing execution.
When using RAII instead:
\code
const QSemaphoreReleaser releaser(sem);
// ... do something that may throw or early return
// implicitly calls sem.release() here and at every other return in between
\endcode
this can no longer happen, because the compiler will make sure that
the QSemaphoreReleaser destructor is always called, and therefore
the semaphore is always released.
QSemaphoreReleaser is move-enabled and can therefore be returned
from functions to transfer responsibility for releasing a semaphore
out of a function or a scope:
\code
{ // some scope
QSemaphoreReleaser releaser; // does nothing
// ...
if (someCondition) {
releaser = QSemaphoreReleaser(sem);
// ...
}
// ...
} // conditionally calls sem.release(), depending on someCondition
\endcode
A QSemaphoreReleaser can be canceled by a call to cancel(). A canceled
semaphore releaser will no longer call QSemaphore::release() in its
destructor.
\sa QMutexLocker
*/
/*!
\fn QSemaphoreReleaser::QSemaphoreReleaser()
Default constructor. Creates a QSemaphoreReleaser that does nothing.
*/
/*!
\fn QSemaphoreReleaser::QSemaphoreReleaser(QSemaphore &sem, int n)
Constructor. Stores the arguments and calls \a{sem}.release(\a{n})
in the destructor.
*/
/*!
\fn QSemaphoreReleaser::QSemaphoreReleaser(QSemaphore *sem, int n)
Constructor. Stores the arguments and calls \a{sem}->release(\a{n})
in the destructor.
*/
/*!
\fn QSemaphoreReleaser::QSemaphoreReleaser(QSemaphoreReleaser &&other)
Move constructor. Takes over responsibility to call QSemaphore::release()
from \a other, which in turn is canceled.
\sa cancel()
*/
/*!
\fn QSemaphoreReleaser::operator=(QSemaphoreReleaser &&other)
Move assignment operator. Takes over responsibility to call QSemaphore::release()
from \a other, which in turn is canceled.
If this semaphore releaser had the responsibility to call some QSemaphore::release()
itself, it performs the call before taking over from \a other.
\sa cancel()
*/
/*!
\fn QSemaphoreReleaser::~QSemaphoreReleaser()
Unless canceled, calls QSemaphore::release() with the arguments provided
to the constructor, or by the last move assignment.
*/
/*!
\fn QSemaphoreReleaser::swap(QSemaphoreReleaser &other)
Exchanges the responsibilites of \c{*this} and \a other.
Unlike move assignment, neither of the two objects ever releases its
semaphore, if any, as a consequence of swapping.
Therefore this function is very fast and never fails.
*/
/*!
\fn QSemaphoreReleaser::semaphore() const
Returns a pointer to the QSemaphore object provided to the constructor,
or by the last move assignment, if any. Otherwise, returns \c nullptr.
*/
/*!
\fn QSemaphoreReleaser::cancel()
Cancels this QSemaphoreReleaser such that the destructor will no longer
call \c{semaphore()->release()}. Returns the value of semaphore()
before this call. After this call, semaphore() will return \c nullptr.
To enable again, assign a new QSemaphoreReleaser:
\code
releaser.cancel(); // avoid releasing old semaphore()
releaser = QSemaphoreReleaser(sem, 42);
// now will call sem.release(42) when 'releaser' is destroyed
\endcode
*/
QT_END_NAMESPACE
#endif // QT_NO_THREAD

View File

@ -69,6 +69,45 @@ private:
QSemaphorePrivate *d;
};
class QSemaphoreReleaser
{
QSemaphore *m_sem = nullptr;
int m_n;
public:
QSemaphoreReleaser() = default;
explicit QSemaphoreReleaser(QSemaphore &sem, int n = 1) Q_DECL_NOTHROW
: m_sem(&sem), m_n(n) {}
explicit QSemaphoreReleaser(QSemaphore *sem, int n = 1) Q_DECL_NOTHROW
: m_sem(sem), m_n(n) {}
QSemaphoreReleaser(QSemaphoreReleaser &&other) Q_DECL_NOTHROW
: m_sem(other.m_sem), m_n(other.m_n)
{ other.m_sem = nullptr; }
QSemaphoreReleaser &operator=(QSemaphoreReleaser &&other) Q_DECL_NOTHROW
{ QSemaphoreReleaser moved(std::move(other)); swap(moved); return *this; }
~QSemaphoreReleaser()
{
if (m_sem)
m_sem->release(m_n);
}
void swap(QSemaphoreReleaser &other) Q_DECL_NOTHROW
{
qSwap(m_sem, other.m_sem);
qSwap(m_n, other.m_n);
}
QSemaphore *semaphore() const Q_DECL_NOTHROW
{ return m_sem; }
QSemaphore *cancel() Q_DECL_NOTHROW
{
QSemaphore *old = m_sem;
m_sem = nullptr;
return old;
}
};
#endif // QT_NO_THREAD
QT_END_NAMESPACE

View File

@ -42,6 +42,7 @@ private slots:
void tryAcquireWithTimeout();
void tryAcquireWithTimeoutStarvation();
void producerConsumer();
void raii();
};
static QSemaphore *semaphore = 0;
@ -415,5 +416,54 @@ void tst_QSemaphore::producerConsumer()
consumer.wait();
}
void tst_QSemaphore::raii()
{
QSemaphore sem;
QCOMPARE(sem.available(), 0);
// basic operation:
{
QSemaphoreReleaser r0;
const QSemaphoreReleaser r1(sem);
const QSemaphoreReleaser r2(sem, 2);
QCOMPARE(r0.semaphore(), nullptr);
QCOMPARE(r1.semaphore(), &sem);
QCOMPARE(r2.semaphore(), &sem);
}
QCOMPARE(sem.available(), 3);
// cancel:
{
const QSemaphoreReleaser r1(sem);
QSemaphoreReleaser r2(sem, 2);
QCOMPARE(r2.cancel(), &sem);
QCOMPARE(r2.semaphore(), nullptr);
}
QCOMPARE(sem.available(), 4);
// move-assignment:
{
const QSemaphoreReleaser r1(sem);
QSemaphoreReleaser r2(sem, 2);
QCOMPARE(sem.available(), 4);
r2 = QSemaphoreReleaser();
QCOMPARE(sem.available(), 6);
r2 = QSemaphoreReleaser(sem, 42);
QCOMPARE(sem.available(), 6);
}
QCOMPARE(sem.available(), 49);
}
QTEST_MAIN(tst_QSemaphore)
#include "tst_qsemaphore.moc"