Map, filter, and reduce

QtConcurrent offers the classic map and filter algorithms for sequences which will however be processed in parallel using several threads. First, we have two basic operations: filter() and reduce() which will operate in place on the sequence. These operations have variants which do not modify the sequence, but instead return a new one as a result, namely filtered() and mapped(). These functions return a QFuture object but they also have their blocking variants, namely blockingMap(), blockingMapped(), blockingFilter(), and blockingFiltered(). Let's have a look at an example of their usage for the map family of functions:

void scaleImage(QImage &image)
{
image = image.scaled(100, 100);
}
QVector<QImage> imageList = ...;

// blocking in-place
QtConcurrent::blockingMap(imageList, scaleImage);
// async in-place
QFuture<void> future = QtConcurrent::map(imageList, scaleImage);

// blocking with copy
QVector<QImage> scaledImages = QtConcurrent::blockingMapped(imageList.constBegin(), imageList.constEnd(), scaleImage);
// async with copy
auto future = QtConcurrent::mapped(imageList.constBegin(), imageList.constEnd(), scaleImage);

These functions will apply an operation or condition to every element of a sequence. Internally, they will use a thread pool and futures to distribute the work over the threads available in the pool. The work will be automatically partitioned in chunks whose sizes will be rebalanced during execution, depending on the processing time for the single elements.

Many parallel computing frameworks, such as the OpenMP framework on the Intel TBB library, implement a so-called fork-join model of parallelism in as much as that execution branches off in parallel at some points of the program (fork) and then the branches meet again (join), combine their results, and resume sequential execution.

QtConcurrent provides some support for that model, in that it implements a fork-join version of the previous algorithms, namely filteredReduced() and mapedReduced(), as well as blockingFilteredReduced() and blockingMapedReduced(). Look at the following example:

QStringList strings = ...;
int countWords(const QString& s) { ... }
int countWords(int& total, const int& count ) { total += count; }

int wordCount = QtConcurrent::blockingFilteredReduced(strings, coundWords, addCounts);

We can see that there is a progression of abstraction levels to be observed here. QThreadPool abstracts away thread creation and execution of tasks in thread context, QFuture and QtConcurrent::run encapsulate asynchronous result delivery, while QtConcurrent::map and QtConcurrent::filter functions relieve us of the task of partitioning the tasks to available threads and waiting for the result of the entire computation.

You might be tempted to say that the C++17 Standard Template Library (STL) algorithms using the std::execution::par execution policy are doing exactly the same, but they aren't. Consider the following example:

std::vector<int> vec;
std::for_each(std::execution::par, seq.begin(), seq.end(), [&](int i) {
vec.push_back(i*2+1); // error: data race!
});

The standard states explicitly:

When using parallel execution policy, it is the programmer's responsibility to avoid deadlocks!

Summing up, we can say that Qt provides some basic support for higher level concurrency constructs, but it is not very advanced. As the QFutures cannot be chained, nor can the QtConcurrent algorithms be combined. But they are at least a starting point.