792 字
4 分钟
多线程

总体思路

原来是单线程:

for (int j = 0; j < image_height; j++) {
for (int i = 0; i < image_width; i++) {
...
write_color(std::cout, ...);
}
}

多线程后变成:

  1. 先创建一个 framebuffer 保存整张图片的颜色。
  2. 多个线程同时工作。
  3. 每个线程通过原子计数器领取下一行 j
  4. 线程只写自己那一行对应的 framebuffer 区域。
  5. 所有线程结束后,主线程再按从上到下、从左到右的顺序输出 PPM。
  6. std::chrono 统计渲染时间。

第 1 步:添加头文件

camera.h 顶部加:

#include <atomic>
#include <chrono>
#include <thread>

原因:

  • atomic:安全地分配扫描线。
  • thread:创建工作线程。
  • chrono:统计渲染耗时。

第 2 步:不要让线程直接写 std::cout

PPM 图片要求像素顺序严格一致。多个线程如果同时 write_color(std::cout, ...),输出顺序会乱,图片就坏了。

所以先在 render() 里创建缓冲区:

std::vector<color> framebuffer(image_width * image_height);

j 行第 i 列像素的位置是:

framebuffer[j * image_width + i]

第 3 步:创建行任务计数器

std::atomic<int> next_row{0};
std::atomic<int> rows_done{0};

next_row 表示下一条还没被领取的扫描线。

每个线程执行:

int j = next_row.fetch_add(1);

这样多个线程不会拿到同一行。

第 4 步:写工作线程函数

render() 里定义一个 lambda:

auto render_row = [this, &world, &framebuffer, &next_row, &rows_done]() {
while (true) {
int j = next_row.fetch_add(1);
if (j >= image_height) {
break;
}
for (int i = 0; i < image_width; i++) {
color pixel_color(0, 0, 0);
for (int sample = 0; sample < samples_per_pixel; sample++) {
ray r = get_ray(i, j);
pixel_color += ray_color(r, max_depth, world);
}
framebuffer[j * image_width + i] = pixel_samples_scale * pixel_color;
}
auto done = rows_done.fetch_add(1) + 1;
std::clog << "\r剩余扫描线: " << (image_height - done) << ' ' << std::flush;
}
};

这里每个线程会不断领取一行、渲染一行,直到所有行都被领完。

第 5 步:创建线程

auto worker_count = std::thread::hardware_concurrency();
if (worker_count == 0) {
worker_count = 4;
}

然后启动线程:

std::vector<std::thread> workers;
workers.reserve(worker_count);
for (unsigned int t = 0; t < worker_count; t++) {
workers.emplace_back(render_row);
}

第 6 步:等待线程结束

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

join() 的意思是:主线程等这些渲染线程全部完成。

第 7 步:统一输出图片

线程全部结束后,再由主线程输出:

std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n";
for (int j = 0; j < image_height; j++) {
for (int i = 0; i < image_width; i++) {
write_color(std::cout, framebuffer[j * image_width + i]);
}
}

这样输出顺序仍然和单线程一样。

第 8 步:统计渲染时间

render() 开始附近加:

auto render_start = std::chrono::steady_clock::now();

在输出完成后加:

auto render_end = std::chrono::steady_clock::now();
std::chrono::duration<double> render_time = render_end - render_start;
std::clog << "渲染时间: " << render_time.count() << " 秒\n";

std::clog 是因为图片数据走 std::cout,日志走 std::clog,两者不会混在一起。

第 9 步:修复随机数线程安全问题

原来的:

inline double random_double() {
return std::rand() / (RAND_MAX + 1.0);
}

std::rand() 在多线程里不适合共享使用。可以在 rtweekend.h 改成:

#include <random>

然后:

inline double random_double() {
static thread_local std::mt19937 generator(std::random_device{}());
static thread_local std::uniform_real_distribution<double> distribution(0.0, 1.0);
return distribution(generator);
}

thread_local 表示每个线程都有自己的随机数生成器,互不干扰。

最终结构就是:

void render(const hittable& world) {
initialize();
auto render_start = std::chrono::steady_clock::now();
std::vector<color> framebuffer(image_width * image_height);
std::atomic<int> next_row{0};
std::atomic<int> rows_done{0};
auto render_row = ...;
create threads;
join threads;
output ppm header;
output framebuffer;
print render time;
}