2

I don't really know how to explain my problem properly, but hopefully you can understand it using the images i provide.

I made this Mandelbrot image generator using a template and a tutorial on the internet, and i'm trying to make the generation process multi-threaded so the image gets split up into 4 equal parts and each thread calculates that part. The problem is, the first half of the image turns out black, and the second half generates fine. I have no idea what the problem is.

This code works fine without the multi-threading, so even though i feel like the problem resides there i still can't find it.

Code:

// mandelbrot.cpp
// compile with: g++ -std=c++11 -pthread mandelbrot.cpp -o mandelbrot
// view output with: eog mandelbrot.ppm

#include <fstream>
#include <iostream>
#include <complex> // if you make use of complex number facilities in C++
#include <thread>
#include <vector>

template <class T>
struct RGB
{
    T r, g, b;
};

template <class T>
class Matrix
{
  public:
    Matrix(const size_t rows, const size_t cols) : _rows(rows), _cols(cols)
    {
        _matrix = new T *[rows];
        for (size_t i = 0; i < rows; ++i)
        {
            _matrix[i] = new T[cols];
        }
    }
    Matrix(const Matrix &m) : _rows(m._rows), _cols(m._cols)
    {
        _matrix = new T *[m._rows];
        for (size_t i = 0; i < m._rows; ++i)
        {
            _matrix[i] = new T[m._cols];
            for (size_t j = 0; j < m._cols; ++j)
            {
                _matrix[i][j] = m._matrix[i][j];
            }
        }
    }
    ~Matrix()
    {
        for (size_t i = 0; i < _rows; ++i)
        {
            delete[] _matrix[i];
        }
        delete[] _matrix;
    }
    T *operator[](const size_t nIndex)
    {
        return _matrix[nIndex];
    }
    size_t width() const { return _cols; }
    size_t height() const { return _rows; }

  protected:
    size_t _rows, _cols;
    T **_matrix;
};

// Portable PixMap image
class PPMImage : public Matrix<RGB<unsigned char>>
{
  public:
    PPMImage(const size_t height, const size_t width) : Matrix(height, width) {}
    void save(const std::string &filename)
    {
        std::ofstream out(filename, std::ios_base::binary);
        out << "P6" << std::endl
            << _cols << " " << _rows << std::endl
            << 255 << std::endl;
        for (size_t y = 0; y < _rows; y++)
            for (size_t x = 0; x < _cols; x++)
                out << _matrix[y][x].r << _matrix[y][x].g << _matrix[y][x].b;
    }
};

const void calculateImage(PPMImage &image, unsigned &width, unsigned &height, unsigned &minX, unsigned &maxX, unsigned &minY, unsigned &maxY)
{
    double MinRe = -2.0;
    double MaxRe = 1.0;
    double MinIm = -1.2;
    double MaxIm = MinIm + (MaxRe - MinRe) * width / height;
    double Re_facor = (MaxRe - MinRe) / (width - 1);
    double Im_factor = (MaxIm - MinIm) / (height - 1);
    unsigned MaxIterations = 30;

    for (unsigned y = minY; y < maxY; ++y)
    {
        double c_im = MaxIm - y * Im_factor;
        for (unsigned x = minX; x < maxX; ++x)
        {
            double c_re = MinRe + x * Re_facor;

            double Z_re = c_re, Z_im = c_im;
            bool isInside = true;
            for (unsigned n = 0; n < MaxIterations; ++n)
            {
                double Z_re2 = Z_re * Z_re, Z_im2 = Z_im * Z_im;
                if (Z_re2 + Z_im2 > 4)
                {
                    isInside = false;
                    break;
                }
                Z_im = 2 * Z_re * Z_im + c_im;
                Z_re = Z_re2 - Z_im2 + c_re;
            }
            if (isInside)
            {
                image[y][x].r = 255;
                image[y][x].g = 0;
                image[y][x].b = 0;
            }
        }
    }
}

int main()
{
    unsigned width = 1600;
    unsigned height = 1600;
    unsigned minX = 0;
    unsigned minY = 0;
    unsigned maxX = 800;
    unsigned maxY = 800;

    std::cout << "Creating image...\n";
    PPMImage image(height, width);

    std::vector<std::thread> workers;
    for (int i = 0; i < 4; i++)
    {
        workers.push_back(std::thread(calculateImage, std::ref(image), std::ref(width), std::ref(height), std::ref(minX), std::ref(maxX), std::ref(minY), std::ref(maxY)));
        std::cout << "Thread #" << i << " created. Calculating (" << minX << "," << minY << ") to (" << maxX << "," << maxY << ").\n";

        if (i == 0)
        {
            minY = 800;
            maxY = 1600;
        }
        else if (i == 1)
        {
            minX = 800;
            maxX = 1600;
            minY = 0;
            maxY = 800;
        }
        else if (i == 2)
        {
            minX = 800;
            maxX = 1600;
            minY = 800;
            maxY = 1600;
        }
    }

    for (auto &th : workers)
    {
        th.join();
    }

    std::cout << "Saving to image...\n";
    image.save("mandelbrot.ppm");

    std::cout << "Finished.\n";
    return 0;
}

Resulting image: Result

You can see a part of the first half is calculated, but most of it stays black, in fact it's "overwritten" by the third and fourth thread.

It's probably a very obvious mistake but i can't figure it out.

If i try to do it without multi-threading, e.g.:

calculateImage(image, width, height, minX, maxX, minY, maxY);

It works fine.

P. Protas
  • 416
  • 4
  • 15
  • 2
    You're passing what looks like bounds to a sub-image (`minX`,`maxX`,etc) by reference, while you're changing the bounds within main. This means each thread will see the same bounds and render to the same area. I would suggest passing all your `int` parameters by value unless they really _need_ to be shared by all threads. – alter_igel Nov 19 '18 at 19:28
  • also image data is not initialized so values that are not `isInside` will contain garbage. – user7860670 Nov 19 '18 at 19:39

1 Answers1

5

You are passing references to minX, minY, maxX, maxY to the threads, but then change their values immediately after launching them.

All threads will be referring to the same variables in main. Changing the value may be observed by all the threads.

Pass the four variables by value instead.

  • Yep, this is the answer. Just removed the ampersands from the minX, maxX, minY, maxY in the function and the std::ref parts from the threading part. It works perfectly now. Seems like changing the value immediatly after the thread launching doesn't provide the expected result. – P. Protas Nov 19 '18 at 19:32
  • @P.Protas I would advice the same for `width` and `height`, because you might run into the same problem later. If there is no reason to pass by reference, then pass by value, in particular between threads. –  Nov 19 '18 at 19:36