+ ** Modified 2010 by HostileFork to use http://hostilefork.com/thinker-qt/
+ ** Modified 2010 by HostileFork to use http://hostilefork.com/thinker-qt/
+ ** Modified 2010 by HostileFork to use http://hostilefork.com/thinker-qt/
+ ** Modified 2010 by HostileFork to use http://hostilefork.com/thinker-qt/
+ ** Modified 2010 by HostileFork to use http://hostilefork.com/thinker-qt/
Please feel free to comment inline on the source! To do so, click in the line-number area to the left on a line which is indicated as having changed. (red or green)
Thinker-Qt doesn’t use signal and slot parameters to exchange data between threads. Instead, it leverages the ability to make copy-on-write “snapshots” of the calculation state.
+ colormap[i] = rgbFromWaveLength(380.0 + (i * 400.0 / ColormapSize));
To make sure we build this colormap only once, we now create it in the QWidget and pass it as a parameter. That’s because unlike the long-running QThread in the original Mandelbrot example, we generate a new Thinker object for each new Mandelbrot calculation (which is queued to a thread pool).
The Thinker::Watcher parallels QFutureWatcher by letting you know when data updates are available (although you can take snapshots at any time you like).
The written() signal is automatically generated when a background calculation writes to its state. So if that calculation is in a tight loop you could get flooded with signals! Here we ask that after we've received a written() signal we don’t want to get another before 400ms has passed. If updates happen in that 400ms, they will be grouped together as a single written() once the time window is cleared.
I'm currently following the convention of QFuture, where there is no implicit cancellation when objects holding references to Thinkers are destroyed. You must either waitForFinished() or cancel() manually.
Riffing off of QtConcurrent’s idea of using the word “Future”, I use the word “Present” to describe the object handed back from run(). If a moment passes and you don’t make a snapshot, you've missed that intermediate state and must use whatever the more “present” one is. (Thinker-Qt was designed for cases like Mandelbrot where those semantics are what you desire.)
The sooner you can clear() a snapshot pointer, the better. It reduces the risk of a write happening in the background calculation while you’re holding onto it. (Should that happen, then the background thread will get held up a bit as it makes a copy-on-write.)
wasPauseRequested() covers both the cases of pause and cancellation requests. (Cancellation will work with any Thinker but resuming after a pause involves coroutine-style programming and is beyond the scope of this sample.)
If you are nested in a function and have exception handling enabled, the routine pollForStopException() is a shortcut that throws an exception which is caught by the internal Thinker loop.
Although the internals of ThinkerQt use QSharedDataPointer and are thread safe with respect to writes, it may be the case that your program’s semantics are such that it’s not appropriate to allow snapshots of certain intermediate steps in a write. This locking protocol is for snapshot atomicity.
In the original Mandelbrot sample, if the GUI thread was unable to process images faster than the RenderThread could produce them, you'd use memory inefficiently. There'd be a backlog of intermediate QImages held alive by their reference in the signal queue, when the most recent image is “the best” and hence the only one that is truly needed. By having only the latest image referenced by the calculation state, Thinker-Qt finesses this.
(Although git put this line in a weird place, it’s the end of the mandelbrot calculation loop.) You might wonder why done() is a signal and not a return value from start(). The reason is that Thinkers are QObjects, and via moveToThread they have been put on the thread given to them by the thread pool. Returning here without signaling done() indicates that you'd like to sit in an event loop and have some of your slots do the rest of the processing… those slots may be the ones to raise the done() signal.
+ class RenderThinkerData : public SnapshottableData
The calculation state is really just QSharedData, but I use “SnapshottableData” because I wanted a polymorphic base class. It’s QSharedData with a virtual destructor—that’s it.
Taking snapshots is central to Thinker-Qt, and if no write to the calculation state happens while you’re holding onto it then it’s essentially free (due to the copy-on-write semantics). SnapshotPointer is a shared pointer class, and you will release the snapshot when you release all copies of the pointer returned from createSnapshot().
Because a Thinker periodically polls for cancels/pauses in a cooperative-multitasking way, it won’t necessarily stop on a dime. So an interesting aspect of using Thinker-Qt is that if you have multiple threads in the thread pool, your new Thinker can be handed off to another thread even before the previous one has finished canceling! (e.g. There’s no need to synchronously waitForFinished here… if a thread is available the new thinker will start running immediately and if none are then it will be put in a queue.)
This concept of “restart” doesn’t exist for a Thinker. If you want to do a new computation, you cancel the old one and start a new one. (Because Thinker-Qt uses a thread pool, this doesn’t involve destroying and recreating threads.)
Data passed into a Thinker’s constructor and then saved as internal state doesn’t need to be protected with a mutex in Thinker-Qt, so this is unnecessary in the modified example.
Indeed, it is generally nice to make the Thinker object a friend of the state object! You can imagine that these are member variables of the Thinker in spirit. Non-friends should be using const member functions to query the object (the only way they can get at these state objects is through read-only snapshots).
Ok technically, since update()s are queued…it’s probably not that significant to be clearing it at this moment. Would've been better to grab the QImage out by value and clear before calling QPixmap::fromImage(), since that’s probably a more expensive operation. But you get the idea: the less time you keep a SnapshotPointer alive, the better! :)
Please feel free to comment inline on the source! To do so, click in the line-number area to the left on a line which is indicated as having changed. (red or green)
Thinker-Qt doesn’t use signal and slot parameters to exchange data between threads. Instead, it leverages the ability to make copy-on-write “snapshots” of the calculation state.
To make sure we build this colormap only once, we now create it in the QWidget and pass it as a parameter. That’s because unlike the long-running QThread in the original Mandelbrot example, we generate a new Thinker object for each new Mandelbrot calculation (which is queued to a thread pool).
The Thinker::Watcher parallels QFutureWatcher by letting you know when data updates are available (although you can take snapshots at any time you like).
The written() signal is automatically generated when a background calculation writes to its state. So if that calculation is in a tight loop you could get flooded with signals! Here we ask that after we've received a written() signal we don’t want to get another before 400ms has passed. If updates happen in that 400ms, they will be grouped together as a single written() once the time window is cleared.
I'm currently following the convention of QFuture, where there is no implicit cancellation when objects holding references to Thinkers are destroyed. You must either waitForFinished() or cancel() manually.
Riffing off of QtConcurrent’s idea of using the word “Future”, I use the word “Present” to describe the object handed back from run(). If a moment passes and you don’t make a snapshot, you've missed that intermediate state and must use whatever the more “present” one is. (Thinker-Qt was designed for cases like Mandelbrot where those semantics are what you desire.)
The sooner you can clear() a snapshot pointer, the better. It reduces the risk of a write happening in the background calculation while you’re holding onto it. (Should that happen, then the background thread will get held up a bit as it makes a copy-on-write.)
wasPauseRequested() covers both the cases of pause and cancellation requests. (Cancellation will work with any Thinker but resuming after a pause involves coroutine-style programming and is beyond the scope of this sample.)
If you are nested in a function and have exception handling enabled, the routine pollForStopException() is a shortcut that throws an exception which is caught by the internal Thinker loop.
Although the internals of ThinkerQt use QSharedDataPointer and are thread safe with respect to writes, it may be the case that your program’s semantics are such that it’s not appropriate to allow snapshots of certain intermediate steps in a write. This locking protocol is for snapshot atomicity.
No emit is required here. The unlock will queue a written() signal to those who are listening, and it will be throttled appropriately.
In the original Mandelbrot sample, if the GUI thread was unable to process images faster than the RenderThread could produce them, you'd use memory inefficiently. There'd be a backlog of intermediate QImages held alive by their reference in the signal queue, when the most recent image is “the best” and hence the only one that is truly needed. By having only the latest image referenced by the calculation state, Thinker-Qt finesses this.
(Although git put this line in a weird place, it’s the end of the mandelbrot calculation loop.) You might wonder why done() is a signal and not a return value from start(). The reason is that Thinkers are QObjects, and via moveToThread they have been put on the thread given to them by the thread pool. Returning here without signaling done() indicates that you'd like to sit in an event loop and have some of your slots do the rest of the processing… those slots may be the ones to raise the done() signal.
The calculation state is really just QSharedData, but I use “SnapshottableData” because I wanted a polymorphic base class. It’s QSharedData with a virtual destructor—that’s it.
Deriving from a Thinker parameterized with the calculation state is what you do. Behind the scenes you get the snapshotting and automatic signaling.
Taking snapshots is central to Thinker-Qt, and if no write to the calculation state happens while you’re holding onto it then it’s essentially free (due to the copy-on-write semantics). SnapshotPointer is a shared pointer class, and you will release the snapshot when you release all copies of the pointer returned from createSnapshot().
Because a Thinker periodically polls for cancels/pauses in a cooperative-multitasking way, it won’t necessarily stop on a dime. So an interesting aspect of using Thinker-Qt is that if you have multiple threads in the thread pool, your new Thinker can be handed off to another thread even before the previous one has finished canceling! (e.g. There’s no need to synchronously waitForFinished here… if a thread is available the new thinker will start running immediately and if none are then it will be put in a queue.)
This routine is just being moved from the RenderThread file. There were no changes.
This routine is just being moved to the MandelbrotWidget file. There were no changes.
This concept of “restart” doesn’t exist for a Thinker. If you want to do a new computation, you cancel the old one and start a new one. (Because Thinker-Qt uses a thread pool, this doesn’t involve destroying and recreating threads.)
Data passed into a Thinker’s constructor and then saved as internal state doesn’t need to be protected with a mutex in Thinker-Qt, so this is unnecessary in the modified example.
It’s nice to have friends!
Indeed, it is generally nice to make the Thinker object a friend of the state object! You can imagine that these are member variables of the Thinker in spirit. Non-friends should be using const member functions to query the object (the only way they can get at these state objects is through read-only snapshots).
Ok technically, since update()s are queued…it’s probably not that significant to be clearing it at this moment. Would've been better to grab the QImage out by value and clear before calling QPixmap::fromImage(), since that’s probably a more expensive operation. But you get the idea: the less time you keep a SnapshotPointer alive, the better! :)