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

SDL多线程环境下纹理替换同步异常问题求助

多线程下SDL纹理替换显示黑色方块问题排查与解决

问题描述

我尝试将渲染可视化与计算逻辑分离到不同线程,确保计算过程中能实时显示纹理变更,但实现时出现异常:纹理未成功替换为目标纹理,仅显示黑色方块。已尝试使用SDL线程和标准库线程,仍未定位问题原因,以下是复现问题的C语言跳棋测试代码:

#include <stdio.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include <SDL2/SDL_ttf.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <pthread.h>

#define MAIN_MENU_BACK "Checkers"
#define SCREEN_WIDTH 1400
#define SCREEN_HEIGHT 923

typedef struct 
{
    char flag;
    SDL_Rect rect;
    SDL_Texture *image;
} Checker;

typedef struct
{
    SDL_Rect rect;
    SDL_Texture *image;
    Checker** arrangment;
} Board;

typedef struct 
{
    SDL_Window *window;
    SDL_Renderer *renderer;
    SDL_Texture *background;
    Board* board;
    SDL_mutex* mutex;
} Game;

Game* game;
bool sdl_initialize(Game *game)
{
    //initializing SDL tools
    if (SDL_Init(SDL_INIT_EVERYTHING)){
        fprintf(stderr, "Error initializing SDL: %s\n", SDL_GetError());
        return true;  
    }

    int img_init = IMG_Init(IMG_INIT_PNG);
    if ((img_init & IMG_INIT_PNG) != IMG_INIT_PNG){
        fprintf(stderr, "Error initializing SDL_IMAGE_PNG: %s\n", IMG_GetError());
        return true;  
    }

    if (TTF_Init()){
        fprintf(stderr, "Error initializing SDL_TTF: %s\n", TTF_GetError());
        return true;  
    }

    //create window
    game->window = SDL_CreateWindow("Checkers", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, SCREEN_WIDTH, SCREEN_HEIGHT, 0);
    if (!game->window){
        fprintf(stderr, "Error creating window: %s\n", SDL_GetError());
        return true;  
    }
    //create renderer
    game->renderer = SDL_CreateRenderer(game->window, -1, 0);
    if (!game->renderer){
        fprintf(stderr, "Error creating renderer: %s\n", SDL_GetError());
        return true;  
    }
    
    //init background texture
    game->background = IMG_LoadTexture(game->renderer, "images/background.png");
    if (!game->background) {
        fprintf(stderr, "Error creating Texture: %s\n", IMG_GetError());
        return true;
    }

    srand((unsigned)time(NULL));

    return false;
}
void out_board(Game* game)
{
    SDL_RenderCopy(game->renderer, game->board->image, NULL, &game->board->rect);
    for (int i = 0; i < 8; i++) {
        for (int j = 0; j < 8; j++) {
            if (game->board->arrangment[i][j].flag == 'r' || game->board->arrangment[i][j].flag == 'w')
            SDL_RenderCopy(game->renderer, game->board->arrangment[i][j].image, NULL, &game->board->arrangment[i][j].rect);
        }
    }
}
void* SDL_Work(void *ptr)
{
    SDL_Texture* new_texture = IMG_LoadTexture(game->renderer, "images/white_checker.png");
    if (new_texture == NULL) {
        printf("Failed to load texture: %s\n", IMG_GetError());
        return NULL;  // Exit the thread if the texture fails to load
    }

    SDL_LockMutex(game->mutex);

    if (game->renderer == NULL) {
        printf("Renderer is not valid!\n");
        return 0;
    }

    SDL_DestroyTexture(game->board->arrangment[0][1].image);
    
    game->board->arrangment[0][1].image = new_texture;
    if (game->board->arrangment[0][1].image == NULL) {
        printf("Failed to load texture: %s\n", IMG_GetError());
    }
    
    SDL_UnlockMutex(game->mutex);
}
int main()
{
    game = (Game*)malloc(sizeof(Game));
    game->window = NULL;
    game->renderer = NULL;
    game->background = NULL;
    game->board = NULL;
    game->mutex = SDL_CreateMutex();
    
    if (sdl_initialize(game)) {
        game_cleanup(game, EXIT_FAILURE);
    }

    add_board(game);

    pthread_t thread;
    pthread_create(&thread, NULL, SDL_Work, NULL);

    while (true) 
    {   
        SDL_Event event;
        while (SDL_PollEvent(&event)) 
        {
            switch (event.type) {
            case SDL_QUIT:
                game_cleanup(game, EXIT_SUCCESS);
                break;  
            default:
                break;
            }
        }

        SDL_LockMutex(game->mutex);

        SDL_RenderClear(game->renderer);
      
        SDL_RenderCopy(game->renderer, game->background, NULL, NULL);

        out_board(game);
       
        SDL_UnlockMutex(game->mutex);
        
        SDL_RenderPresent(game->renderer);
        
        SDL_Delay(16);
    }
    game_cleanup(game, EXIT_SUCCESS);

    SDL_DestroyMutex(game->mutex);

    return 0;
}

问题根源

  1. SDL渲染器非线程安全:SDL的SDL_Renderer及其关联的纹理操作(IMG_LoadTexture、SDL_DestroyTexture)并非线程安全,所有与渲染器相关的操作必须在创建渲染器的主线程中执行。你在工作线程中直接调用IMG_LoadTexture创建纹理,会导致渲染资源状态混乱,最终显示黑色方块。
  2. 纹理生命周期冲突:工作线程销毁旧纹理时,主线程可能正在使用该纹理进行渲染,触发未定义行为,导致渲染异常。
  3. 同步逻辑覆盖不全:当前互斥锁仅保护了纹理指针的替换,但未解决跨线程操作渲染资源的核心问题。

解决方案

核心思路

工作线程仅负责计算逻辑,不直接操作SDL渲染资源;通过线程安全的消息队列通知主线程,由主线程完成纹理的加载、替换和销毁,严格遵循SDL的线程使用规则。

修正后的代码示例

1. 添加消息队列相关定义

// 线程消息结构体,用于传递纹理更新请求
typedef struct {
    int type;          // 消息类型:0表示更新纹理
    int row;           // 目标棋子行
    int col;           // 目标棋子列
    const char* path;  // 纹理路径
} ThreadMsg;

#define MSG_QUEUE_SIZE 10
ThreadMsg msg_queue[MSG_QUEUE_SIZE];
int queue_head = 0;
int queue_tail = 0;
SDL_mutex* queue_mutex;  // 消息队列互斥锁
SDL_cond* queue_cond;    // 消息队列条件变量

2. 修改工作线程逻辑

工作线程不再操作纹理,仅发送更新请求:

void* SDL_Work(void *ptr)
{
    // 模拟计算完成后,发送纹理更新请求
    ThreadMsg update_msg = {
        .type = 0,
        .row = 0,
        .col = 1,
        .path = "images/white_checker.png"
    };

    // 写入消息队列
    SDL_LockMutex(queue_mutex);
    msg_queue[queue_tail] = update_msg;
    queue_tail = (queue_tail + 1) % MSG_QUEUE_SIZE;
    SDL_CondSignal(queue_cond);  // 通知主线程有新消息
    SDL_UnlockMutex(queue_mutex);

    return NULL;
}

3. 修改主线程逻辑

主线程负责处理消息队列中的请求,完成纹理更新:

int main()
{
    game = (Game*)malloc(sizeof(Game));
    game->window = NULL;
    game->renderer = NULL;
    game->background = NULL;
    game->board = NULL;
    game->mutex = SDL_CreateMutex();
    
    // 初始化消息队列同步对象
    queue_mutex = SDL_CreateMutex();
    queue_cond = SDL_CreateCond();

    if (sdl_initialize(game)) {
        game_cleanup(game, EXIT_FAILURE);
    }

    add_board(game);

    pthread_t thread;
    pthread_create(&thread, NULL, SDL_Work, NULL);

    while (true) 
    {   
        SDL_Event event;
        while (SDL_PollEvent(&event)) 
        {
            switch (event.type) {
            case SDL_QUIT:
                game_cleanup(game, EXIT_SUCCESS);
                break;  
            default:
                break;
            }
        }

        // 处理线程消息队列
        SDL_LockMutex(queue_mutex);
        while (queue_head != queue_tail) {
            ThreadMsg curr_msg = msg_queue[queue_head];
            queue_head = (queue_head + 1) % MSG_QUEUE_SIZE;
            SDL_UnlockMutex(queue_mutex);

            // 处理纹理更新请求
            if (curr_msg.type == 0) {
                SDL_Texture* new_tex = IMG_LoadTexture(game->renderer, curr_msg.path);
                if (new_tex != NULL) {
                    SDL_LockMutex(game->mutex);
                    // 销毁旧纹理,替换为新纹理
                    SDL_DestroyTexture(game->board->arrangment[curr_msg.row][curr_msg.col].image);
                    game->board->arrangment[curr_msg.row][curr_msg.col].image = new_tex;
                    SDL_UnlockMutex(game->mutex);
                } else {
                    fprintf(stderr, "Failed to load texture: %s\n", IMG_GetError());
                }
            }

            SDL_LockMutex(queue_mutex);
        }
        SDL_UnlockMutex(queue_mutex);

        // 渲染流程
        SDL_LockMutex(game->mutex);
        SDL_RenderClear(game->renderer);
        SDL_RenderCopy(game->renderer, game->background, NULL, NULL);
        out_board(game);
        SDL_UnlockMutex(game->mutex);
        
        SDL_RenderPresent(game->renderer);
        SDL_Delay(16);
    }

    // 清理资源
    SDL_DestroyCond(queue_cond);
    SDL_DestroyMutex(queue_mutex);
    SDL_DestroyMutex(game->mutex);
    game_cleanup(game, EXIT_SUCCESS);

    return 0;
}

关键注意事项

  • 所有SDL渲染相关API(纹理创建、销毁、渲染拷贝)必须在主线程调用,这是SDL的硬性规则。
  • 使用互斥锁保护共享数据(如棋盘纹理指针)的读写,避免数据竞争。
  • 通过条件变量+消息队列实现线程间安全通信,确保工作线程的更新请求能被主线程及时处理。

内容的提问来源于stack exchange,提问作者Kasatka

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 22:34:58