Working with QFuture

QFuture is the standard Qt approach to creating and managing asynchronous background tasks. A "future" represents the eventual outcome of an asynchronous task and is backed by a promise to produce a result when the task finishes. With a QFuture, you can: continue processing when the task finishes; be notified of errors; and cancel the task. In the ArcGIS Maps SDK for Qt, you receive a QFuture as the return value for any of the SDK’s asynchronous tasks.

You may be familiar with "TaskWatcher", which is our older solution for handling asynchronous tasks. Use only one or the other - do not mix QFuture calls with TaskWatcher calls or their corresponding "completed()" signals.

Return Value

QFuture is a template class. The template parameter tells you what type will be returned when the task finishes. For example, a QFuture<bool> will produce a boolean value, and a QFuture<RouteTask> will produce a RouteTask object. QFuture<void> covers the case where there is no result, other than the fact that the task has finished.

Continuing when the Task Finishes

To continue processing when a task finishes, pass a "continuation block" to the QFuture via the then() method. When the task finishes, your continuation block will run. A continuation block is a lambda, and it will be passed a parameter that matches the template parameter of the QFuture. This will be the result of the asynchronous task.

RouteTask::createRouteResultAsync(portalItem).then(this, [] (const RouteResult& routeResult)
{
  // continue processing "routeResult" here
});

Handling Errors

The Qt Maps SDK defines an ErrorException class to represent errors encountered during the processing of an asynchronous task. Use QFuture’s standard onFailed() handler to capture such an error, if one occurs. The error will be passed as a parameter to the lambda you provide. The onFailed() handler can be chained together with a then() continuation block.

Note that QFuture allows you to provide multiple onFailed() handlers, to distinguish different types of errors. This works similarly to a "try/catch" block. The Qt Maps SDK will always provide an error of type ErrorException, but you can also catch a general QException or specify no type to catch all errors.

RouteTask::createRouteResultAsync(portalItem).then(this, [] (const RouteResult& routeResult)
{
  // continue processing "routeResult" here
}).onFailed(this, [] (const ErrorException& e)
{
  // handle error
});

Canceling

You can programmatically cancel an asynchronous task by calling cancel() on the QFuture object. Store the QFuture in some member variable, and perhaps call cancel() in response to a button click or other user interaction.

QFuture<RouteResult> future = RouteTask::createRouteResultAsync(portalItem);
future.cancel();

Responding to Cancellation

Like the onFailed() handler, you may supply an onCanceled() handler to be informed if the QFuture is canceled. It can be chained along with your then() and onFailed() handlers. Use this if you need to perform some special cleanup if the task is canceled.

RouteTask::createRouteResultAsync(portalItem).then(this, [] (const RouteResult& routeResult)
{
  // continue processing "routeResult" here
}).onFailed(this, [] (const ErrorException& e)
{
  // handle error
}).onCanceled(this, [] ()
{
  // respond to cancellation
});

Threading

QFuture allows you to control which thread your handlers run on. The then(), onFailed(), and onCanceled() methods each have overloads that take a context parameter as their first argument. When using these overloads, the handler will run on the thread associated with the context parameter. The examples above pass in this as the context parameter. Doing so ensures that the handler will run on the thread of the this object, which is usually the right choice.

Any code that affects the User Interface must run on the UI thread. Changes to a QObject’s parent value must run on the same thread as the parent object. Using the context parameter ensures that your handlers will be run in a thread-safe manner. Your this object will usually be on the appropriate thread, unless you’ve taken special steps to work on a background thread. If you have, make sure you understand what thread your handlers will run on.

Do not block the UI thread. This will cause your interface to freeze. If you have heavy processing to do, choose an approach that puts the heavy processing on a background thread. The Qt Maps SDK is already doing this for you with its asynchronous tasks, but you can also use QFuture for your own post-processing. To do so, use the overload of then() that takes a QtFuture::Launch parameter as its first argument. Pass QtFuture::Launch::Async to run your handler on a background thread, but take care to send the results back to the main thread if necessary (i.e., use another then() block with a context parameter).

QFuture provides result() and waitForFinished() methods that block the current thread and wait for the asynchronous task to finish. Though it might be tempting, don’t use these methods on the UI thread. Use a proper then() continuation block. Using result() or waitForFinished() might be appropriate in advanced scenarios where you are processing on a background thread.

routeTask->createDefaultParametersAsync().then(QtFuture::Launch::Async, [routeTask] (const RouteParameters& routeParameters)
{
  // Launch::Async puts us on a background thread
  // Heavy processing here will not impact the UI thread
  Stop s1(Point(-13047255.711, 4036263.325, SpatialReference::webMercator()));
  Stop s2(Point(-13047611.732, 4036263.325, SpatialReference::webMercator()));
  routeParameters.setStops({ s1, s2 });
  routeParameters.setReturnDirections(true);
  routeParameters.setReturnStops(true);
  auto solveRouteFuture = routeTask->solveRouteAsync(routeParameters).then([] (const RouteResult& routeResult)
  {
    // continue processing "routeResult" here
  });

  // safe to call waitForFinished() on a background thread
  solveRouteFuture.waitForFinished();
}).onFailed(this, [] (const ErrorException& e)
{
  // handle error
});

Memory Management

Asynchronous tasks from the Qt Maps SDK sometimes allocate new objects. The methods that do so are the ones that return a QFuture with a pointer type. For example, QFuture<FeatureQueryResult*> is returned from the method queryFeaturesAsync(). By default, the newly allocated objects will be parented to the object that initiated the task. For example, the FeatureTable on which you called queryFeaturesAsync() will be the parent of the FeatureQueryResult.

You can control this behavior by passing in an optional parent QObject as the last parameter. When specified, any allocated objects will be parented to the provided parent object. Standard Qt parent/child memory management applies: when the parent is deleted, it first recursively deletes its child objects. You may also call deleteLater() on the result when you are finished with it. Make sure you understand the lifetime of these result objects to avoid unbounded memory growth.

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.