1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
|
## Doing things in a Sequence
The time when an application starts, especially one that needs
to load quite some data, is usually one of contention. Translations
need to be loaded and resources like icons and images initialized. As
the application matures, more and more of such tasks are piled on to
it. It will have to check for updates from a server, and load a
greeting of the day to the user. Eventually, the application will
take ages to load, users will tweet about how they are making coffee
while it comes up, and the programmers will start to find a solution.
The application will come up a lot faster if it defers as many
tasks as possible while it creates and shows the user interface, and
also takes as many as possible of the startup tasks of an application
off the main thread. The main thread is the one that runs when
`main()` is entered, and in which the user interface lives. Everything
that slows down or intermittendly blocks the main thread may be
experienced by the user as the user interface being sluggish or
hung. This is a common use case where concurrent programming can
help.
But ... this is also one of the examples where standard thread pools
fail. The startup tasks commonly need to be done in a certain order
and are of different priority, and also should not be all tackled by
the application process at the same time.
For example, the applications icons and translations may be needed
first and urgently, where the information on available updates can
still be processed a couple of seconds later. There are ways around
this that are rather cumbersome, like using timers to queue up some
tasks later or using chains of functions that queue up new tasks when
one group is done. The following example will illustrate some aspects
of how ThreadWeaver comes with the necessary tools to specify the
order of tasks, on application startup and otherwise. The following
`main()`[^4] function allocates a main widget and an object of type
`ViewController` that takes care of the startup tasks.
@@snippet(HelloInternet/main.cpp,hellointernet-main,cpp)
The example application shows an image that it eventually loads from
the network, and a caption for it. In the constructor of
`ViewController`, the startup operations need to be kicked off. The
operations in this example are
* to load a placeholder image that is shown while the application
loads the image and caption from the network,
* to load the post that contains the caption, but only the URL of the
image to show,
* and then, once the image URL is known, to finally load the image
from the network and display it.
The application's user interface will be shown right away, even before
step 1 has been completed. Let's assume that the three steps need to
be done in order, not in parallel.
The important aspect is to do as little as possible in the
constructor, considering that it is called from the main
thread. Creating jobs and queueing them is not expensive however, so
the constructor focuses on that and then returns.
@@snippet(HelloInternet/ViewController.cpp,hellointernet-sequence,cpp)
Remember the assumption that the three startup steps have to be
performed in order. The new thing here is that instead of queueing
individual jobs, the constructor creates a `Sequence`, and then adds
jobs to that. A sequence has the jobs performed by the thread pool in
the order they have been added to it. The jobs each simply call a
member function of `ViewController` when being
executed. ThreadWeaver's execution logic guarantees that the next job
is only executed after the previous one has been finished. Because of
that, only one of the member functions will be called at a time, and
they will be called in the order the jobs have been added to the
sequence.
Since only one of the member functions will be called at a time, there
is no need for further synchronization of access to the member
variables of `ViewController`. This raises the question of how the
controller submits new captions, statuses and images to the main
widget. It would be a mistake to simply call member functions of the
main widget from the methods of `ViewController`, since these are
executed from a worker thread. The controller submits update by using
Qt signals that are connected to corresponding slots in the main
widget. The parameters of the signals are passed by value, not by
reference or pointers, making use of the implicit sharing built into
Qt to avoid copying. This approach relies on the fact that the
reference counting of Qt's implicit-sharing mechanism is thread safe.
@@snippet(HelloInternet/ViewController.cpp,hellointernet-loadresource,cpp)
The method `loadPlaceholderFromResource()` implements the first step,
to load an image from a resource that acts as a place holder until the
real images has been downloaded. It cheats to appear busy by first
sleeping for a short while. While it does so, the user interface will
already appear to the user, with a blank background. It then emits a
signal to make the main widget show a status message that indicates
the program is downloading the post.
The method is called from the worker thread that executes the job, not
the main thread. When the signal is emitted, Qt notices that sender and
receiver are not in the same thread at the time, and sends the signal
asynchronously. The receiver will not be called from the thread
executing `loadPlaceholderFromResource()`, instead it will be invoked
from the event loop of the main thread. That means there is no shared
data between the controller and the main widget for processing the
signal, and no further serialization of access to the QString variable
holding the status text is necessary.
Once the method returns and the job executing it completes, the next
job of the sequence will be unlocked. This causes the method
`loadPostFromTumblr()` to be executed by a worker thread. This method
illustrates the convenience built into Qt to process data present in
Open Standard formats (XML, in this case), even though this won't be
discussed here in detail.[^5] If processing the data turns out to be
expensive, the user interface will not be blocked by it, since it
is not performed by the main thread.
@@snippet(HelloInternet/ViewController.cpp,hellointernet-loadpost,cpp)
In case an error occurs, the method invokes another method called
`error()`. `error()` indicates the problem to the user by setting a
status messages in the main widget. But it also apparently aborts the
execution of the sequence, as the code assumes it does not continue
after calling it.
@@snippet(HelloInternet/ViewController.cpp,hellointernet-error,cpp)
`error()` shows a different placeholder image, and emits the status
message to the main widget. It then raises an exception of type
`ThreadWeaver::JobFailed`, which will be caught by the worker thread
executing the current job. The worker thread sets the status of
the job to a failed state. Specific to sequences (because only
sequences know the order of the execution of their elements), this
will cause the sequence to abort the execution of it's remaining
elements. Raising the exception will abort the processing of the job,
but not terminate the worker thread. Leaking any other type of
exception than `ThreadWeaver::Exception` from the `run()` method of a
job is considered a runtime error. The exception will not be caught by
the worker thread, and the application will terminate.
The example illustrates the steps necessary to perform concurrent
operations in a certain order. It also shows how a specialized
object (`ViewController`, in this case) can handle the data shared
between the sequential operations, how to submit data and status
information back to the user interface, and how to signal error
conditions from job execution.

[^4]: See `examples/HelloInternet` in the ThreadWeaver repository.
[^5]: The example uses the
[Tumblr API version 1](https://www.tumblr.com/docs/en/api/v1).
|