是一个 Mandelbrot 分形的多线程计算。 用户将看到分形,并能够在该窗口中平移和缩放。Mandelbrot 分形是一个处理复数 (a + bi) 的数值集,该图像中的每个黑色像素都趋向于发散到一个无限值,,而绿色像素则有界于一个有限值。绿色像素属于 Mandelbrot 集。CPU 架构的浮点数精度限制了缩放。如果继续缩放,则会出现视觉伪像,因为比例因子只能处理 15 到 17 位有效数字。
必须仔细设计应用程序的架构。因为我们在使用线程,所以很容易导致死锁、线程饥饿,甚至更糟的是,冻结 UI。我们真的想最大限度地利用 CPU。为此,我们将在每个内核上执行尽可能多的线程。每个线程将负责在返回结果之前计算 Mandelbrot 集的一部分。
MandelbrotWidget:这要求显示图片。它处理绘图和用户交互。这个对象存在于 UI 线程中。
MandelbrotCalculator:它处理图片请求并在将其发送回
MandelbrotWidget 之前聚合生成的 JobResults。这个对象存在于它自己的线程中。
Job:这会在将结果传输回 MandelbrotCalculator 之前计算最终图片的一部分.每个作业都存在于自己的线程中。
MandelbrotCalculator 线程将使用 QThreadPool 类在它们自己的线程中分派作业。这将根据您的 CPU 内核完美扩展。
每个Job将计算最终图片的一行,然后通过 JobResult 对象将其发送回 MandelbrotCalculator。
MandelbrotCalculator 线程实际上是计算的协调者。考虑一个用户在计算完成之前放大图片
MandelbrotWidget 将向 MandelbrotCalculator 请求一张新图片,MandelbrotCalculator 又必须在分派新作业之前取消所有当前作业
使用 QRunnable 定义作业类
让我们深入了解项目的核心。为了加速 Mandelbrot 图片生成,我们将整个计算拆分为多个作业。作业是任务的请求。 根据您的 CPU 架构,多个作业将同时执行。Job 类产生一个包含结果值的 JobResult 函数。Job 类为完整图片的一行生成值。 例如,800 x 600 的图像分辨率需要 600 个作业,每个作业生成 800 个值。
JobResult.h
#ifndef JOBRESULT_H
#define JOBRESULT_H#include <QSize>
#include <QVector>
#include <QPointF>struct JobResult
{//输入数据(areaSize,pixelPositionY,...)//作业类生成的Result值JobResult(int valueCount = 1) :areaSize(0, 0),pixelPositionY(0),moveOffset(0, 0),scaleFactor(0.0),values(valueCount){}QSize areaSize;int pixelPositionY;QPointF moveOffset;double scaleFactor;QVector<int> values;
};#endif // JOBRESULT_H
Job.h
#ifndef JOB_H
#define JOB_H#include <QObject>
#include <QRunnable>
#include <QPointF>
#include <QSize>
#include <QAtomicInteger>#include "JobResult.h"class Job : public QObject, public QRunnable
{//这个Job类是一个QRunnable,所以我们可以重写run()来实现Mandelbrot图片算法//Job 也继承自 QObject,让我们可以使用 Qt 的信号/槽特性Q_OBJECT
public:Job(QObject *parent = 0);void run() override;void setPixelPositionY(int value);void setMoveOffset(const QPointF& value);void setScaleFactor(double value);void setAreaSize(const QSize& value);void setIterationMax(int value);signals://当算法结束时,将发出 jobCompleted() 信号//jobResult 参数包含结果值void jobCompleted(JobResult jobResult);public slots://abort() 槽将允许我们停止更新 mIsAbort 标志值的作业void abort();private://mAbort 不是经典的 bool,而是 QAtomicInteger<bool>//这种 Qt 跨平台类型使我们可以不间断地执行原子操作//您可以使用互斥锁或其他同步机制来完成这项工作//但使用原子变量是安全更新和访问来自不同线程的变量的快速方法QAtomicInteger<bool> mAbort;//mPixelPositionY变量是图片高度索引//因为每个Job只为一个图片行生成数据,所以我们需要这些信息int mPixelPositionY;//mMoveOffset变量是Mandelbrot原点偏移量//用户可以平移图片,因此原点不会总是 (0, 0)QPointF mMoveOffset;//mScaleFactor 用户还可以放大图片。double mScaleFactor;//mAreaSize 变量是以像素为单位的最终图片大小QSize mAreaSize;//mIterationMax 变量是允许确定一个像素的 Mandelbrot 结果的迭代次数int mIterationMax;
};#endif // JOB_H
Job.cpp
#include "Job.h"Job::Job(QObject* parent) :QObject(parent),mAbort(false),mPixelPositionY(0),mMoveOffset(0.0, 0.0),mScaleFactor(0.0),mAreaSize(0, 0),mIterationMax(1)
{
}void Job::run()
{//初始化一个 JobResult 变量//区域大小的宽度用于将JobResult::values构造为具有正确初始大小的QVector//其他输入数据从Job 复制到JobResult,让JobResult 的接收者得到带有上下文输入数据的结果JobResult jobResult(mAreaSize.width());jobResult.areaSize = mAreaSize;jobResult.pixelPositionY = mPixelPositionY;jobResult.moveOffset = mMoveOffset;jobResult.scaleFactor = mScaleFactor;double imageHalfWidth = mAreaSize.width() / 2.0;double imageHalfHeight = mAreaSize.height() / 2.0;//for循环在一行上遍历所有x个像素位置for (int imageX = 0; imageX < mAreaSize.width(); ++imageX) {if (mAbort.load()) {return;}int iteration = 0;//像素位置转换为复平面坐标double x0 = (imageX - imageHalfWidth) * mScaleFactor + mMoveOffset.x();double y0 = (mPixelPositionY - imageHalfHeight) * mScaleFactor - mMoveOffset.y();double x = 0.0;double y = 0.0;//如果试验次数超过最大迭代次数do {double nextX = (x * x) - (y * y) + x0;y = 2.0 * x * y + y0;x = nextX;iteration++;} while(iteration < mIterationMax&& (x * x) + (y * y) < 4.0);jobResult.values[imageX] = iteration;}emit jobCompleted(jobResult);
}void Job::setPixelPositionY(int value)
{mPixelPositionY = value;
}void Job::setMoveOffset(const QPointF& value)
{mMoveOffset = value;
}void Job::setScaleFactor(double value)
{mScaleFactor = value;
}void Job::setAreaSize(const QSize& value)
{mAreaSize = value;
}void Job::setIterationMax(int value)
{mIterationMax = value;
}
//此方法执行值 true 的原子写入
//原子机制确保我们可以从多个线程调用 abort() 而不会中断 run() 函数中的 mAbort 读取
void Job::abort()
{mAbort.store(true);
}
在 MandelbrotCalculator 中使用 QThreadPool
MandelbrotCalculator.h
#ifndef MANDELBROTCALCULATOR_H
#define MANDELBROTCALCULATOR_H#include <QObject>
#include <QSize>
#include <QPointF>
#include <QElapsedTimer>
#include <QList>#include "JobResult.h"class Job;class MandelbrotCalculator : public QObject
{Q_OBJECT
public:explicit MandelbrotCalculator(QObject *parent = 0);signals://pictureLinesGenerated():这个信号被定期触发以发送结果void pictureLinesGenerated(QList<JobResult> jobResults);//abortAllJobs():该信号用于中止所有活动作业void abortAllJobs();public slots://generatePicture():调用者使用这个槽来请求一个新的 Mandelbrot 图片//此功能准备并启动作业void generatePicture(QSize areaSize, QPointF moveOffset, double scaleFactor, int iterationMax);//process():这个槽处理作业产生的结果void process(JobResult jobResult);private://createJob():这是一个创建和配置新作业的辅助函数Job* createJob(int pixelPositionY);//clearJobs():此插槽删除排队的作业并中止活动的作业void clearJobs();private:QPointF mMoveOffset;double mScaleFactor;QSize mAreaSize;int mIterationMax;//mReceivedJobResults 变量是接收到的 JobResult 的计数,它是由作业发送的int mReceivedJobResults;//mJobResults 变量是一个包含接收到的 JobResult 的列表QList<JobResult> mJobResults;//mTimer 变量计算为请求的图片运行所有作业所用的时间QElapsedTimer mTimer;
};#endif // MANDELBROTCALCULATOR_H
MandelbrotCalculator.cpp
#include "MandelbrotCalculator.h"
#include <QDebug>
#include <QThreadPool>
#include "Job.h"
const int JOB_RESULT_THRESHOLD = 10;
MandelbrotCalculator::MandelbrotCalculator(QObject *parent): QObject(parent),mMoveOffset(0.0, 0.0),mScaleFactor(0.005),mAreaSize(0, 0),mIterationMax(10),mReceivedJobResults(0),mJobResults(),mTimer()
{
}void MandelbrotCalculator::generatePicture(QSize areaSize, QPointF moveOffset, double scaleFactor, int iterationMax)
{if (areaSize.isEmpty()) {return;}//启动 mTimer来跟踪整个生成时长mTimer.start();//每个新的图片生成将首先通过调用 clearJobs() 取消现有作业clearJobs();mAreaSize = areaSize;mMoveOffset = moveOffset;mScaleFactor = scaleFactor;mIterationMax = iterationMax;for(int pixelPositionY = 0; pixelPositionY < mAreaSize.height(); pixelPositionY++) {//QThreadPool::globalInstance() 是一个静态函数,它根据 CPU 的核心数为我们提供最佳全局线程池//即使我们为所有 Job 类调用 start(),也只有第一个类会立即启动//其他人被添加到等待可用线程的池队列中QThreadPool::globalInstance()->start(createJob(pixelPositionY));}
}
//每次作业完成其任务时都会调用此槽
void MandelbrotCalculator::process(JobResult jobResult)
{//首先要检查的是当前的 JobResult 对于当前的输入数据是否仍然有效//当请求新图片时,我们清除作业队列并中止活动作业if (jobResult.areaSize != mAreaSize ||jobResult.moveOffset != mMoveOffset ||jobResult.scaleFactor != mScaleFactor) {return;}//如果旧的 JobResult 仍然发送到这个 process() 槽//我们可以增加 mReceivedJobResults 计数器并将此 JobResult 附加到我们的成员队列 mJobResultsmReceivedJobResults++;mJobResults.append(jobResult);if (mJobResults.size() >= JOB_RESULT_THRESHOLD || mReceivedJobResults == mAreaSize.height()) {emit pictureLinesGenerated(mJobResults);mJobResults.clear();}if (mReceivedJobResults == mAreaSize.height()) {qDebug() << "Generated in " << mTimer.elapsed() << " ms";}
}Job* MandelbrotCalculator::createJob(int pixelPositionY)
{//作业是在堆上分配的//此操作在 MandelbrotCalculator 线程中需要一些时间//当我们调用 QThreadPool::start() 时,线程池获得了作业的所有权//它会在 Job::run() 结束时被线程池删除Job* job = new Job();job->setPixelPositionY(pixelPositionY);job->setMoveOffset(mMoveOffset);job->setScaleFactor(mScaleFactor);job->setAreaSize(mAreaSize);job->setIterationMax(mIterationMax);//发出 abortAllJobs() 信号将调用所有作业的 abort() 槽connect(this, &MandelbrotCalculator::abortAllJobs,job, &Job::abort);//每次 Job 完成其任务时都会执行我们的 process() 槽connect(job, &Job::jobCompleted,this, &MandelbrotCalculator::process);return job;
}void MandelbrotCalculator::clearJobs()
{mReceivedJobResults = 0;emit abortAllJobs();QThreadPool::globalInstance()->clear();
}
您应该识别一些已知的变量名称,例如 mScaleFactor、mMoveOffset、mAreaSize 或 mIterationMax
#ifndef MANDELBROTWIDGET_H
#define MANDELBROTWIDGET_H#include <memory>#include <QWidget>
#include <QPoint>
#include <QThread>
#include <QList>#include "MandelbrotCalculator.h"class QResizeEvent;class MandelbrotWidget : public QWidget
{Q_OBJECTpublic:explicit MandelbrotWidget(QWidget *parent = 0);~MandelbrotWidget();public slots://processJobResults() 函数将处理由 MandelbrotCalculator 调度的 JobResult 列表void processJobResults(QList<JobResult> jobResults);signals://每次用户更改输入数据(偏移、比例或区域大小)时都会发出 requestPicture() 信号void requestPicture(QSize areaSize, QPointF moveOffset, double scaleFactor, int iterationMax);protected://PaintEvent() 函数使用当前 mImage 绘制widgetvoid paintEvent(QPaintEvent* event) override;//当用户调整窗口大小时,resizeEvent() 函数会调整 Mandelbrot 区域的大小void resizeEvent(QResizeEvent* event) override;//wheelEvent() 函数处理用户鼠标滚轮事件以应用比例因子void wheelEvent(QWheelEvent* event) override;//mousePressEvent() 函数和 mouseMoveEvent() 检索用户鼠标事件以移动 Mandelbrot 图片void mousePressEvent(QMouseEvent* event) override;void mouseMoveEvent(QMouseEvent* event) override;private://generateColorFromIteration() 是一个辅助函数,用于对 Mandelbrot 图片进行着色。 按像素的迭代值转换为颜色值QRgb generateColorFromIteration(int iteration);private://mMandelbrotCalculator 变量是我们的多线程作业管理器,widget将向它发出请求并等待结果MandelbrotCalculator mMandelbrotCalculator;//mThreadCalculator 变量允许 Mandelbrot 计算器在它自己的线程中运行QThread mThreadCalculator;double mScaleFactor;//widget使用mLastMouseMovePosition变量来处理平移功能的用户事件QPoint mLastMouseMovePosition;QPointF mMoveOffset;QSize mAreaSize;int mIterationMax;//mImage 变量是widget显示的当前图片。 它是一个unique_ptr指针,因此MandelbrotWidget是mImage的所有者std::unique_ptr<QImage> mImage;
};#endif // MANDELBROTWIDGET_H
MandelbrotWidget.cpp
#include "MandelbrotWidget.h"#include <QResizeEvent>
#include <QImage>
#include <QPainter>
#include <QtMath>const int ITERATION_MAX = 4000;
const double DEFAULT_SCALE = 0.005;
const double DEFAULT_OFFSET_X = -0.74364390249094747;
const double DEFAULT_OFFSET_Y = 0.13182589977450967;MandelbrotWidget::MandelbrotWidget(QWidget *parent) :QWidget(parent),mMandelbrotCalculator(),mThreadCalculator(this),mScaleFactor(DEFAULT_SCALE),mLastMouseMovePosition(),mMoveOffset(DEFAULT_OFFSET_X, DEFAULT_OFFSET_Y),mAreaSize(),mIterationMax(ITERATION_MAX)
{mMandelbrotCalculator.moveToThread(&mThreadCalculator);connect(this, &MandelbrotWidget::requestPicture,&mMandelbrotCalculator, &MandelbrotCalculator::generatePicture);connect(&mMandelbrotCalculator, &MandelbrotCalculator::pictureLinesGenerated,this, &MandelbrotWidget::processJobResults);mThreadCalculator.start();
}MandelbrotWidget::~MandelbrotWidget()
{ mThreadCalculator.quit();mThreadCalculator.wait(1000);if (!mThreadCalculator.isFinished()) {mThreadCalculator.terminate();}
}void MandelbrotWidget::processJobResults(QList<JobResult> jobResults)
{int yMin = height();int yMax = 0;for(JobResult& jobResult : jobResults) {if (mImage->size() != jobResult.areaSize) {continue;}int y = jobResult.pixelPositionY;QRgb* scanLine = reinterpret_cast<QRgb*>(mImage->scanLine(y));for (int x = 0; x < mAreaSize.width(); ++x) {scanLine[x] = generateColorFromIteration(jobResult.values[x]);}if (y < yMin) {yMin = y;}if (y > yMax) {yMax = y;}}repaint(0, yMin,width(), yMax);
}void MandelbrotWidget::resizeEvent(QResizeEvent* event)
{mAreaSize = event->size();mImage = std::make_unique<QImage>(mAreaSize, QImage::Format_RGB32);mImage->fill(Qt::black);emit requestPicture(mAreaSize, mMoveOffset, mScaleFactor, mIterationMax);
}void MandelbrotWidget::wheelEvent(QWheelEvent* event)
{int delta = event->delta();mScaleFactor *= qPow(0.75, delta / 120.0);emit requestPicture(mAreaSize, mMoveOffset, mScaleFactor, mIterationMax);
}void MandelbrotWidget::mousePressEvent(QMouseEvent* event)
{if (event->buttons() & Qt::LeftButton) {mLastMouseMovePosition = event->pos();}
}void MandelbrotWidget::mouseMoveEvent(QMouseEvent* event)
{if (event->buttons() & Qt::LeftButton) {QPointF offset = mLastMouseMovePosition - event->pos();mLastMouseMovePosition = event->pos();offset.setY(-offset.y());mMoveOffset += offset * mScaleFactor;emit requestPicture(mAreaSize, mMoveOffset, mScaleFactor, mIterationMax);}
}void MandelbrotWidget::paintEvent(QPaintEvent* event)
{QPainter painter(this);painter.save();QRect imageRect = event->region().boundingRect();painter.drawImage(imageRect, *mImage, imageRect);painter.setPen(Qt::white);painter.drawText(10, 20, QString("Size: %1 x %2").arg(mImage->width()).arg(mImage->height()));painter.drawText(10, 35, QString("Offset: %1 x %2").arg(mMoveOffset.x()).arg(mMoveOffset.y()));painter.drawText(10, 50, QString("Scale: %1").arg(mScaleFactor));painter.drawText(10, 65, QString("Max iteration: %1").arg(ITERATION_MAX));painter.restore();
}QRgb MandelbrotWidget::generateColorFromIteration(int iteration)
{if (iteration == mIterationMax) {return qRgb(50, 50, 255);}return qRgb(0, 0, (255.0 * iteration / mIterationMax));
}