4、线程管理基础:创建线程、线程入口函数、线程参数传递、线程删除与动态创建

在嵌入式实时操作系统(RTOS)中,线程(Thread)是任务调度的最小执行单元。Azure RTOS ThreadX 以其卓越的实时性和紧凑的内核设计,为开发者提供了高效且灵活的线程管理机制。本章将深入剖析 ThreadX 线程管理的四大核心操作:创建线程、编写线程入口函数、传递线程参数以及删除线程。我们将从内核数据结构出发,结合代码实例与工程实践,帮助您建立对 ThreadX 线程生命周期的完整认知,为后续的信号量、队列等同步机制打下坚实基础。

ThreadX 线程生命周期与核心操作 1. 创建线程 tx_thread_create() 分配TCB与栈空间 2. 就绪/运行 调度器按优先级执行 入口函数被调用 3. 参数传递 thread_input 指针 传递任意类型数据 4. 删除线程 tx_thread_delete() 释放TCB与栈内存 动态创建 运行时按需创建 配合内存池管理 调度 数据流 终止 复用 核心:TCB(线程控制块)管理 + 栈空间分配 + 调度器介入

上图清晰地展示了 ThreadX 线程从创建到删除的完整生命周期。线程并非孤立存在,其创建、运行、参数传递与销毁均围绕内核中的线程控制块(TCB)展开。接下来,我们将逐一拆解每个环节的底层原理与编码实践。

4.1 核心原理:线程控制块(TCB)与栈空间

在 ThreadX 中,每个线程都由两部分核心资源构成:线程控制块(TCB)和线程栈。TCB 是一个结构体,由内核维护,包含了线程的状态、优先级、入口函数指针、栈指针、时间片、事件标志等关键信息。线程栈则是一块连续的内存区域,用于保存线程的上下文(CPU 寄存器值)、局部变量和函数调用信息。

创建线程的本质,就是向内核注册一个 TCB,并为其分配一块栈空间。ThreadX 的调度器通过遍历就绪线程列表,根据优先级和时间片算法,选择下一个要运行的线程,并将其上下文从栈中恢复。因此,TCB 和栈的初始化质量直接决定了系统的稳定性。

关键数据结构:TX_THREAD 结构体是 TCB 的具体实现。在创建线程时,您需要提供一个 TX_THREAD 类型的变量(通常定义为全局变量或静态变量),内核会填充其内部字段。栈空间则通常由一个无符号字符数组或字节数组提供。

4.2 操作流程:创建线程与入口函数编写

创建线程的 API 为 tx_thread_create,其原型如下:

UINT tx_thread_create(
    TX_THREAD *thread_ptr,        // 指向 TCB 的指针
    CHAR *name_ptr,               // 线程名称(调试用)
    VOID (*entry_function)(ULONG), // 入口函数指针
    ULONG entry_input,            // 传递给入口函数的参数
    VOID *stack_start,            // 栈起始地址
    ULONG stack_size,             // 栈大小(字节)
    UINT priority,                // 优先级(0-31,0最高)
    UINT preempt_threshold,       // 抢占阈值
    ULONG time_slice,             // 时间片(0表示不轮转)
    UINT auto_start,              // 是否自动启动(TX_AUTO_START / TX_DONT_START)
    UINT *created_thread_count    // 可选:指向线程计数器的指针
);

入口函数必须遵循固定格式:VOID my_thread_entry(ULONG initial_input)。该函数通常包含一个无限循环,用于执行周期性任务或事件等待。线程函数不应返回,否则会导致系统错误。

标准创建流程:

  1. 定义 TX_THREAD 结构体变量和栈数组。
  2. 调用 tx_thread_create,传入参数。
  3. 检查返回值,若为 TX_SUCCESS 则创建成功。
  4. 若 auto_start 设为 TX_DONT_START,后续需调用 tx_thread_resume 启动。
TX_THREAD my_thread;
UCHAR my_thread_stack[4096];

VOID my_thread_entry(ULONG input) {
    while(1) {
        // 任务逻辑
        tx_thread_sleep(10); // 延时
    }
}

UINT create_my_thread() {
    UINT status = tx_thread_create(
        &my_thread, "My Thread", my_thread_entry, 0,
        my_thread_stack, sizeof(my_thread_stack),
        15, 15, 0, TX_AUTO_START, TX_NULL);
    return status;
}

4.3 案例拆解:线程参数传递的多种方式

线程入口函数仅接收一个 ULONG 类型的参数。这个参数是线程与外部世界交互的初始通道。虽然只有一个参数,但通过指针转换,可以传递任意复杂的数据结构。

传递方式适用场景示例
直接传值传递简单标志、索引、计数器tx_thread_create(..., 5, ...) 表示线程 ID 为 5
传递结构体指针传递配置参数、初始化数据将 APP_CFG* 强制转换为 ULONG
传递字符串指针传递设备名称、文件路径(ULONG)"UART1"
传递函数指针回调机制将回调函数地址作为参数传入

实战场景:假设我们需要创建两个 LED 闪烁线程,一个闪烁频率为 1Hz,另一个为 2Hz。我们可以定义一个结构体,包含 GPIO 端口号和延时时间,然后将结构体指针作为参数传入。

typedef struct {
    UINT gpio_port;
    UINT delay_ticks;
} LED_CFG;

LED_CFG led1_cfg = {1, 100}; // 100 ticks @ 10ms = 1s
LED_CFG led2_cfg = {2, 50};  // 50 ticks = 0.5s

VOID led_thread_entry(ULONG arg) {
    LED_CFG *cfg = (LED_CFG*)arg;
    while(1) {
        // 翻转 GPIO
        tx_thread_sleep(cfg->delay_ticks);
    }
}

// 创建两个线程
tx_thread_create(&led1_tcb, "LED1", led_thread_entry, (ULONG)&led1_cfg, ...);
tx_thread_create(&led2_tcb, "LED2", led_thread_entry, (ULONG)&led2_cfg, ...);
经验提醒:当传递结构体指针时,务必确保该结构体的生命周期覆盖线程的整个运行周期。如果结构体是局部变量,线程可能访问到已失效的内存,导致不可预料的错误。建议使用静态或全局变量。

4.4 线程删除与动态创建:资源回收与灵活部署

线程删除使用 tx_thread_delete API。该函数会终止线程的执行,并释放其 TCB 和栈空间(如果这些内存是由应用程序管理的)。但需要注意,线程不能删除自身,只能由其他线程或定时器上下文中删除。

删除流程:

  1. 确保目标线程已终止(即其入口函数已返回)或处于挂起状态。
  2. 调用 tx_thread_terminate 强制终止(如果还在运行)。
  3. 调用 tx_thread_delete 释放资源。

动态创建则是指在系统运行过程中,根据实际需求(如新连接接入、任务动态加载)创建线程。这通常与字节池(Byte Pool)配合使用,从池中分配 TCB 和栈内存,删除时再释放回池中,避免内存碎片。

TX_BYTE_POOL my_pool;
TX_THREAD *dynamic_thread_ptr;
UCHAR *stack_ptr;

// 初始化字节池
tx_byte_pool_create(&my_pool, "Dynamic Pool", pool_memory, POOL_SIZE);

// 动态创建线程
UINT create_dynamic_thread() {
    // 从池中分配 TCB 和栈
    tx_byte_allocate(&my_pool, (VOID**)&dynamic_thread_ptr, sizeof(TX_THREAD), TX_NO_WAIT);
    tx_byte_allocate(&my_pool, (VOID**)&stack_ptr, STACK_SIZE, TX_NO_WAIT);
    
    // 创建线程
    UINT status = tx_thread_create(dynamic_thread_ptr, "Dyn", dyn_entry, 0,
                                   stack_ptr, STACK_SIZE, 20, 20, 0, TX_AUTO_START, TX_NULL);
    return status;
}

// 删除动态线程
UINT delete_dynamic_thread() {
    tx_thread_terminate(dynamic_thread_ptr);
    tx_thread_delete(dynamic_thread_ptr);
    // 释放内存回池
    tx_byte_release(dynamic_thread_ptr);
    tx_byte_release(stack_ptr);
}

4.5 常见误区与总结复盘

在实际开发中,线程管理环节容易出现以下几类问题:

  • 栈溢出:栈大小分配不足,导致内存踩踏。建议使用 ThreadX 提供的 tx_thread_stack_error_notify 回调进行监控。
  • 优先级反转:未合理设置抢占阈值或使用优先级继承,导致高优先级任务被低优先级任务阻塞。
  • 删除未终止的线程:直接调用 tx_thread_delete 而线程仍在运行,会导致系统崩溃。必须先终止。
  • 参数指针失效:传递了指向局部变量的指针,线程启动后该变量已销毁。
  • 动态内存泄漏:删除线程后未释放 TCB 和栈内存,长期运行导致内存耗尽。
风险提示:在中断服务函数(ISR)中调用 tx_thread_create 或 tx_thread_delete 是不允许的。这些 API 只能在任务上下文中调用。若需在 ISR 中触发线程管理,应通过信号量或事件标志组通知一个高优先级线程来执行。

总结复盘:线程管理是 RTOS 应用的地基。创建线程时,要像“配置硬件外设”一样严谨:明确栈大小、优先级、时间片。参数传递时,要遵循“指针生命周期大于线程生命周期”的原则。动态创建与删除是嵌入式系统灵活性的体现,但必须以严谨的内存管理为前提。建议在项目初期就制定线程资源规划表,明确每个线程的 TCB、栈来源(静态或动态池),并建立统一的错误处理机制。

掌握本章内容后,您应能熟练地在 ThreadX 中创建、配置、传递参数并安全地销毁线程。下一阶段,我们将探讨线程之间的同步与通信机制,届时您将看到线程管理如何与信号量、互斥锁等内核对象协同工作,构建出健壮的实时应用系统。