The Sandbox threading API is part of EditorCommon.
Responsiveness of the user-interface is a critical aspect of the user-experience. The user-interface runs on only a single thread called the main thread. It is imperative that the main thread is never blocked by some long-running operation. Any operation that takes more than a few milliseconds to complete should run asynchronously (i.e., on another thread).
Multithreaded programming is inherently difficult and error prone, so we need to provide some common concepts that are used throughout the Sandbox. Also, threads should be managed by a single system so that we can utilize resources efficiently. In addition, the API must be simple enough to encourage concurrent programming.
Multi-threading in Qt:
The Sandbox user-interface is built on top of the Qt library, so you must conform to Qt's assumptions with respect to multi-threading. For a comprehensive description of Qt's multithreading model read Thread Basics.
Among the central features of Qt are QObject, QWidget, and the signal&slot mechanism. QObject is the base class of virtually every other class in Qt. QWidget (which derives from QObject) is the base class of every user-interface class. The signal&slot mechanism is used to connect events (signals) of one QObject to callbacks (slots) of one or more other QObjects. Signal&slot implements loose coupling, in the sense that the signaling object is not aware of its listeners.
Nothing is thread-safe unless stated otherwise.
Qt's documentation explicitly states what functions are thread-safe. For example, see the note in the text of postEvent().
Qt is inherently single-threaded, as it is built around a single main event loop. As a corollary, all user-interface elements (objects deriving from QWidget) must be created on the main thread. This is also true for (modal) dialogs. In particular, do not call
CQuestionDialog::SQuestionDialog (our version of
QObjects have the notion of thread affinity. An object can be explicitly moved to another thread. Each thread has its own event loop which processes the slots of objects it owns.
Signals&slots are thread-safe and non-blocking by default. The connection mode can be changed, however, rendering a connection potentially unsafe.
Therefore, when reasoning about the thread-safety of a particular signal&slot connection, consider the connection mode and the thread affinity of the target object.
Qt does a good job giving you debugging information. In the Visual Studio output window, look for messages similar to these:
Qt debugging messages
Two function templates are provided for task-based concurrency:
Async runs a function on a thread different from the main thread, and
PostOnMainThread runs a function on the main thread (i.e., user-interface thread). Both functions return a std::future that can be used to retrieve the result. If
PostOnMainThread is called on the main thread, the function is executed immediately. Otherwise, the function is pushed on a FIFO-queue. A function passed to
Async can be executed by any worker thread in any order.
Both functions are recursive, which means that a function executed by either
PostOnMainThread is also free to call
PostOnMainThread again. However, an asynchronous task should never block on another asynchronous task. Since there is usually just a fixed number of worker threads, this could lead to a deadlock.
Arguments are copied, and the same rules apply as for passing arguments to std::async or std::thread. Use std::ref for pass-by-reference; or capture-by-reference, if the function is a lambda.
The functions are modeled after std::async.
Async should be preferred over
std::async, however, as it uses Sandbox' thread pools. Note that, in contrast, to
std::async, Async does not block when the return value is ignored:
LaunchAsync vs std::async
A typical scenario is that a costly operation should produce some value asynchronously, which is then consumed by the main thread. With a task-based approach, this can be written as follows.
Example 1: Wait for the main thread.
Sometimes, asynchronous operations need to call back to the main thread. For example, they might need to show a confirmation dialog. Recall that all widgets, including dialogs, must be created on the main thread.
Another example is that two functions can be run in parallel before their results are used:
In some other task-based APIs, there is the concept of a continuation (see NET framework, PPL's then or boost's then). Continuations are a powerful concept in which tasks can be chained together arbitrarily. Most importantly, a task need not be aware of what other tasks follow. Typically, there are also functions to synchronize with a group of tasks, and you can wait for the completion of any tasks in a group, or all of them. In conjunction with continuations, task-based programming is a very powerful paradigm.
In example 0, we showed how to launch one task on the main thread, once an asynchronous task has finished. This is a special case of continuation, where the task executed on the main thread is a continuation of the asynchronous task. Since this is a very common use-case, two convenience function templates are provided —
AsyncNotify ---- that build on top of
AsyncFinalize and AsyncNotify
Both function templates take to functions as an argument: a work function run asynchronously, and a finalize function to run on the main thread. The difference between
AsyncNotify is that the former passes the result of the work function to the finalize function, whereas the latter calls the finalize function without any arguments. Both functions return a future of the work function.
Note that no additional arguments can be passed to
finalizeFn. Use lambdas with capture instead.