792 字
4 分钟
多线程
总体思路
原来是单线程:
for (int j = 0; j < image_height; j++) { for (int i = 0; i < image_width; i++) { ... write_color(std::cout, ...); }}多线程后变成:
- 先创建一个
framebuffer保存整张图片的颜色。 - 多个线程同时工作。
- 每个线程通过原子计数器领取下一行
j。 - 线程只写自己那一行对应的 framebuffer 区域。
- 所有线程结束后,主线程再按从上到下、从左到右的顺序输出 PPM。
- 用
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;}