You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 08:30:04