教程 49 - 任务系统 (Job System)


🔗 快速导航


📋 目录

点击展开/折叠

🎯 概述

任务系统(Job System) 是现代游戏引擎的核心并发架构,它比传统的固定线程模型更灵活、更高效。任务系统将工作分解为小的、独立的任务,然后动态分配到工作线程池中执行。

在本教程中,我们将:

  • ✅ 理解任务系统的优势
  • ✅ 实现无锁环形队列
  • ✅ 设计任务类型和优先级系统
  • ✅ 实现工作线程池和任务调度
  • ✅ 处理任务结果回调

❓ 为什么需要任务系统?

传统的多线程模型使用固定线程,每个线程执行特定的工作:

任务系统的优势

任务队列

工作线程 1

工作线程 2

工作线程 3

工作线程 4

动态负载均衡

固定线程模型的问题

渲染线程

有时空闲

物理线程

有时过载

AI 线程

有时空闲

音频线程

CPU 利用率不均

任务系统的优势

优势描述示例
负载均衡工作自动分配到空闲线程所有核心保持高利用率
细粒度并行任务可以很小,易于并行每个敌人 AI 是一个任务
优先级管理重要任务先执行关键路径任务优先
类型隔离相同类型任务在同一线程资源加载避免磁盘抖动
简化编程无需管理线程生命周期只需提交任务

🆚 固定线程 vs 任务系统

固定线程模型

// 每个系统有自己的线程
void render_thread() {
    while (running) {
        wait_for_frame();
        render_scene();  // 可能很快完成,然后线程空闲
    }
}

void physics_thread() {
    while (running) {
        wait_for_tick();
        simulate_physics();  // 可能很慢,其他线程在等待
    }
}

任务系统模型

// 提交细粒度任务
for (u32 i = 0; i < enemy_count; ++i) {
    job_info job = job_create(update_enemy_ai, NULL, NULL, &enemies[i], ...);
    job_system_submit(job);
}

for (u32 i = 0; i < particle_count; i += BATCH_SIZE) {
    job_info job = job_create(update_particles_batch, NULL, NULL, &batch_data, ...);
    job_system_submit(job);
}

// 工作线程自动分配和执行这些任务

对比表

特性固定线程任务系统
线程数量固定(通常 4-6 个)动态(通常等于 CPU 核心数)
负载均衡手动自动
CPU 利用率不均匀接近 100%
编程复杂度高(需要管理同步)低(只需提交任务)
可扩展性差(不适应不同 CPU)好(自动适应核心数)

🔄 环形队列 (Ring Queue)

环形队列是任务系统的基础数据结构,用于存储待执行的任务。

数据结构

/**
 * @brief 特定大小的环形队列
 * 不会动态调整大小
 * 这是一个先进先出(FIFO)结构
 */
typedef struct ring_queue {
    /** @brief 当前包含的元素数量 */
    u32 length;

    /** @brief 每个元素的大小(字节) */
    u32 stride;

    /** @brief 总可用元素数量 */
    u32 capacity;

    /** @brief 保存数据的内存块 */
    void* block;

    /** @brief 指示队列是否拥有其内存块 */
    b8 owns_memory;

    /** @brief 列表头部的索引 */
    i32 head;

    /** @brief 列表尾部的索引 */
    i32 tail;
} ring_queue;

可视化

环形队列布局:
capacity = 8, head = 2, tail = 6, length = 4

[x][x][A][B][C][D][x][x]
       ↑           ↑
      head        tail

入队(enqueue):tail 位置插入,tail++
出队(dequeue):从 head 位置取出,head++

环绕(wrapping):
当 head 或 tail 到达 capacity 时,回绕到 0

[x][x][x][x][C][D][E][F]
               ↑       ↑
              head    tail (已环绕)

核心操作

创建队列

/**
 * @brief 创建给定容量和步长的新环形队列
 *
 * @param stride 每个元素的大小(字节)
 * @param capacity 队列中可用的总元素数
 * @param memory 用于保存数据的内存块
 *               应为 stride * capacity 大小
 *               如果传入 0,则自动分配和释放
 * @param out_queue 指向保存新创建队列的指针
 * @return 成功返回 true,否则返回 false
 */
b8 ring_queue_create(u32 stride, u32 capacity, void* memory, ring_queue* out_queue);

实现

b8 ring_queue_create(u32 stride, u32 capacity, void* memory, ring_queue* out_queue) {
    out_queue->length = 0;
    out_queue->stride = stride;
    out_queue->capacity = capacity;
    out_queue->head = 0;
    out_queue->tail = 0;
    out_queue->owns_memory = memory == 0;

    if (memory) {
        out_queue->block = memory;
    } else {
        out_queue->block = kallocate(stride * capacity, MEMORY_TAG_RING_QUEUE);
    }

    return true;
}

入队(Enqueue)

/**
 * @brief 如果有空间,将值添加到队列
 *
 * @param queue 指向要添加数据的队列的指针
 * @param value 要添加的值
 * @return 成功返回 true,否则返回 false
 */
b8 ring_queue_enqueue(ring_queue* queue, void* value) {
    if (queue->length >= queue->capacity) {
        return false;  // 队列已满
    }

    // 复制数据到 tail 位置
    u64 addr = (u64)queue->block;
    addr += (queue->tail * queue->stride);
    kcopy_memory((void*)addr, value, queue->stride);

    // 移动 tail,环绕
    queue->tail = (queue->tail + 1) % queue->capacity;
    queue->length++;

    return true;
}

出队(Dequeue)

/**
 * @brief 尝试从提供的队列中检索下一个值
 *
 * @param queue 指向要检索数据的队列的指针
 * @param out_value 指向保存检索值的指针
 * @return 成功返回 true,否则返回 false
 */
b8 ring_queue_dequeue(ring_queue* queue, void* out_value) {
    if (queue->length == 0) {
        return false;  // 队列为空
    }

    // 从 head 位置复制数据
    u64 addr = (u64)queue->block;
    addr += (queue->head * queue->stride);
    kcopy_memory(out_value, (void*)addr, queue->stride);

    // 移动 head,环绕
    queue->head = (queue->head + 1) % queue->capacity;
    queue->length--;

    return true;
}

窥视(Peek)

/**
 * @brief 尝试检索但不移除队列中的下一个值(如果不为空)
 *
 * @param queue 指向要检索数据的队列的常量指针
 * @param out_value 指向保存检索值的指针
 * @return 成功返回 true,否则返回 false
 */
b8 ring_queue_peek(const ring_queue* queue, void* out_value) {
    if (queue->length == 0) {
        return false;
    }

    u64 addr = (u64)queue->block;
    addr += (queue->head * queue->stride);
    kcopy_memory(out_value, (void*)addr, queue->stride);

    return true;
}

📦 任务系统架构

任务信息结构

/**
 * @brief 任务开始函数指针定义
 */
typedef b8 (*pfn_job_start)(void* params, void* result_data);

/**
 * @brief 任务完成回调函数指针定义
 */
typedef void (*pfn_job_on_complete)(void* params);

/**
 * @brief 描述要运行的任务
 */
typedef struct job_info {
    /** @brief 任务类型,用于确定任务在哪个线程上执行 */
    job_type type;

    /** @brief 任务的优先级,高优先级任务先运行 */
    job_priority priority;

    /** @brief 任务开始时调用的函数指针(必需) */
    pfn_job_start entry_point;

    /** @brief 任务成功完成时调用的函数指针(可选) */
    pfn_job_on_complete on_success;

    /** @brief 任务失败时调用的函数指针(可选) */
    pfn_job_on_complete on_fail;

    /** @brief 执行时传递给入口点的数据 */
    void* param_data;

    /** @brief 传递给任务的数据大小 */
    u32 param_data_size;

    /** @brief 执行时传递给成功/失败函数的数据 */
    void* result_data;

    /** @brief 传递给成功/失败函数的数据大小 */
    u32 result_data_size;
} job_info;

任务类型

/**
 * @brief 描述任务类型
 */
typedef enum job_type {
    /**
     * @brief 没有特定线程要求的通用任务
     * 这意味着此任务在哪个工作线程上运行并不重要
     */
    JOB_TYPE_GENERAL = 0x02,

    /**
     * @brief 资源加载任务
     * 资源应始终在同一线程上加载,以避免潜在的磁盘抖动
     */
    JOB_TYPE_RESOURCE_LOAD = 0x04,

    /**
     * @brief 使用 GPU 资源的任务应绑定到使用此任务类型的线程
     * 多线程渲染器将使用特定的工作线程
     * 对于单线程渲染器,这将在主线程上
     */
    JOB_TYPE_GPU_RESOURCE = 0x08,
} job_type;

类型掩码

// 工作线程可以处理多种类型的任务
u32 type_masks[4] = {
    JOB_TYPE_GENERAL,                                    // 线程 0:只处理通用任务
    JOB_TYPE_RESOURCE_LOAD,                              // 线程 1:只处理资源加载
    JOB_TYPE_GENERAL | JOB_TYPE_GPU_RESOURCE,            // 线程 2:通用 + GPU
    JOB_TYPE_GENERAL | JOB_TYPE_RESOURCE_LOAD            // 线程 3:通用 + 资源
};

// 检查线程是否可以处理任务
b8 can_handle = (thread->type_mask & job->type) != 0;

任务优先级

/**
 * @brief 确定任务使用哪个任务队列
 * 高优先级队列总是先被清空,然后是正常优先级,最后是低优先级
 */
typedef enum job_priority {
    /** @brief 最低优先级任务,用于可以等待的事情,如日志刷新 */
    JOB_PRIORITY_LOW,

    /** @brief 正常优先级任务,用于中等优先级任务,如加载资源 */
    JOB_PRIORITY_NORMAL,

    /** @brief 最高优先级任务,应谨慎使用,仅用于时间关键操作 */
    JOB_PRIORITY_HIGH
} job_priority;

优先级队列

HIGH

NORMAL

LOW

任务提交

优先级?

高优先级队列

正常优先级队列

低优先级队列

工作线程

先处理高优先级

再处理正常优先级

最后处理低优先级


🔧 任务系统实现

系统状态

#define MAX_JOB_RESULTS 512

typedef struct job_thread {
    u8 index;
    kthread thread;
    job_info info;

    /** @brief 保护此线程信息访问的互斥锁 */
    kmutex info_mutex;

    /** @brief 此线程可以处理的任务类型 */
    u32 type_mask;
} job_thread;

typedef struct job_result_entry {
    u16 id;
    pfn_job_on_complete callback;
    u32 param_size;
    void* params;
} job_result_entry;

typedef struct job_system_state {
    b8 running;
    u8 thread_count;
    job_thread job_threads[32];

    /** @brief 三个优先级队列 */
    ring_queue low_priority_queue;
    ring_queue normal_priority_queue;
    ring_queue high_priority_queue;

    /** @brief 每个队列的互斥锁 */
    kmutex low_pri_queue_mutex;
    kmutex normal_pri_queue_mutex;
    kmutex high_pri_queue_mutex;

    /** @brief 待处理的结果数组 */
    job_result_entry pending_results[MAX_JOB_RESULTS];
    kmutex result_mutex;
} job_system_state;

工作线程

/**
 * @brief 工作线程的主循环
 */
u32 job_thread_run(void* params) {
    u32 index = *(u32*)params;
    job_thread* thread = &state_ptr->job_threads[index];
    u64 thread_id = thread->thread.thread_id;

    KTRACE("Starting job thread #%i (id=%#x, type=%#x).",
           thread->index, thread_id, thread->type_mask);

    // 创建互斥锁
    if (!kmutex_create(&thread->info_mutex)) {
        KERROR("Failed to create job thread mutex! Aborting thread.");
        return 0;
    }

    // 永久运行,等待任务
    while (true) {
        if (!state_ptr || !state_ptr->running || !thread) {
            break;
        }

        // 🔒 锁定并获取任务信息副本
        if (!kmutex_lock(&thread->info_mutex)) {
            KERROR("Failed to obtain lock on job thread mutex!");
        }
        job_info info = thread->info;
        if (!kmutex_unlock(&thread->info_mutex)) {
            KERROR("Failed to release lock on job thread mutex!");
        }

        // 如果有任务,执行它
        if (info.entry_point) {
            b8 result = info.entry_point(info.param_data, info.result_data);

            // 存储结果以便稍后在主线程上执行
            if (result && info.on_success) {
                store_result(info.on_success, info.result_data_size, info.result_data);
            } else if (!result && info.on_fail) {
                store_result(info.on_fail, info.result_data_size, info.result_data);
            }

            // 清理参数和结果数据
            if (info.param_data) {
                kfree(info.param_data, info.param_data_size, MEMORY_TAG_JOB);
            }
            if (info.result_data) {
                kfree(info.result_data, info.result_data_size, MEMORY_TAG_JOB);
            }

            // 🔒 锁定并重置线程的任务对象
            if (!kmutex_lock(&thread->info_mutex)) {
                KERROR("Failed to obtain lock on job thread mutex!");
            }
            kzero_memory(&thread->info, sizeof(job_info));
            if (!kmutex_unlock(&thread->info_mutex)) {
                KERROR("Failed to release lock on job thread mutex!");
            }
        }

        if (state_ptr->running) {
            // TODO: 使用条件变量代替轮询
            kthread_sleep(&thread->thread, 10);
        } else {
            break;
        }
    }

    // 销毁互斥锁
    kmutex_destroy(&thread->info_mutex);
    return 1;
}

任务调度

/**
 * @brief 尝试从队列中获取任务并分配给空闲线程
 */
b8 process_queue(ring_queue* queue, kmutex* queue_mutex) {
    job_info job;

    // 🔒 从队列获取任务
    if (!kmutex_lock(queue_mutex)) {
        KERROR("Failed to lock queue mutex!");
        return false;
    }

    b8 has_job = ring_queue_dequeue(queue, &job);

    if (!kmutex_unlock(queue_mutex)) {
        KERROR("Failed to unlock queue mutex!");
    }

    if (!has_job) {
        return false;
    }

    // 找到可以处理此任务的空闲线程
    for (u8 i = 0; i < state_ptr->thread_count; ++i) {
        job_thread* thread = &state_ptr->job_threads[i];

        // 检查线程是否可以处理此类型的任务
        if ((thread->type_mask & job.type) == 0) {
            continue;  // 类型不匹配
        }

        // 🔒 检查线程是否空闲
        if (!kmutex_lock(&thread->info_mutex)) {
            KERROR("Failed to lock thread mutex!");
            continue;
        }

        b8 is_idle = thread->info.entry_point == 0;

        if (is_idle) {
            // 分配任务给此线程
            thread->info = job;
        }

        if (!kmutex_unlock(&thread->info_mutex)) {
            KERROR("Failed to unlock thread mutex!");
        }

        if (is_idle) {
            return true;  // 成功分配
        }
    }

    // 没有空闲线程,将任务放回队列
    if (!kmutex_lock(queue_mutex)) {
        KERROR("Failed to lock queue mutex!");
        return false;
    }

    if (!ring_queue_enqueue(queue, &job)) {
        KERROR("Failed to re-enqueue job!");
    }

    if (!kmutex_unlock(queue_mutex)) {
        KERROR("Failed to unlock queue mutex!");
    }

    return false;
}

💼 任务 API

创建任务

/**
 * @brief 创建默认类型(通用)和优先级(正常)的新任务
 */
KAPI job_info job_create(pfn_job_start entry_point,
                         pfn_job_on_complete on_success,
                         pfn_job_on_complete on_fail,
                         void* param_data,
                         u32 param_data_size,
                         u32 result_data_size) {
    job_info job;
    job.entry_point = entry_point;
    job.on_success = on_success;
    job.on_fail = on_fail;
    job.param_data_size = param_data_size;
    job.result_data_size = result_data_size;
    job.type = JOB_TYPE_GENERAL;
    job.priority = JOB_PRIORITY_NORMAL;

    // 复制参数数据
    if (param_data_size > 0) {
        job.param_data = kallocate(param_data_size, MEMORY_TAG_JOB);
        kcopy_memory(job.param_data, param_data, param_data_size);
    } else {
        job.param_data = 0;
    }

    // 分配结果数据
    if (result_data_size > 0) {
        job.result_data = kallocate(result_data_size, MEMORY_TAG_JOB);
    } else {
        job.result_data = 0;
    }

    return job;
}

/**
 * @brief 创建指定类型的新任务
 */
KAPI job_info job_create_type(pfn_job_start entry_point,
                              pfn_job_on_complete on_success,
                              pfn_job_on_complete on_fail,
                              void* param_data,
                              u32 param_data_size,
                              u32 result_data_size,
                              job_type type) {
    job_info job = job_create(entry_point, on_success, on_fail,
                             param_data, param_data_size, result_data_size);
    job.type = type;
    return job;
}

/**
 * @brief 创建指定优先级的新任务
 */
KAPI job_info job_create_priority(pfn_job_start entry_point,
                                  pfn_job_on_complete on_success,
                                  pfn_job_on_complete on_fail,
                                  void* param_data,
                                  u32 param_data_size,
                                  u32 result_data_size,
                                  job_type type,
                                  job_priority priority) {
    job_info job = job_create_type(entry_point, on_success, on_fail,
                                   param_data, param_data_size,
                                   result_data_size, type);
    job.priority = priority;
    return job;
}

提交任务

/**
 * @brief 提交要排队执行的任务
 */
KAPI void job_system_submit(job_info info) {
    // 根据优先级选择队列
    ring_queue* queue = NULL;
    kmutex* queue_mutex = NULL;

    switch (info.priority) {
        case JOB_PRIORITY_HIGH:
            queue = &state_ptr->high_priority_queue;
            queue_mutex = &state_ptr->high_pri_queue_mutex;
            break;
        case JOB_PRIORITY_NORMAL:
            queue = &state_ptr->normal_priority_queue;
            queue_mutex = &state_ptr->normal_pri_queue_mutex;
            break;
        case JOB_PRIORITY_LOW:
            queue = &state_ptr->low_priority_queue;
            queue_mutex = &state_ptr->low_pri_queue_mutex;
            break;
    }

    // 🔒 将任务加入队列
    if (!kmutex_lock(queue_mutex)) {
        KERROR("Failed to lock queue mutex!");
        return;
    }

    if (!ring_queue_enqueue(queue, &info)) {
        KERROR("Failed to enqueue job! Queue full?");
    }

    if (!kmutex_unlock(queue_mutex)) {
        KERROR("Failed to unlock queue mutex!");
    }
}

系统更新

/**
 * @brief 更新任务系统,应在每个更新周期调用一次
 * 处理任务调度和结果回调
 */
void job_system_update() {
    // 按优先级顺序处理队列
    // 高优先级 → 正常优先级 → 低优先级

    // 1. 处理高优先级队列(直到为空)
    while (process_queue(&state_ptr->high_priority_queue,
                        &state_ptr->high_pri_queue_mutex)) {
        // 继续处理直到队列为空
    }

    // 2. 处理正常优先级队列
    while (process_queue(&state_ptr->normal_priority_queue,
                        &state_ptr->normal_pri_queue_mutex)) {
        // 继续处理
    }

    // 3. 处理低优先级队列
    while (process_queue(&state_ptr->low_priority_queue,
                        &state_ptr->low_pri_queue_mutex)) {
        // 继续处理
    }

    // 4. 处理完成的任务结果(在主线程上)
    if (!kmutex_lock(&state_ptr->result_mutex)) {
        KERROR("Failed to lock result mutex!");
        return;
    }

    for (u16 i = 0; i < MAX_JOB_RESULTS; ++i) {
        job_result_entry* entry = &state_ptr->pending_results[i];

        if (entry->id != INVALID_ID_U16) {
            // 调用回调
            if (entry->callback) {
                entry->callback(entry->params);
            }

            // 清理
            if (entry->params) {
                kfree(entry->params, entry->param_size, MEMORY_TAG_JOB);
            }

            // 标记为可用
            entry->id = INVALID_ID_U16;
        }
    }

    if (!kmutex_unlock(&state_ptr->result_mutex)) {
        KERROR("Failed to unlock result mutex!");
    }
}

🎮 游戏引擎应用

示例 1:异步资源加载

typedef struct texture_load_params {
    const char* path;
    texture* out_texture;
} texture_load_params;

// 任务入口点(在工作线程上执行)
b8 load_texture_job(void* params, void* result_data) {
    texture_load_params* load_params = (texture_load_params*)params;

    KINFO("Loading texture: %s", load_params->path);

    // 执行 I/O(不阻塞主线程)
    image_resource_data* image = load_image_resource(load_params->path);

    if (!image) {
        KERROR("Failed to load texture: %s", load_params->path);
        return false;
    }

    // 处理图像数据
    load_params->out_texture = create_texture_from_image(image);

    return load_params->out_texture != 0;
}

// 成功回调(在主线程上执行)
void on_texture_loaded(void* params) {
    texture_load_params* load_params = (texture_load_params*)params;
    KINFO("Texture loaded successfully: %s", load_params->path);

    // 在主线程上创建 GPU 资源
    upload_texture_to_gpu(load_params->out_texture);
}

// 失败回调
void on_texture_load_failed(void* params) {
    texture_load_params* load_params = (texture_load_params*)params;
    KERROR("Failed to load texture: %s", load_params->path);

    // 使用默认纹理
    load_params->out_texture = get_default_texture();
}

// 使用
void request_texture_load(const char* path, texture* out_texture) {
    texture_load_params params = {
        .path = path,
        .out_texture = out_texture
    };

    job_info job = job_create_type(
        load_texture_job,
        on_texture_loaded,
        on_texture_load_failed,
        &params,
        sizeof(params),
        0,
        JOB_TYPE_RESOURCE_LOAD  // 确保在资源加载线程上执行
    );

    job_system_submit(job);
}

示例 2:并行粒子更新

typedef struct particle_batch_params {
    particle* particles;
    u32 start_index;
    u32 count;
    f32 delta_time;
} particle_batch_params;

b8 update_particle_batch(void* params, void* result_data) {
    particle_batch_params* batch = (particle_batch_params*)params;

    for (u32 i = 0; i < batch->count; ++i) {
        particle* p = &batch->particles[batch->start_index + i];

        // 更新粒子
        p->position.x += p->velocity.x * batch->delta_time;
        p->position.y += p->velocity.y * batch->delta_time;
        p->position.z += p->velocity.z * batch->delta_time;

        p->lifetime -= batch->delta_time;
    }

    return true;
}

void update_particles_parallel(particle* particles, u32 count, f32 delta_time) {
    const u32 BATCH_SIZE = 1000;
    u32 batch_count = (count + BATCH_SIZE - 1) / BATCH_SIZE;

    for (u32 i = 0; i < batch_count; ++i) {
        particle_batch_params params = {
            .particles = particles,
            .start_index = i * BATCH_SIZE,
            .count = min(BATCH_SIZE, count - i * BATCH_SIZE),
            .delta_time = delta_time
        };

        job_info job = job_create(
            update_particle_batch,
            NULL,  // 无成功回调
            NULL,  // 无失败回调
            &params,
            sizeof(params),
            0
        );

        job_system_submit(job);
    }
}

示例 3:AI 更新

typedef struct ai_update_params {
    entity* entities;
    u32 entity_index;
} ai_update_params;

b8 update_ai_job(void* params, void* result_data) {
    ai_update_params* ai_params = (ai_update_params*)params;
    entity* e = &ai_params->entities[ai_params->entity_index];

    // 执行 AI 逻辑
    ai_think(e);
    ai_move(e);
    ai_attack(e);

    return true;
}

void update_all_ai(entity* entities, u32 count) {
    for (u32 i = 0; i < count; ++i) {
        ai_update_params params = {
            .entities = entities,
            .entity_index = i
        };

        job_info job = job_create(
            update_ai_job,
            NULL,
            NULL,
            &params,
            sizeof(params),
            0
        );

        job_system_submit(job);
    }
}

⚡ 性能优化

1. 减少锁争用

// ❌ 每个任务都锁定
for (u32 i = 0; i < 1000; ++i) {
    job_system_submit(job);  // 每次都锁定队列
}

// ✅ 批量提交
job_info jobs[1000];
for (u32 i = 0; i < 1000; ++i) {
    jobs[i] = job_create(...);
}
job_system_submit_batch(jobs, 1000);  // 只锁定一次

2. 使用无锁队列

// 替换互斥锁保护的队列为无锁队列
typedef struct lockfree_ring_queue {
    atomic_uint head;
    atomic_uint tail;
    // ...
} lockfree_ring_queue;

b8 lockfree_enqueue(lockfree_ring_queue* queue, void* value) {
    u32 current_tail, next_tail;
    do {
        current_tail = atomic_load(&queue->tail);
        next_tail = (current_tail + 1) % queue->capacity;

        if (next_tail == atomic_load(&queue->head)) {
            return false;  // 队列已满
        }
    } while (!atomic_compare_exchange_weak(&queue->tail, &current_tail, next_tail));

    // 写入数据
    // ...

    return true;
}

3. 工作窃取(Work Stealing)

// 每个线程有自己的本地队列
typedef struct job_thread {
    ring_queue local_queue;  // 本地队列
    // ...
} job_thread;

u32 job_thread_run(void* params) {
    job_thread* thread = (job_thread*)params;

    while (running) {
        job_info job;

        // 1. 先检查本地队列
        if (ring_queue_dequeue(&thread->local_queue, &job)) {
            execute_job(&job);
        }
        // 2. 如果本地队列为空,从全局队列获取
        else if (dequeue_from_global(&job)) {
            execute_job(&job);
        }
        // 3. 如果全局队列也为空,从其他线程"窃取"
        else if (steal_from_other_thread(&job)) {
            execute_job(&job);
        }
        else {
            kthread_sleep(&thread->thread, 1);
        }
    }

    return 0;
}

4. 条件变量代替轮询

typedef struct job_system_state {
    kcondition work_available;  // 条件变量
    // ...
} job_system_state;

// 提交任务时通知工作线程
void job_system_submit(job_info info) {
    // 加入队列...

    // 通知一个等待的线程
    kcondition_signal(&state_ptr->work_available);
}

// 工作线程等待
u32 job_thread_run(void* params) {
    while (running) {
        job_info job;

        if (!get_job_from_queue(&job)) {
            // 没有任务,等待通知
            kmutex_lock(&queue_mutex);
            kcondition_wait(&state_ptr->work_available, &queue_mutex);
            kmutex_unlock(&queue_mutex);
            continue;
        }

        execute_job(&job);
    }

    return 0;
}

🚀 实践练习

练习 1:实现任务依赖

支持任务之间的依赖关系:

typedef struct job_info {
    // ... 现有字段 ...

    u32 dependency_count;
    job_handle* dependencies;  // 依赖的任务句柄
} job_info;

// TODO: 实现依赖检查
b8 dependencies_satisfied(job_info* job) {
    for (u32 i = 0; i < job->dependency_count; ++i) {
        if (!job_is_complete(job->dependencies[i])) {
            return false;
        }
    }
    return true;
}

// 使用示例
job_handle load_job = job_system_submit(load_texture_job);
job_handle process_job = job_system_submit_with_dependency(
    process_texture_job,
    &load_job,
    1
);

练习 2:实现任务优先级继承

高优先级任务依赖低优先级任务时,提升低优先级任务:

void promote_dependencies(job_info* high_priority_job) {
    // TODO: 遍历所有依赖
    for (u32 i = 0; i < high_priority_job->dependency_count; ++i) {
        job_info* dep = get_job(high_priority_job->dependencies[i]);

        // 如果依赖任务优先级更低,提升它
        if (dep->priority < high_priority_job->priority) {
            dep->priority = high_priority_job->priority;

            // 递归提升依赖的依赖
            promote_dependencies(dep);
        }
    }
}

练习 3:实现任务取消

允许取消尚未执行的任务:

typedef struct job_handle {
    u32 id;
    u32 generation;
    b8* cancelled_flag;  // 指向取消标志
} job_handle;

b8 job_cancel(job_handle handle) {
    // TODO:
    // 1. 验证句柄有效性
    // 2. 如果任务在队列中,移除它
    // 3. 如果任务正在执行,设置取消标志
    // 4. 任务内部检查标志并提前退出

    return false;
}

// 在任务中检查取消
b8 long_running_job(void* params, void* result) {
    job_handle* handle = (job_handle*)params;

    for (u32 i = 0; i < 1000000; ++i) {
        // 定期检查取消标志
        if (i % 1000 == 0 && *handle->cancelled_flag) {
            KINFO("Job cancelled at iteration %u", i);
            return false;
        }

        // 执行工作...
    }

    return true;
}

❓ 常见问题

Q1: 任务系统 vs 线程池,有什么区别?

A: 任务系统是线程池的高级封装:

特性线程池任务系统
抽象层次低(管理线程)高(管理任务)
负载均衡手动自动
优先级需要手动实现内置支持
类型隔离需要手动实现内置支持
结果处理需要手动实现自动回调

线程池

// 低级别:直接管理线程
thread_pool_execute(pool, worker_function, params);

任务系统

// 高级别:提交任务,系统处理细节
job_info job = job_create(task_func, on_success, on_fail, params, ...);
job_system_submit(job);
Q2: 如何确定任务的粒度?

A: 任务粒度需要平衡:

太粗(每个任务太大)

// ❌ 一个任务更新所有敌人(10000 个)
job_info job = job_create(update_all_enemies, ...);
// 问题:无法并行,其他核心空闲

太细(每个任务太小)

// ❌ 每个敌人一个任务
for (u32 i = 0; i < 10000; ++i) {
    job_info job = job_create(update_one_enemy, &enemies[i], ...);
    job_system_submit(job);
}
// 问题:任务创建/调度开销大于执行时间

合适的粒度

// ✅ 批量处理
const u32 BATCH_SIZE = 100;  // 经验值:100-1000
for (u32 i = 0; i < enemy_count; i += BATCH_SIZE) {
    job_info job = job_create(update_enemy_batch, &batch_params, ...);
    job_system_submit(job);
}

经验法则

  • 任务执行时间应 >> 任务调度开销
  • 通常:0.1-1 毫秒的执行时间是合适的
  • 使用性能分析工具测量和调优
Q3: 如何避免任务之间的数据竞争?

A: 几种模式:

1. 分区数据(推荐)

// 每个任务处理不重叠的数据
for (u32 i = 0; i < 4; ++i) {
    particle_batch_params params = {
        .particles = particles,
        .start_index = i * 250,  // 不重叠
        .count = 250
    };
    job_system_submit(job_create(update_particles, &params, ...));
}

2. 只读共享数据

// 所有任务读取相同的配置数据
const config* shared_config = get_global_config();

for (u32 i = 0; i < task_count; ++i) {
    // shared_config 是 const,可以安全共享
    job_system_submit(job_create(task_func, shared_config, ...));
}

3. 原子操作

atomic_uint global_counter = 0;

b8 counting_job(void* params, void* result) {
    // 原子递增,无需锁
    atomic_fetch_add(&global_counter, 1);
    return true;
}

4. 每线程数据(TLS)

_Thread_local allocator thread_allocator;

b8 allocation_job(void* params, void* result) {
    // 每个线程使用自己的分配器
    void* memory = allocate(&thread_allocator, size);
    return true;
}

5. 最后合并

typedef struct reduce_result {
    u32 thread_id;
    u32 local_sum;
} reduce_result;

// 每个任务计算部分和
b8 sum_job(void* params, void* result) {
    reduce_result* res = (reduce_result*)result;
    res->thread_id = get_thread_id();
    res->local_sum = 0;

    // 计算局部和(无竞争)
    for (u32 i = start; i < end; ++i) {
        res->local_sum += array[i];
    }

    return true;
}

// 在主线程合并结果
void on_all_sums_complete(void* params) {
    u32 total_sum = 0;
    for (u32 i = 0; i < task_count; ++i) {
        total_sum += results[i].local_sum;
    }
    KINFO("Total sum: %u", total_sum);
}
Q4: 任务系统如何处理异常/错误?

A: 使用返回值和失败回调:

b8 risky_job(void* params, void* result) {
    // 尝试执行操作
    if (!perform_operation()) {
        KERROR("Operation failed in job!");
        return false;  // 表示失败
    }

    // 成功
    return true;
}

void on_job_success(void* params) {
    KINFO("Job completed successfully");
    // 继续处理...
}

void on_job_failure(void* params) {
    KERROR("Job failed!");
    // 错误处理:
    // - 重试
    // - 使用默认值
    // - 通知用户
    // - 记录日志
}

// 使用
job_info job = job_create(
    risky_job,
    on_job_success,
    on_job_failure,  // 重要:提供失败处理
    params,
    sizeof(params),
    0
);
job_system_submit(job);

错误传播

typedef struct job_error {
    b8 has_error;
    char message[256];
    i32 error_code;
} job_error;

b8 job_with_detailed_error(void* params, void* result) {
    job_error* error = (job_error*)result;

    if (!operation1()) {
        error->has_error = true;
        error->error_code = ERROR_OPERATION1_FAILED;
        string_copy(error->message, "Operation 1 failed", 256);
        return false;
    }

    return true;
}

void on_error(void* params) {
    job_error* error = (job_error*)params;
    KERROR("Job failed: %s (code: %d)", error->message, error->error_code);
}
Q5: 如何调试任务系统中的问题?

A: 调试技巧:

1. 任务跟踪

typedef struct job_info {
    // ... 现有字段 ...

    const char* debug_name;  // 调试名称
    u64 submit_time;         // 提交时间
    u64 start_time;          // 开始执行时间
    u64 end_time;            // 结束时间
} job_info;

b8 job_entry_point(void* params, void* result) {
    job_info* job = get_current_job();

    KDEBUG("[Job:%s] Started on thread %#x", job->debug_name, get_thread_id());

    // 执行工作...

    u64 duration = job->end_time - job->start_time;
    KDEBUG("[Job:%s] Completed in %llu ms", job->debug_name, duration);

    return true;
}

2. 性能分析

typedef struct job_stats {
    u64 total_jobs_submitted;
    u64 total_jobs_completed;
    u64 total_execution_time;
    u64 queue_wait_time;
} job_stats;

void print_job_stats() {
    KINFO("=== Job System Stats ===");
    KINFO("Total submitted: %llu", stats.total_jobs_submitted);
    KINFO("Total completed: %llu", stats.total_jobs_completed);
    KINFO("Avg exec time: %llu ms", stats.total_execution_time / stats.total_jobs_completed);
    KINFO("Avg wait time: %llu ms", stats.queue_wait_time / stats.total_jobs_completed);
}

3. 可视化工具

// 导出事件用于 Chrome Tracing
void trace_job_submit(job_info* job) {
    fprintf(trace_file, "{\"name\":\"%s\",\"ph\":\"B\",\"ts\":%llu,\"pid\":1,\"tid\":%d},\n",
            job->debug_name, get_timestamp(), get_thread_id());
}

void trace_job_complete(job_info* job) {
    fprintf(trace_file, "{\"name\":\"%s\",\"ph\":\"E\",\"ts\":%llu,\"pid\":1,\"tid\":%d},\n",
            job->debug_name, get_timestamp(), get_thread_id());
}

// 在 Chrome 中打开 chrome://tracing 查看

4. 断言和验证

b8 job_entry_point(void* params, void* result) {
    // 验证线程类型
    KASSERT_DEBUG((get_current_thread_type_mask() & JOB_TYPE_RESOURCE_LOAD) != 0);

    // 验证参数
    KASSERT_DEBUG(params != NULL);

    // 执行...

    return true;
}

📚 总结

任务系统是现代游戏引擎并发架构的核心,它提供了比固定线程更灵活、更高效的方式来利用多核 CPU。本教程涵盖了:

✅ 关键要点

概念要点
环形队列高效的 FIFO 数据结构,支持并发访问
任务类型通用、资源加载、GPU 资源
任务优先级高、正常、低三级队列
工作线程自动调度和执行任务
回调机制成功/失败回调在主线程执行

🔑 核心 API

// 创建任务
job_info job = job_create(entry_point, on_success, on_fail, params, ...);

// 提交任务
job_system_submit(job);

// 更新系统(主循环中)
job_system_update();

📈 优势

  • 自动负载均衡:任务动态分配到空闲线程
  • 🎯 优先级管理:重要任务先执行
  • 🔧 类型隔离:避免资源争用(如磁盘抖动)
  • 💡 简化编程:只需提交任务,无需管理线程

🚀 下一步

在下一篇教程中,我们将学习:

  • 教程 50:更高级的引擎功能

任务系统 是充分利用多核 CPU 的关键技术,它使游戏引擎能够高效地并行处理复杂的游戏逻辑!


📖 关注公众号

在这里插入图片描述

关注我,领取章节视频教程

📅 最后更新:2025-12-01
✍️ 作者:上手实验室
📧 联系提交 Issue

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值