[Interest] Issue with QFutureWatcher cancel() delay

Sean Murphy Sean.M.Murphy at us.kbr.com
Fri Sep 23 20:01:38 CEST 2022

I'm having an issue using the QtConcurrent module for processing large
images, specifically allowing the user to cancel processing an image
once it has begun.

I've got two main classes involved in this process:
  - ProjectModel - a model class running in the UI thread that manages
                   what images are part of the current project
  - ImageTileManager - a class that runs in a separate thread that
                       handles requests from the project model to do
                       the processing on the requested file, renders
                       the result to a QImage, and then send the image
                       back to the ProjectModel to display to the user.

Everything works great when the user requests processing file(s) to
image(s) and lets it run to completion. Where I'm having some trouble is
when the user requests processing, but then changes their minds and
cancels the job in progress. I'm hitting a lengthy delay when that
occurs. Below is a sample run where I've enabled debug messages to
track what's happening:

 1. ProjectModel::slotImageRequested QTime("13:05:02.657") requesting        "{82216618-dde4-41b1-bd6d-d991d9c6f4ca}"
 2. ImageTileManager::queueJob       QTime("13:05:02.658") queuing job       "{82216618-dde4-41b1-bd6d-d991d9c6f4ca}"
 3. ImageTileManager::processJob     QTime("13:05:02.659") processing        "{82216618-dde4-41b1-bd6d-d991d9c6f4ca}"
 4. ProjectModel::slotCancelLoading  QTime("13:05:04.792") canceling         "{82216618-dde4-41b1-bd6d-d991d9c6f4ca}"
 5. ImageTileManager::cancelJob      QTime("13:05:11.534") canceling load on "{82216618-dde4-41b1-bd6d-d991d9c6f4ca}"
 6. ProjectModel::slotImageRequested QTime("13:05:18.965") requesting        "{449d7140-5bb2-4a6f-a3cf-98ba2529f237}"
 7. ImageTileManager::cancelJob      QTime("13:06:47.989") load canceled     "{82216618-dde4-41b1-bd6d-d991d9c6f4ca}"
 8. ImageTileManager::queueJob       QTime("13:06:48.968") queuing job       "{449d7140-5bb2-4a6f-a3cf-98ba2529f237}"
 9. ImageTileManager::loadFinished   QTime("13:06:48.969") load finished     "LoadFutureWatcher {82216618-dde4-41b1-bd6d-d991d9c6f4ca}"
10. ImageTileManager::processJob     QTime("13:06:48.969") processing        "{449d7140-5bb2-4a6f-a3cf-98ba2529f237}"
11. ImageTileManager::loadFinished   QTime("13:06:54.140") load finished     "LoadFutureWatcher {449d7140-5bb2-4a6f-a3cf-98ba2529f237}"

Debug lines explained:
  Lines 1 & 2: The user requests processing of a file in the UI (1),
     which is acknowledged by the processing class (2)
  Line 3: the processing class begins processing
  Lines 4 & 5: The user requests cancelation of the job (4), the
     processing class acknowledges and starts the cancellation process (5)
  Line 6: The user requests processing of a new job
  Line 7: the processing class has finally canceled the original job,
     roughly 1 minute, 36 seconds after the request to cancel
  Line 8: The processing class finally queues the request sent in (6)
  Line 9: The future watcher from the first job emits its finished() signal
  Lines 10 & 11: The processing class begins working on the second
     job (10) and is allowed to finish (11)

The cancel() routine in the processing class looks like:
  void ImageTileManager::cancelJob(const QUuid &uuid)
    QMutexLocker locker(&mJobQueueMutex);

    // check to see if it's our current job
    if(mCurrentJob.first == uuid)
        mLoadWatcher->disconnect(); // disconnect signals from the QFutureWatcher
        qDebug() << __FUNCTION__ << QTime::currentTime() << "canceling load on" << uuid.toString();  // <- this produces message (5) above
        mLoadWatcher->cancel();  // tell QFutureWatcher to cancel the current run
        mLoadWatcher->waitForFinished(); // block until finished
        qDebug() << __FUNCTION__ << QTime::currentTime() << "load canceled" << uuid.toString(); // <- this produces message (6) above
    } else {
      // delete queued job from the queue

I've got two main questions:
  1. What can I do to speed up the process of starting on the second job
     once the user requests canceling the first? I realize I'm using the
     blocking "waitForFinished()" call, but previous to this example, I
     wasn't blocking and instead reacting the the QFutureWatcher's
     finished() signal to indicate when the first job was finally canceled
     before starting the next job in the queue. Under that setup, it still
     takes roughly the same delay. The way I currently have my ImageTileManager
     designed, I only want it working on processing one image at a time,
     using QtConcurrent to throw all of the machine's cores at processing a
     single image as fast as possible, instead of trying to process multiple
     images simulataneously. I'm not sure if there's some sort of design
     change I can make where once a user elects to cancel a job, that job
     gets moved off into yet another thread (this time just for cleanup purposes)
     and the the ImageTileManager can launch the next QtConcurrent run on the
     next job in the queue, and then I just let the operating system handle the
     fight between the cleanup threads and the next's jobs processing threads?
  2. Once I switched to using the blocking call, I added the
     "mLoadWatcher->disconnect()" call before I call cancel() on the QFutureWatcher,
     since I plan to block anyways and I no longer need to care about the finished()
     signal. However, if you note line 9 in the debug log, somehow there's still a
     signal/slot connection between the QFutureWatcher's finished() signal and
     my ImageTileManager::loadFinished() slot. This was unexpected, and I'm not
     sure how to prevent it?
   a. For what it's worth, I make the original connection like so:
    connect(mLoadWatcher, &QFutureWatcher<void>::finished, this,
            static_cast<Qt::ConnectionType>(Qt::AutoConnection | Qt::UniqueConnection));

Thanks in advance,

This e-mail, including any attached files, may contain confidential information, privileged information and/or trade secrets for the sole use of the intended recipient. Any review, use, distribution, or disclosure by others is strictly prohibited. If you are not the intended recipient (or authorized to receive information for the intended recipient), please contact the sender by reply e-mail and delete all copies of this message.

More information about the Interest mailing list