🔗 快速导航
📋 目录
点击展开/折叠🎯 概述
任务系统(Job System) 是现代游戏引擎的核心并发架构,它比传统的固定线程模型更灵活、更高效。任务系统将工作分解为小的、独立的任务,然后动态分配到工作线程池中执行。
在本教程中,我们将:
- ✅ 理解任务系统的优势
- ✅ 实现无锁环形队列
- ✅ 设计任务类型和优先级系统
- ✅ 实现工作线程池和任务调度
- ✅ 处理任务结果回调
❓ 为什么需要任务系统?
传统的多线程模型使用固定线程,每个线程执行特定的工作:
任务系统的优势:
| 优势 | 描述 | 示例 |
|---|---|---|
| 负载均衡 | 工作自动分配到空闲线程 | 所有核心保持高利用率 |
| 细粒度并行 | 任务可以很小,易于并行 | 每个敌人 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;
优先级队列:
🔧 任务系统实现
系统状态
#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,
¶ms,
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, // 无失败回调
¶ms,
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,
¶ms,
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, ¤t_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 毫秒的执行时间是合适的
- 使用性能分析工具测量和调优
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, ¶ms, ...));
}
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



1837

被折叠的 条评论
为什么被折叠?



