17.2.3.  Concurrent Map/Reduce Example

[ fromfile: threads.xml id: parallellife ]

Revisiting Conway's Game of Life from Section 9.10, we will parallelize the computation using a QtConcurrent mapReduce() algorithm. For this to work, we must first break up the puzzle into smaller pieces. We define each piece as a LifeSlice, shown in Example 17.24.

Example 17.24. src/threads/life/lifeslice.h

[ . . . . ]
struct LifeSlice {
    LifeSlice() {};
    LifeSlice(QRect r, QImage i) : rect(r), image(i) {}
    QRect rect;
    QImage image;
};
[ . . . . ]

<include src="src/threads/life/lifeslice.h" href="src/threads/life/lifeslice.h" id="lifeslice-h" mode="cpp"/>


A LifeSlice consists of a QImage with a QRect indicating which piece it came from. This is the argument and return type of the mapping function (Example 17.25). Do not use QPixmap in LifeSlice.[105]

Example 17.25. src/threads/life/lifemainwindow.cpp

[ . . . . ]
struct LifeFunctor : public std::unary_function<LifeSlice, LifeSlice> {
    LifeSlice operator() (LifeSlice slice);
};

LifeSlice LifeFunctor::operator()(LifeSlice slice) {    1
    QRect rect = slice.rect;
    QImage image = slice.image;
    QImage next = QImage(rect.size(), QImage::Format_Mono);
    next.fill(DEAD);
    int h = rect.height();  int w = rect.width();

    for (int c=0; c<w; ++c) {
        for (int r=0; r<h; ++r) {
            int x = c+rect.x();
            int y = r+rect.y();
            bool isAlive = (image.pixelIndex(x, y) == ALIVE);
            int nc = neighborCount(image, x, y);
            if (!isAlive && nc == 3)
                 next.setPixel(c, r, ALIVE);
            if (!isAlive) continue;
            if (nc == 2 || nc == 3)
                next.setPixel(c,r, ALIVE);
        }
    }
    slice.image = next;
    return slice;
}

1

Mapping function

<include segid="mapfunctor" mode="cpp" href="src/threads/life/lifemainwindow.cpp" id="life-mapfunctor" src="src/threads/life/lifemainwindow.cpp"/>


Deriving a unary function object, LifeFunctor from the Standard Library template unary_function gives our functor additional type "traits", used by generic algorithms to get the argument and return type of the functor. This functor defines an operator() that takes a LifeSlice argument and returns a LifeSlice value.

The return type of the mapping function must be the (second) argument type of the reduce function Example 17.26.

Example 17.26. src/threads/life/lifemainwindow.cpp

[ . . . . ]
void stitchReduce(QImage& next, const LifeSlice &slice) {
    if (next.isNull()) 
        next = QImage(boardSize, QImage::Format_Mono);
    QPainter painter(&next);
    painter.drawImage(slice.rect.topLeft(), slice.image);   1
}

1

Draw one piece of an image onto another.

<include src="src/threads/life/lifemainwindow.cpp" allfiles="1" segid="reduce" href="src/threads/life/lifemainwindow.cpp" mode="cpp" id="life-reduce"/>


The reduce function must somehow take together the partial results produced by each of the worker threads and reassemble them back into a coherent QImage. As in the collage example, the maping function uses the high-level QPainter API to draw one image onto another, avoiding the need for nested loops of individual pixel assignments.

The main loop Example 17.27 must break up the problem into smaller pieces, send them to a QtConcurrent blockingMappedReduced(), and send the results to the LifeWidget.

Example 17.27. src/threads/life/lifemainwindow.cpp

[ . . . . ]

void LifeMainWindow::calculate() {
    int w = boardSize.width();
    // This might not be optimal but it seems to work well...    
    int segments = QThreadPool::globalInstance()->maxThreadCount() * 2; 
    int ws = w/segments;                    1
    LifeFunctor functor;                    2
    while (m_running) {
        qApp->processEvents();              3
        m_numGenerations++;
        QList<LifeSlice> slices;            4
        for (int c=0; c<segments; ++c) {
            int tlx = c*ws;
            QRect rect(tlx, 0, ws, boardSize.height());
            LifeSlice slice(rect, m_current);    
            slices << slice;                5
        }
        m_current = QtConcurrent::blockingMappedReduced(slices, functor,
                    stitchReduce, QtConcurrent::UnorderedReduce );  6
        m_lifeWidget->setImage(m_current);
    }
}

1

Width of a segment.

2

Map functor.

3

Ensure GUI is still responsive.

4

Break up into smaller pieces.

5

Add the pieces to a collection to be processed in parallel.

6

Do the work in parallel. stitchReduce can be called on each piece as it is ready.

<include segid="blockingMappedReduced" mode="cpp" href="src/threads/life/lifemainwindow.cpp" id="life-concurrent" src="src/threads/life/lifemainwindow.cpp"/>


The qApp->processEvents() is required in this loop for the main event loop to receive and process other GUI events. If you comment that line out and try running the application, you notice that as soon as the calculations start, there is no way to stop or quit.

This program gets 10 frames per second (fps) with four threads, compared to 4fps with one thread, on a 1024x768 board. Not quite a factor of 4, but with a larger life board, you might observe better improvements. Keep in mind, there is a synchronization point that cannot be avoided after each generation is computed.



[105] This restriction is discussed in Section 17.2: In general, it is not safe to use QPixmap outside the GUI (main) thread.