Add a safer way to use QThreadPool::reserveThread

Add startOnReservedThread that specifically releases a reserved thread
and uses it atomically for a given task. This can make a positive
number of reserved threads work.

Change-Id: I4bd1dced24bb46fcb365f12cbc9c7905dc66cdf1
Reviewed-by: David Faure <david.faure@kdab.com>
This commit is contained in:
Allan Sandfeld Jensen 2021-05-10 17:34:20 +02:00
parent 2c3617dfcb
commit 733923c81c
3 changed files with 106 additions and 2 deletions

View File

@ -770,6 +770,57 @@ void QThreadPool::releaseThread()
d->tryToStartMoreThreads(); d->tryToStartMoreThreads();
} }
/*!
Releases a thread previously reserved with reserveThread() and uses it
to run \a runnable.
Note that the thread pool takes ownership of the \a runnable if
\l{QRunnable::autoDelete()}{runnable->autoDelete()} returns \c true,
and the \a runnable will be deleted automatically by the thread
pool after the \l{QRunnable::run()}{runnable->run()} returns. If
\l{QRunnable::autoDelete()}{runnable->autoDelete()} returns \c false,
ownership of \a runnable remains with the caller. Note that
changing the auto-deletion on \a runnable after calling this
functions results in undefined behavior.
\note Calling this when no threads are reserved results in
undefined behavior.
\since 6.3
\sa reserveThread(), start()
*/
void QThreadPool::startOnReservedThread(QRunnable *runnable)
{
if (!runnable)
return releaseThread();
Q_D(QThreadPool);
QMutexLocker locker(&d->mutex);
Q_ASSERT(d->reservedThreads > 0);
--d->reservedThreads;
if (!d->tryStart(runnable)) {
// This can only happen if we reserved max threads,
// and something took the one minimum thread.
d->enqueueTask(runnable, INT_MAX);
}
}
/*!
\overload
\since 6.3
Releases a thread previously reserved with reserveThread() and uses it
to run \a functionToRun.
*/
void QThreadPool::startOnReservedThread(std::function<void()> functionToRun)
{
if (!functionToRun)
return releaseThread();
startOnReservedThread(QRunnable::create(std::move(functionToRun)));
}
/*! /*!
Waits up to \a msecs milliseconds for all threads to exit and removes all Waits up to \a msecs milliseconds for all threads to exit and removes all
threads from the thread pool. Returns \c true if all threads were removed; threads from the thread pool. Returns \c true if all threads were removed;

View File

@ -75,6 +75,9 @@ public:
void start(std::function<void()> functionToRun, int priority = 0); void start(std::function<void()> functionToRun, int priority = 0);
bool tryStart(std::function<void()> functionToRun); bool tryStart(std::function<void()> functionToRun);
void startOnReservedThread(QRunnable *runnable);
void startOnReservedThread(std::function<void()> functionToRun);
int expiryTimeout() const; int expiryTimeout() const;
void setExpiryTimeout(int expiryTimeout); void setExpiryTimeout(int expiryTimeout);

View File

@ -89,6 +89,7 @@ private slots:
void releaseThread_data(); void releaseThread_data();
void releaseThread(); void releaseThread();
void reserveAndStart(); void reserveAndStart();
void reserveAndStart2();
void releaseAndBlock(); void releaseAndBlock();
void start(); void start();
void tryStart(); void tryStart();
@ -729,17 +730,66 @@ void tst_QThreadPool::reserveAndStart() // QTBUG-21051
// start() will wake up the waiting thread. // start() will wake up the waiting thread.
threadpool->start(&task); threadpool->start(&task);
QTRY_COMPARE(threadpool->activeThreadCount(), 2); QTRY_COMPARE(threadpool->activeThreadCount(), 2);
QTRY_COMPARE(task.count.loadRelaxed(), 2);
WaitingTask task2;
// startOnReservedThread() will try to take the reserved task, but end up waiting instead
threadpool->startOnReservedThread(&task2);
QTRY_COMPARE(threadpool->activeThreadCount(), 1);
task.waitForStarted.acquire(); task.waitForStarted.acquire();
task.waitBeforeDone.release(); task.waitBeforeDone.release();
QTRY_COMPARE(task.count.loadRelaxed(), 2);
QTRY_COMPARE(threadpool->activeThreadCount(), 1); QTRY_COMPARE(threadpool->activeThreadCount(), 1);
task2.waitForStarted.acquire();
task2.waitBeforeDone.release();
threadpool->releaseThread();
QTRY_COMPARE(threadpool->activeThreadCount(), 0); QTRY_COMPARE(threadpool->activeThreadCount(), 0);
threadpool->setMaxThreadCount(savedLimit); threadpool->setMaxThreadCount(savedLimit);
} }
void tst_QThreadPool::reserveAndStart2()
{
class WaitingTask : public QRunnable
{
public:
QSemaphore waitBeforeDone;
WaitingTask() { setAutoDelete(false); }
void run() override
{
waitBeforeDone.acquire();
}
};
// Set up
QThreadPool *threadpool = QThreadPool::globalInstance();
int savedLimit = threadpool->maxThreadCount();
threadpool->setMaxThreadCount(2);
// reserve
threadpool->reserveThread();
// start two task, to get a running thread and one queued
WaitingTask task1, task2, task3;
threadpool->start(&task1);
// one running thread, one reserved:
QCOMPARE(threadpool->activeThreadCount(), 2);
// task2 starts queued
threadpool->start(&task2);
QCOMPARE(threadpool->activeThreadCount(), 2);
// startOnReservedThread() will take the reserved thread however, bypassing the queue
threadpool->startOnReservedThread(&task3);
// two running threads, none reserved:
QCOMPARE(threadpool->activeThreadCount(), 2);
task3.waitBeforeDone.release();
// task3 can finish even if all other tasks are blocking
// then task2 will use the previously reserved thread
task2.waitBeforeDone.release();
QTRY_COMPARE(threadpool->activeThreadCount(), 1);
task1.waitBeforeDone.release();
QTRY_COMPARE(threadpool->activeThreadCount(), 0);
}
void tst_QThreadPool::releaseAndBlock() void tst_QThreadPool::releaseAndBlock()
{ {
class WaitingTask : public QRunnable class WaitingTask : public QRunnable