STM32F1与STM32CubeIDE编程实例ThreadX的线程服务Thread Service
STM32F1与STM32CubeIDE编程实例-ThreadX的线程服务(Thread Service)
ThreadX的线程服务(Thread Service)
文章目录
在本文中,将详细介绍ThreadX的线程服务。线程服务涉及线程创建与销毁、线程信息查询、线程优先级更改、线程终止、线程唤醒等等。
1、线程控制块(Thread Control Block,TCB)介绍
ThreadX通过线程控制块来管理线程运行时的状态。它还用于在需要上下文切换时保留有关线程的信息。TCB在tx_thread.h文件,定义为:TX_THREAD数据结构。TX_THREAD部分代码如下:
1 | typedef struct TX\_THREAD\_STRUCT |
线程控制块可以位于内存中的任何位置,但最常见的是通过在任何函数范围之外定义控制块来使控制块成为全局结构。 将线程控制块定义在其他区域需要更加小心,就像所有动态分配的内存一样。 如果在 C 函数中分配了控制块,则与其关联的内存将在调用线程的堆栈上分配。 一般来说,避免对控制块使用本地存储,因为一旦函数返回,它的整个局部变量堆栈空间就会被释放——不管其他线程是否将它用于控制块。
线程控制块中还有许多其他有用的字段,包括堆栈指针、时间片值和优先级。 开发者可以检查 TCB 的成员,但严禁对其进行修改。 没有明确的值指示线程当前是否正在执行。 在给定时间只有一个线程执行,ThreadX 会跟踪其他地方当前正在执行的线程。 请注意,执行线程的 tx_state 值为 TX_READY。
2、 创建线程与删除
1 | UINT tx\_thread\_create(TX_THREAD \*thread_ptr, /\*线程控制块指针\*/ |
线程执行函数定义为:
1 | void thread\_entry(void\* params){ |
线程使用 TX_THREAD 数据类型声明,并使用 tx_thread_create 创建。 每个线程必须有自己的栈;需要确定堆栈大小以及为堆栈分配内存的方式。如下图所示,一个典型的线程堆栈:

为线程栈分配内存有几种方法,包括使用字节池、块池和数组; 或者简单地指定内存中的物理起始地址。 堆栈大小至关重要; 它必须足够大以适应最坏情况下的函数调用嵌套、局部变量分配和保存线程的最后执行上下文。 预定义的最小堆栈大小常量TX_MINIMUM_STACK 对于大多数应用程序来说可能太小了。 最好选择更大的堆栈而不是较小的堆栈。对于线程栈内存分配,还需要结合MCU的物理内存、扩展内存以及应用程序需要使用到的线程数量和其他服务数量所占用内存进行精细分配。
当堆栈区域太小时会发生什么? 在大多数情况下,运行时环境只是假设有足够的堆栈空间。 这会导致线程执行破坏与其堆栈区域相邻(通常是之前)的内存。 结果非常不可预测,但通常包括程序计数器中的不自然变化。 这通常被称为Jumping Into The Weeds。 当然,防止这个问题的唯一方法是确保所有线程堆栈都足够大。
Threadx支持运行时对线程的栈是否溢出进行检测。开启这一功能需要在编译配置文件中启用TX_ENABLE_STACK_CHECKING或TX_PORT_THREAD_STACK_ERROR_HANDLING宏。在运行时,当出现栈溢出时,将通过_tx_thread_stack_error_handler回调函数通知用户。如果该函数不指定,则由ThreadX内部处理函数_tx_thread_stack_error_handler进行处理。
栈溢出处理回调函数定义如下:
1 | VOID (\*_tx_thread_application_stack_error_handler)(TX_THREAD \*thread_ptr) |
有时线程完成执行任务后,需要释放线程所占用的资源,因此需要将删除线程,线程删除API原型如下:
1 | UINT tx\_thread\_delete(TX_THREAD \*thread_ptr); |
多线程的一个重要特性是可以从多个线程调用同一个 C 函数。 这一特征提供了相当多的多功能性,还有助于减少代码空间。 但是,它要求从多个线程调用的 C 函数是**可重入(Reentrant)**的。 可重入函数是可以在已经执行时安全调用的函数。例如,如果该函数正在由当前线程执行,然后被抢占线程再次调用,就会发生这种情况。为了实现可重入,一个函数 将调用者的返回地址存储在当前堆栈中(而不是将其存储在寄存器中),并且不依赖于它先前设置的全局或静态 C 变量。 大多数编译器确实将返回地址放在堆栈上。 因此,应用程序开发人员只需要担心全局变量和静态变量的使用。
不可重入函数的一个示例是标准 C 库中的字符串标记函数 strtok。 此函数通过将指针保存在静态变量中来记住后续调用中的前一个字符串指针。 如果这个函数是从多个线程调用的,它很可能会返回一个无效的指针。
例如,创建一个线程:
1 | // 线程控制块 |
3、 线程信息查询
1 | UINT tx\_thread\_info\_get(TX_THREAD \*thread_ptr, /\*线程控制块\*/ |
此函数用于查询指定线程的信息。还可以通过遍历线程列表来查询当前系统的线程信息,例如:
1 | void print\_threads\_info(TX_THREAD\* start_thread) { |
1 | TX_THREAD my_thread; |
如果变量status的值为TX_SUCCESS,则成功检索信息。
4、 线程状态控制
下面为线程状态控制相关函数:
- **UINT tx_thread_reset(TX_THREAD *thread_ptr)**:此函数让线程从线程创建期间指定的入口点准备再次运行。 应用程序必须在此调用完成后调用 tx_thread_resume 才能真正运行线程。
- **UINT tx_thread_resume(TX_THREAD *thread_ptr)**:此函数处理应用程序恢复线程服务。 但实际的线程恢复过程是在Threadx核心服务中进行的。
- **UINT tx_thread_suspend(TX_THREAD *thread_ptr)**:此函数处理应用程序挂起请求。 如果挂起需要实际处理,则该函数调用实际的挂起线程例程。
- **UINT tx_thread_terminate(TX_THREAD *thread_ptr);**:此函数处理应用程序线程终止请求。 一旦一个线程被终止,它就不能再次执行,除非它被删除并重新创建。
- **UINT tx_thread_sleep(ULONG timer_ticks)**:此函数处理应用程序线程睡眠请求。 如果睡眠请求是从非线程中调用的,则返回错误。
- **UINT tx_thread_wait_abort(TX_THREAD *thread_ptr)**:此函数中止指定线程所处的等待条件。不管线程正在等待什么对象并向指定线程返回一个 TX_WAIT_ABORTED 状态。
ThreadX中线程的各种状态转换如下:

- 当线程准备好执行时处于**就绪(Ready)状态,并放到线程就绪列表中。在线程就绪列表中,只有优先级最高的就绪线程才会被调度执行。 当线程被调度器调度后,线程的状态将被更改为正在执行(Executing)**状态。
- 如果更高优先级的线程已准备就绪,正在执行的线程会恢复为就绪状态。 然后执行新的高优先级就绪线程,并将其逻辑状态更改为正在执行。 每次发生线程抢占时,线程的状态会在就绪和正在执行之间转换。
- 在任意指定时刻,只有一个线程处于正在执行状态。 这是因为处于正在执行状态的线程拥有处理器的权限。
- 处于已挂起(Suspended)状态的线程不具备执行条件。导致线程处于已挂起状态的原因包括挂起时间、队列消息、信号灯、互斥锁、事件标志、内存和基本线程挂起。处于已挂起状态的线程,被放到挂起线程列表中。在排除导致挂起的原因后,线程将恢复为就绪状态,并转移到线程就绪列表中。
- 处于已完成(Completed)状态的线程是指已完成其处理任务并从其entry函数返回的线程。 entry函数在线程创建期间指定。 处于已完成状态的线程无法再次执行。
- 如果线程本身或由另外一个线程调用了
tx_thread_terminate服务,则线程处于“已终止”状态。 处于已终止状态的线程无法再次执行。
4.1 恢复线程执行
当使用TX_DONT_START选项创建线程时,它会处于挂起状态。 当线程因为调用 tx_thread_suspend 而被挂起时,它也被置于挂起状态。 此类线程可以恢复的唯一方法是当另一个线程调用tx_thread_resume服务并将它们从挂起状态中删除时。
例如:
1 | TX_THREAD my_thread; |
当参数status返回TX_SUCCESS时,服务调用成功。
4.2 线程休眠
在某些情况下,线程需要暂停特定的时间。 这是通过tx_thread_sleep服务实现的,它会导致调用线程挂起指定数量的计时器节拍。 以下是一个示例服务调用,说明了线程如何将自身挂起 100 个计时器滴答:
1 | status = tx\_thread\_sleep(100); |
如果变量status 的值为TX_SUCCESS,则当前运行的线程在规定的计时器滴答数内被挂起(或休眠)。
4.3 暂停线程执行
可以通过调用 tx_thread_suspend 服务来挂起指定的线程。 一个线程可以挂起自己,也可以挂起另一个线程,也可以被另一个线程挂起
线。 如果线程以这种方式挂起,则必须通过调用 tx_thread_resume 服务来恢复它。 这种类型的挂起称为无条件挂起。请注意,还有其他形式的条件挂起,例如,线程由于等待不可用的资源而被挂起,或者线程在特定时间段内处于休眠状态。
例如:
1 | status = tx\_thread\_suspend(&some_thread); |
如果变量 status的值为TX_SUCCESS,则指定线程无条件挂起。 如果指定的线程已经有条件地挂起,则内部保持无条件挂起,直到解除先前的挂起。 当先前的挂起解除时,则执行指定线程的无条件挂起。 如果指定的线程已经无条件挂起,则此服务调用无效。
4.4 终止线程执行
tx_thread_terminate服务终止指定的应用程序线程,无论该线程当前是否挂起。 线程可能会自行终止。 已终止的线程无法再次执行。 如果需要执行一个终止的线程,那么可以重置它或删除它,然后重新创建它。例如:
1 | status = tx\_thread\_suspend(&some_thread); |
如果变量 status 的值为 TX_SUCCESS,则指定的线程已终止。
4.5 中止线程挂起
在某些情况下,线程可能被迫等待某个资源的时间过长(甚至永久!)。** tx_thread_wait_abort**服务可帮助开发人员防止出现这种不需要的情况。 此服务中止睡眠或指定线程的任何与等待相关的挂起。 如果等待成功中止,则从线程正在等待的服务返回一个 TX_WAIT_ABORTED 值。 请注意,此服务不会释放由 tx_thread_suspend 服务进行的显式挂起。例如:
1 | status = tx\_thread\_wait\_abort(&some_thread); |
如果变量 status 的值为 TX_SUCCESS,则线程 some_thread 的睡眠或挂起条件已中止,并且挂起的线程可以使用返回值 TX_WAIT_ABORTED。 然后,先前挂起的线程可以自由地采取它认为合适的任何操作。
5、 线程优先级、抢占阈值、时间片更改
5.1 更改线程优先组
当一个线程被创建时,它必须在那个时候被分配一个优先级。 但是,可以使用此服务随时更改线程的优先级。
- **UINT tx_thread_priority_change(TX_THREAD *thread_ptr,UINT new_priority, UINT *old_priority)**:该函数改变指定线程的优先级。 如果调用线程当前正在执行并且优先级更改导致更高优先级的线程准备好执行,它还会返回旧优先级并处理抢占。
例如:
1 | TX_THREAD my_thread; |
示例代码将线程的优先级更改为0,即最高优先线。如果status的值为TX_SUCCESS,则表示更改成功。
5.2 更改线程抢占阈值
线程的抢占阈值可以在创建时或运行时建立。函数tx_thread_preemption_change 更改现有线程的抢占阈值。 Preemption-Threshold 防止线程被优先级等于或小于 Preemption-Threshold 值的其他线程抢占。
- **UINT tx_thread_preemption_change(TX_THREAD *thread_ptr,UINT new_threshold, UINT *old_threshold)**:此函数处理抢占阈值更改请求。 先前的抢占返回给调用者。 如果新请求允许执行更高优先级的线程,则在此函数内部发生抢占。
例如:
1 | TX_THREAD my_thread; |
示例中,线程的抢占阈值更改为零 (0)。 这是可能的最高优先级,因此这意味着没有其他线程可以抢占该线程。 但是,这并不能阻止中断抢占该线程。 如果 my_thread 在调用此服务之前使用时间片,则该功能将被禁用。
5.3 更改线程时间片
线程的可选时间片可以在创建线程时指定,并且可以在执行期间随时更改。 该服务允许一个线程改变它自己的时间片或另一个线程的时间片。
- **UINT tx_thread_time_slice_change(TX_THREAD *thread_ptr,ULONG new_time_slice, ULONG *old_time_slice)**:此函数处理线程时间片更改请求。 前一个时间片返回给调用者。 如果新请求是针对正在执行的线程发出的,它也会被放入实际的时间片倒计时变量中。
例如:
1 | TX_THREAD my_thread; |
示例代码将线程的时间片值更改为20。
为一个线程选择一个时间片意味着在相同或更高优先级的其他线程有机会执行之前,它不会执行超过指定数量的计时器计时。 请注意,如果已指定抢占阈值,则禁用该线程的时间片。
6、 查询当前正在运行线程
- **TX_THREAD *tx_thread_identify(VOID)**:返回当前正在运行线程控制块指针,如果当前没有线程运行,则返回NULL。
如果从 ISR 调用此服务,则返回值表示在执行中断处理程序之前正在运行的线程。
例如:
1 | TX_THREAD\* current_thread = tx\_thread\_identify(); |
7、线程放弃控制
一个线程可以通过使用 tx_thread_relinquish 服务自愿将控制权交给另一个线程。 通常采取此操作是为了实现某种形式的循环调度。 该动作是由当前正在执行的线程进行的协作调用,它暂时放弃对处理器的控制,从而允许执行具有相同或更高优先级的其他线程。 这种技术有时被称为协作多线程。
- **VOID tx_thread_relinquish(VOID)**:此函数将当前执行的线程移动到具有相同优先级的就绪线程列表的末尾。
例如:
1 | tx\_thread\_relinquish(); |
在 tx_thread_relinquish 调用者再次执行之前,调用此服务使所有其他具有相同优先级(或更高)的就绪线程有机会执行。
8、线程执行概述
ThreadX 应用程序中有四种类型的程序执行:初始化、线程执行、中断服务例程 (ISR) 和应用程序计时器。

初始化是程序执行的第一种类型。 初始化包括处理器复位和线程调度循环的入口点之间的所有程序执行。
初始化完成后,ThreadX 进入其线程调度循环。 调度循环寻找准备好执行的应用程序线程。 当找到一个就绪线程时,ThreadX 将控制权转移给它。 一旦线程完成(或另一个更高优先级的线程准备就绪),执行将转移回线程调度循环,以便找到下一个最高优先级的就绪线程。 这种不断执行和调度线程的过程是 ThreadX 应用程序中最常见的程序执行类型。
中断是实时系统的基石。 没有中断,很难及时响应外部世界的变化。 发生中断时会发生什么? 检测到中断后,处理器会保存有关当前程序执行的关键信息(通常在堆栈上),然后将控制权转移到预定义的程序区域。 这个预定义的程序区域通常称为中断服务程序。 在大多数情况下,中断发生在线程执行期间(或线程调度循环中)。 但是,中断也可能发生在正在执行的 ISR 或应用程序定时器中。
应用程序定时器与 ISR 非常相似,除了实际的硬件实现(通常使用单个周期性硬件中断)对应用程序隐藏。 应用程序使用此类计时器来执行超时、定期操作和/或看门狗服务。 就像 ISR 一样,应用程序计时器最常中断线程执行。 然而,与 ISR 不同的是,应用程序定时器不能相互中断。
9、线程设计
ThreadX 对可以创建的线程数或可以使用的优先级组合没有任何限制。 但是,为了优化性能并最小化目标大小,应遵守以下准则:
- 最小化应用系统中的线程数量
- 谨慎选择优先级
- 最小化优先级的数量
- 考虑抢占阈值
- 使用互斥锁时考虑优先级继承
- 考虑循环调度
- 考虑时间片
还有其他指导方针,例如确保使用线程来完成特定的工作单元,而不是一系列不同的操作。
9.1 最小化线程数量
通常,应用程序中的线程数会显着影响系统开销的数量。 这是由几个因素造成的,包括维护线程所需的系统资源量,以及调度程序激活下一个就绪线程所需的时间。 每个线程,无论是否必要,都会消耗堆栈空间以及线程本身的内存空间以及 TCB 的内存。
9.2 谨慎选择优先
选择线程优先级是多线程最重要的方面之一。 一个常见的错误是根据感知到的线程重要性概念分配优先级,而不是确定运行时实际需要什么。 滥用线程优先级会使其他线程饿死,造成优先级倒置,减少处理带宽,并使应用程序的运行时行为难以理解。 如果线程饥饿是一个问题,应用程序可以使用附加逻辑逐渐提高饥饿线程的优先级,直到它们有机会执行。 但是,首先正确选择优先级可能会显着减少此问题。
9.3 最小化优先级的数量
ThreadX 提供了 32 个不同的优先级值,可以分配给线程。 但是,开发人员应仔细分配优先级,并应根据所讨论线程的重要性确定优先级。 具有许多不同线程优先级的应用程序本质上比具有较少优先级的应用程序需要更多的系统开销。回想一下,ThreadX 提供了一种基于优先级的抢占式调度算法。 这意味着在没有高优先级线程准备好执行之前,低优先级线程不会执行。 如果较高优先级的线程始终准备就绪,则较低优先级的线程永远不会执行。
要了解线程优先级对上下文切换开销的影响,假设三个线程,名称分为为: thread_1、thread_2 和 thread_3 。另外,假设所有线程都已挂起并等待消息。 当 thread_1 收到消息时,它立即将其转发给 thread_2。 thread_2 然后将消息转发给 thread_3。 thread_3 只是丢弃该消息。 每个线程处理完它的消息后,它会再次挂起自己并等待另一个消息。执行这三个线程所需的处理根据它们的优先级而有很大差异。 如果所有线程具有相同的优先级,则在每个线程的执行之间发生一次上下文切换。 当每个线程在空消息队列上挂起时,就会发生上下文切换。
但是,如果 thread_2 的优先级高于 thread_1,并且 thread_3 的优先级高于 thread_2,则上下文切换的次数会加倍。 这是因为当 tx_queue_send 服务检测到更高优先级的线程现在已准备好时,会发生另一个上下文切换。
如果这些线程需要不同的优先级,那么 ThreadX 抢占阈值机制可以防止这些额外的上下文切换。 这是一个重要的特性,因为它在线程调度期间允许多个不同的线程优先级,同时消除了在线程执行期间发生的一些不需要的上下文切换。
9.4 考虑抢占阈值
回想一下,与线程优先级相关的一个潜在问题是优先级反转。当较高优先级的线程因为较低优先级的线程拥有较高优先级线程所需的资源而被挂起时,就会发生优先级反转。 在某些情况下,两个不同优先级的线程需要共享一个公共资源。如果这些线程是唯一活跃的,则优先级反转时间受限于较低优先级线程持有资源的时间。 这种情况既是确定性的,也是非常正常的。 但是,如果一个或多个中等优先级的线程在此优先级反转条件下变为活动状态(从而抢占较低优先级的线程),则优先级反转时间变得不确定,应用程序可能会失败。
在 ThreadX 中有三种防止优先级反转的主要方法:
- 首先,开发人员可以选择应用程序优先级并以防止优先级反转问题的方式设计运行时行为。
- 其次,低优先级线程可以利用抢占阈值来阻止来自中间线程的抢占,同时它们与高优先级线程共享资源。
- 最后,使用 ThreadX 互斥对象来保护系统资源的线程可以利用可选的互斥优先级继承来消除不确定的优先级反转。
9.5 考虑优先继承
在优先级继承中,低优先级线程临时获取高优先级线程的优先级,该高优先级线程试图获得低优先级线程拥有的相同互斥锁。 当低优先级线程释放互斥锁时,它的原始优先级随即恢复,高优先级线程获得互斥锁的所有权。 此功能通过将反转时间限制为较低优先级线程持有互斥锁的时间来消除优先级反转。 请注意,优先级继承仅适用于互斥体,但不适用于计数信号量。
9.6 考虑轮询调度
ThreadX 支持对具有相同优先级的多个线程进行循环调度。这是通过协同调用 tx_thread_relinquish 服务来实现的。调用该服务使所有其他具有相同优先级的就绪线程有机会在 tx_thread_relinquish 服务的调用者执行之前执行 再次。
9.7 考虑时间片
时间片为具有相同优先级的线程提供了另一种形式的循环调度。 ThreadX 使时间片在每个线程的基础上可用。 应用程序在创建线程时分配线程的时间片,并且可以在运行时修改时间片。 当一个线程的时间片到期时,所有其他具有相同优先级的就绪线程都有机会在时间片线程再次执行之前执行。
文章来源: https://iotsmart.blog.csdn.net/article/details/125098722