OpenGL ES 2异步纹理加载线程安全问题及实现方案咨询
问题描述
我在使用OpenGL ES 2,尝试用std::async在后台加载纹理,但子线程里创建GL纹理的逻辑完全不工作。查资料知道OpenGL不是线程安全的,这应该就是问题根源。想请教怎么写代码才能让大部分纹理加载工作在后台执行,同时避免这个问题。当前代码如下:
#define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" /// png format GLuint load_texture( const GLsizei width, const GLsizei height, const GLenum type, const GLvoid *pixels) { // create new OpenGL texture GLuint texture_object_id; glGenTextures(1, &texture_object_id); glBindTexture(GL_TEXTURE_2D, texture_object_id); // set texture filtering glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // generate texture from bitmap data glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); // don't create MipMaps, we must assert npot2! // glGenerateMipmap(GL_TEXTURE_2D); // release and return texture glBindTexture(GL_TEXTURE_2D, 0); // log_info("width: %d, height: %d, type: %d, pixels: %p", width, height, type, pixels); return texture_object_id; } /* externed */ vec2 tex_size; // last loaded png size as (w, h) /// textures std::mutex textures_mtx; GLuint load_png_asset_into_texture(const char* relative_path) { std::lock_guard<std::mutex> lock(textures_mtx); int w = 0, h = 0, comp= 0; unsigned char* image = stbi_load(relative_path, &w, &h, &comp, STBI_rgb_alpha); if(image == nullptr){ log_debug( "%s(%s) FAILED!", __FUNCTION__, relative_path); return 0; } GLuint tex = load_texture(w, h, comp, image); log_info("loaded %s(%s) as %d", __FUNCTION__, relative_path, tex); stbi_image_free(image); return tex; } void check_tex_for_reload(int idx) { std::string cb_path; std::lock_guard<std::mutex> lock(disc_lock); int w, h; cb_path = fmt::format("{}/{}.png", APP_PATH("covers"), all_apps[idx].info.id); if (!if_exists(cb_path.c_str()) || !is_png_vaild(cb_path.c_str(), &w, &h)) { log_info("%s doesnt exist", cb_path.c_str()); all_apps[idx].icon.cover_exists = false; } else all_apps[idx].icon.cover_exists = true; } void check_n_load_textures(int idx) { std::lock_guard<std::mutex> lock(disc_lock); int w, h; if (all_apps[idx].icon.texture.load() == GL_NULL) { std::string cb_path = fmt::format("{}/{}.png", APP_PATH("covers"), all_apps[idx].info.id); if (all_apps[idx].icon.cover_exists) all_apps[idx].icon.texture = load_png_asset_into_texture(cb_path.c_str()); else // load icon0 if cover doesnt exist all_apps[idx].icon.texture = load_png_asset_into_texture(all_apps[idx].info.picpath.c_str()); } } static void check_n_draw_textures(int idx, int SH_type, vec4 col) { // std::lock_guard<std::mutex> lock(disc_lock); cover_t.render_tex(cb_tex, SH_type, col); if (!all_apps[idx].icon.cover_exists && all_apps[idx].icon.texture == GL_NULL) cover_i.render_tex(fallback_t, SH_type, col); else cover_i.render_tex(all_apps[idx].icon.texture.load(), SH_type, col); // log_info("tex %d", all_apps[idx].icon.texture.load() ); } static void LoadIconsAsync(int i){ vec4 colo = (1.); uint64_t fmem = 0; check_tex_for_reload(i); check_n_load_textures(i); } std::vector<std::future<void>> m_future; int i = 0; for (item_t &item : all_apps) { m_future.push_back(std::async(std::launch::async, LoadIconsAsync, i)); i++; }
主循环中调用:
check_n_draw_textures(tex_idx, +1, colo);
我曾尝试用互斥锁提升线程安全性,但之后了解到GL上下文共享的相关内容,希望得到可行的实现建议。
可行实现方案
核心思路:把CPU密集的图像解码、文件读取工作放到后台线程,GL纹理的创建/上传必须回到主线程(OpenGL上下文所在线程)执行。OpenGL上下文是线程绑定的,子线程无法直接操作主线程的GL上下文(ES 2中上下文共享限制较多,不如直接分离工作逻辑简单)。
1. 重构逻辑:分离解码与GL操作
- 后台线程仅负责:检查文件存在性、读取文件、用stbi解码图像像素数据
- 主线程负责:在主循环的空闲时机,将解码好的像素数据上传为GL纹理
2. 实现线程安全的任务队列
创建一个线程安全的队列,后台线程把解码后的图像数据包装成任务放入队列,主线程定期取出任务执行GL纹理创建。
代码示例:任务队列与数据结构
#include <mutex> #include <queue> #include <memory> #include <vector> // 存储解码后的图像数据,供主线程创建纹理使用 struct TextureLoadTask { int idx; // 对应all_apps的索引 std::vector<unsigned char> pixel_data; int width; int height; }; // 线程安全的任务队列 class TaskQueue { public: void push(std::unique_ptr<TextureLoadTask> task) { std::lock_guard<std::mutex> lock(mtx); queue.push(std::move(task)); } std::unique_ptr<TextureLoadTask> pop() { std::lock_guard<std::mutex> lock(mtx); if (queue.empty()) return nullptr; auto task = std::move(queue.front()); queue.pop(); return task; } bool empty() { std::lock_guard<std::mutex> lock(mtx); return queue.empty(); } private: std::mutex mtx; std::queue<std::unique_ptr<TextureLoadTask>> queue; }; TaskQueue g_texture_tasks;
3. 修改后台加载函数
后台线程只做解码,不调用任何GL函数:
static void LoadIconsAsync(int i){ // 1. 检查封面是否存在 std::string cb_path = fmt::format("{}/{}.png", APP_PATH("covers"), all_apps[i].info.id); int w, h; bool cover_exists = if_exists(cb_path.c_str()) && is_png_vaild(cb_path.c_str(), &w, &h); // 线程安全更新cover状态 { std::lock_guard<std::mutex> lock(disc_lock); all_apps[i].icon.cover_exists = cover_exists; } // 2. 检查纹理是否已加载,避免重复工作 { std::lock_guard<std::mutex> lock(disc_lock); if (all_apps[i].icon.texture.load() != GL_NULL) { return; } } // 3. 解码图像 const char* load_path = cover_exists ? cb_path.c_str() : all_apps[i].info.picpath.c_str(); int img_w = 0, img_h = 0, comp = 0; unsigned char* image = stbi_load(load_path, &img_w, &img_h, &comp, STBI_rgb_alpha); if (!image) { log_debug("%s(%s) FAILED!", __FUNCTION__, load_path); return; } // 4. 包装任务入队列 auto task = std::make_unique<TextureLoadTask>(); task->idx = i; task->width = img_w; task->height = img_h; task->pixel_data.assign(image, image + img_w * img_h * 4); // 拷贝像素数据 stbi_image_free(image); g_texture_tasks.push(std::move(task)); }
4. 主线程处理任务
在主循环的空闲阶段(比如渲染结束后),取出队列中的任务执行GL纹理创建:
// 处理纹理加载任务的函数,在主循环中调用 void process_texture_tasks() { while (auto task = g_texture_tasks.pop()) { // 调用原有的load_texture创建纹理(仅在主线程执行) GLuint tex = load_texture(task->width, task->height, GL_RGBA, task->pixel_data.data()); // 线程安全更新all_apps中的纹理ID std::lock_guard<std::mutex> lock(disc_lock); all_apps[task->idx].icon.texture = tex; log_info("loaded texture for app %d as %d", task->idx, tex); } } // 主循环示例 while (running) { // 输入处理、渲染逻辑 check_n_draw_textures(tex_idx, +1, colo); // 处理纹理加载任务 process_texture_tasks(); // 交换缓冲区、处理事件等 }
5. 调整原有load_texture函数
确保它只在主线程被调用,无需额外锁(主线程单线程执行GL操作):
GLuint load_texture( const GLsizei width, const GLsizei height, const GLenum type, const GLvoid *pixels) { GLuint texture_object_id; glGenTextures(1, &texture_object_id); glBindTexture(GL_TEXTURE_2D, texture_object_id); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); glBindTexture(GL_TEXTURE_2D, 0); return texture_object_id; }
关键注意事项
- 禁止子线程调用GL函数:
glGenTextures、glBindTexture等所有GL API都是上下文绑定的,子线程没有主线程的GL上下文,调用会导致崩溃或无效果。 - 共享数据线程安全:对
all_apps等共享数据的读写必须用互斥锁保护,避免并发读写导致数据竞争。 - 内存管理:用智能指针管理任务对象,避免内存泄漏。
内容的提问来源于stack exchange,提问作者Seth Kruse
相关产品推荐
相关产品推荐

