Ubuntu 23.10下SDL2与std::thread渲染冲突及退出方案咨询
问题解决:SDL2多线程渲染异常与退出响应兼容方案
问题根源
Ubuntu 23.10搭载的SDL2版本对跨线程调用渲染API的限制更严格——SDL的视频渲染上下文(SDL_Renderer/SDL_Window)并非线程安全,跨线程直接操作这些对象会触发未定义行为,而Ubuntu 22.04的SDL版本可能未严格校验这种操作,导致代码在旧版本正常、新版本异常。
移出线程后主线程被program阻塞,无法处理退出事件,本质是主线程被耗时操作占用,无法轮询SDL事件队列。
正确实现方案
核心思路:用原子布尔变量作为跨线程退出信号,子线程负责耗时的迷宫生成/求解+渲染逻辑(需定期检查退出信号),主线程专注处理事件循环,收到退出指令时触发子线程终止,再完成资源清理。
修改后的完整代码
#include <SDL2/SDL.h> #include <array> #include <cstdlib> #include <time.h> #include <thread> #include <atomic> #include "const.hpp" #include "maze_crea.hpp" #include "maze_solver.hpp" // 原子布尔变量,用于跨线程传递退出信号(主线程设置,子线程检查) std::atomic<bool> g_shouldQuit = false; // 修改program函数,添加停止标志参数,在耗时操作中定期检查 void program(SDL_Window* pWindow, SDL_Renderer* pRenderer, std::atomic<bool>& shouldQuit){ std::array<int, MAZE_SIZE<int>> maze; std::array<SDL_Rect, MAZE_SIZE<int>> squares; for (int i = 0; i < MAZE_SIZE<int>; i++){ squares[i] = { (i % SIDE_SIZE<int>) * (WIDTHSCREEN<int> / SIDE_SIZE<int>), (i / SIDE_SIZE<int>) * (HEIGHTSCREEN<int> / SIDE_SIZE<int>), WIDTHSCREEN<int> / SIDE_SIZE<int>, HEIGHTSCREEN<int> / SIDE_SIZE<int> }; if (((i % SIDE_SIZE<int>) % 2 == 0) || ((i / SIDE_SIZE<int>) % 2 == 0)){ maze[i] = 0; } else { maze[i] = i; } // 循环中检查退出信号,避免阻塞过久 if(shouldQuit.load()) return; } SDL_SetWindowTitle(pWindow, "Maze Solver (creation)"); // 假设mazeCrea内部是循环执行的,需要修改它接受停止标志并定期检查 mazeCrea(pRenderer, maze, squares, shouldQuit); if(shouldQuit.load()) return; SDL_SetWindowTitle(pWindow, "Maze Solver (solution)"); // 同理修改mazeSolver,加入停止标志检查 mazeSolver(pRenderer, maze, squares, shouldQuit); } int main(int argc, char* argv[]) { if (SDL_Init(SDL_INIT_VIDEO) < 0) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[DEBUG] > %s", SDL_GetError()); return EXIT_FAILURE; } SDL_Window* pWindow{ nullptr }; SDL_Renderer* pRenderer{ nullptr }; if (SDL_CreateWindowAndRenderer(WIDTHSCREEN<unsigned int>, HEIGHTSCREEN<unsigned int>, SDL_WINDOW_SHOWN, &pWindow, &pRenderer) < 0) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[DEBUG] > %s", SDL_GetError()); SDL_Quit(); return EXIT_FAILURE; } srand(time(NULL)); // 启动子线程,传递窗口、渲染器和退出标志 std::thread Thread(program, pWindow, pRenderer, std::ref(g_shouldQuit)); SDL_Event events; while (true) { while (SDL_PollEvent(&events)) { switch (events.type) { case SDL_QUIT: // 设置退出信号,通知子线程终止 g_shouldQuit.store(true); // 等待子线程结束,避免资源提前释放 Thread.join(); // 清理SDL资源 SDL_DestroyRenderer(pRenderer); SDL_DestroyWindow(pWindow); SDL_Quit(); return EXIT_SUCCESS; break; } } // 短暂休眠,减少空循环的CPU占用 SDL_Delay(10); } // 正常流程下不会走到这里,保险起见仍加入清理逻辑 Thread.join(); SDL_DestroyRenderer(pRenderer); SDL_DestroyWindow(pWindow); SDL_Quit(); return EXIT_SUCCESS; }
关键修改点说明
- 原子退出信号:使用
std::atomic<bool>保证跨线程读写的线程安全性,避免数据竞争。 - 子线程定期检查:在
program的循环、mazeCrea、mazeSolver内部添加退出标志检查,确保子线程能及时响应主线程的终止指令(需同步修改maze_crea.hpp和maze_solver.hpp对应的函数接口,加入停止参数)。 - 主线程事件循环:启动子线程后立即进入事件循环,收到
SDL_QUIT时先设置退出标志,再等待子线程结束,最后清理资源——避免子线程还在操作SDL资源时主线程提前释放,导致崩溃。 - CPU占用优化:主线程事件循环中加入
SDL_Delay(10),减少空循环的CPU消耗。
额外注意事项
- 如果
mazeCrea或mazeSolver是长时间阻塞的函数(无内部循环),需要将其拆分为可中断的步骤,或者使用SDL的线程信号机制,最稳妥的方式是让这些函数支持定期检查退出标志。 - 严格遵循SDL线程安全规范:
SDL_Window和SDL_Renderer的创建/销毁必须在主线程完成,子线程仅可在主线程初始化完成后调用渲染API(部分SDL版本允许,但仍需谨慎),更安全的方式是将渲染逻辑移回主线程,子线程仅传递计算好的迷宫数据。
内容的提问来源于stack exchange,提问作者ISEVEN
相关产品推荐
相关产品推荐

