ThreadX学习(2)——线程

ThreadX学习(2)——线程

  • 学习参考:
  • ThreadX中的线程
    • 线程创建
    • 堆栈分配
    • 互斥锁
    • 线程优先级
      • 优先级反转
      • 优先级继承
      • 抢占阈值
    • 线程状态
    • 数据结构
      • TCB
      • 就绪列表
    • API
      • 1.tx_thread_ create
      • 2.tx_thread_delete
      • 3.tx_thread_identify
      • 4.tx_thread_info_get
      • 5.tx_thread_preemption_change
      • 6.tx_thread_priority_change
      • 7.tx_thread_relinquish
      • 8.tx_thread_resume
      • 9.tx_thread_sleep
      • 10.tx_thread_suspend
      • 11.tx_thread_terminate
      • 12.tx_thread_time_slice_change

学习参考:

  • 《Real-Time Embedded Multithreading: Using ThreadX and ARM》
    (链接:https://pan.baidu.com/s/1GMScXSlHq13yS4AVxP_zPw 提取码:ysuw )
  • 安富莱_STM32-V7开发板ThreadX内核教程(V0.7)——硬汉嵌入式论坛

ThreadX中的线程

在ThreadX中,一般没有进程的概念,统称为线程

ThreadX学习(2)——线程_第1张图片
关于调度器的实现细节,ThreadX可能是用汇编写的,没看懂。但大概是一个寻找最高优先级线程来运行的循环,通过_tx_thread_execute_ptr变量来表示下一个运行的线程。其他线程状态变化的API也会修改这个变量。

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抢占阈值机制可以防止这些额外的上下文切换。这是一个重要的特性,因为在线程调度期间,它允许几个不同的线程优先级,同时消除了线程执行期间发生的一些不需要的上下文切换。

线程创建

创建线程时,需要指定几个参数:

  • 线程控制块(TCB):每个线程都必须有一个TCB,其中包含对该线程的内部处理至关重要的系统信息。但大多数应用程序不需要访问TCB的内容。
  • 线程名称:每个线程都分配了一个名称,主要用于标识目的。
  • 线程入口函数:线程的实际C代码所在的位置。
  • 线程入口输入:在线程入口函数第一次执行时传递给它的值,完全由开发人员决定。
  • 堆栈指针和大小:每个线程都必须有一个堆栈,因此指定了一个指向实际堆栈位置的指针,以及堆栈大小。
  • 线程优先级:必须提前设置优先级,但可以在运行时更改它。
  • 抢占阈值:可选项,若与优先级的值相等,则表示禁用抢占阈值特性。
  • 时间片:可选项,指定相同优先级的就绪线程之间的,可运行的计时器节拍数。注意,使用抢占阈值则禁用时间片选项。时间片值为0也表示禁用时间片选项。
  • start选项:必须指定,表示线程是立即启动,还是处于挂起状态。

堆栈分配

创建线程时,需要给每个线程分配一个堆栈(当线程被抢占时,线程可以存储信息的地方,比如返回地址和局部变量。)

每个堆栈需要一个连续字节块,它由内存字节池 memory byte pool,或内存块池 memory block pool以及数组分配,前两者都需要提前创建。

同一内存字节池/内存块池,可以为多个线程分配堆栈。

以上皆由tx_application_define函数完成。

互斥锁

一旦一个线程获得了互斥锁的所有权,它将保持所有权,直到它自愿放弃互斥锁。

不管线程的优先级如何,没有一个线程可以抢占另一个线程拥有的互斥锁。这是提供线程间互斥的一个重要特性。

因此,低优先级线程应尽量避免长时间占用互斥锁。

线程优先级

ThreadX提供了一种灵活的动态优先级分配方法。尽管每个线程必须有一个优先级,但ThreadX对如何使用优先级没有任何限制。作为一种极端情况,所有线程都可以被分配相同的优先级,而不会发生改变。但是,在大多数情况下,优先级值的分配和修改只是为了反映线程处理过程中重要性的变化。

ThreadX提供了从0-31的优先级值,其中值0表示最高优先级,值31表示最低优先级。

优先级反转

当两个具有不同优先级的线程共享一个公共资源时,可能会出现优先级反转的情况:

当低优先级线程获取了高优先级线程所需的资源(如上节互斥锁),导致高优先级线程挂起时,就会出现这种情况。如果这些线程是唯一活动的线程,则优先级反转时间受低优先级线程占用资源的时间限制。这种情况是确定的,也是很正常的。

如果一个或多个中等优先级的线程在此优先级反转条件期间处于活动状态(因此抢占了较低优先级的线程),则低优先级保有资源的时间将会延长,优先级反转时间将变得不确定,应用程序可能会失败。

优先级继承和抢占阈值,可以解决优先级反转的不确定问题。

优先级继承

在优先级继承中,一个低优先级的线程暂时获得一个高优先级的线程的优先级,而高优先级的线程正在试图获得由低优先级线程拥有的相同的互斥锁。如此可以避免低优先级线程被中等优先级线程抢占。

当低优先级的线程释放互斥锁时,它的原始优先级将被恢复,高优先级的线程将获得互斥锁的所有权。

这个特性通过将反转时间限制在低优先级线程持有互斥锁的时间,从而消除了优先级反转。注意,优先级继承只对互斥量有效,而对计数信号量无效。

抢占阈值

抢占阈值 Preemption-Threshold, 是ThreadX特有的特性。创建线程时,开发人员可以为禁用抢占指定优先级上限。这意味着优先级大于指定上限的线程仍然允许抢占,但是优先级等于或小于指定上限的线程不允许抢占。抢占阈值可以在线程执行期间的任何时候被修改。

例如,一个线程优先级值20和抢占阈值15,只有优先级高于15(即0到14)的线程才被允许抢占该线程。即使优先级15到19高于线程的优先级20,但仍不被允许抢占该线程。

通过更改线程的抢占阈值,同样可以解决优先级反转的问题。

线程状态

ThreadX 有五个不同的线程状态:就绪、挂起、执行、终止和完成。
ThreadX学习(2)——线程_第2张图片
当一个线程准备好执行时,它会被放在就绪线程列表中。当ThreadX调度一个线程执行时,它会选择并删除就绪线程列表中具有最高优先级的线程。如果列表中的所有线程具有相同的优先级,ThreadX将选择等待时间最长的线程,并采用时间片轮转的调度方式。

数据结构

TCB

在ThreadX中,TCB为结构体TX_THREAD_STRUCT

通常开发人员不需要知道TCB的细节。但某些情况下特别是调试时,检查某些字段非常有用。如:

  • tx_run_count:调度线程的次数。计数器增加表示正在调度和执行线程。
  • tx_state:线程的状态。下面的列表代表了可能的线程状态:
    • TX_READY
    • TX_COMPLETED
    • TX_TERMINATED
    • TX_SUSPENDED
    • TX_SLEEP
    • TX_QUEUE_SUSP
    • TX_SEMAPHORE_SUSP
    • TX_EVENT_FLAG
    • TX_BLOCK_MEMORY
    • TX_BYTE_MEMORY
    • TX_MUTEX_SUSP
    • TX_IO_DRIVER

TCB中还有许多其他有用的字段,包括堆栈指针、时间片值和优先级。开发人员可以检查TCB的成员,但严禁修改它们。

没有显式的值来指示线程当前是否正在执行。在给定的时间只有一个线程执行,ThreadX在其他地方跟踪当前执行的线程。

注意,正在执行的线程的tx_state的值是TX_READY。

就绪列表

就绪列表其实是一个容量为32(表示32个优先级)的链表数组,数组的每个元素都是线程TCB链表头,存储该优先级已经就绪的线程TCB结点。
ThreadX学习(2)——线程_第3张图片
该数据结构由common/inc/tx_thread.h文件定义:

THREAD_DECLARE  TX_THREAD *     _tx_thread_priority_list[TX_MAX_PRIORITIES];

除此之外,还定义了一个ULONG类型的变量,作为每个优先级是否有线程就绪的标志。
在这里插入图片描述
如上图,ULONG变量一共32位,每位表示一个优先级,若该位为1,则表示此优先级有就绪线程。

该数据结构由common/inc/tx_thread.h文件定义:

THREAD_DECLARE  ULONG           _tx_thread_priority_maps[TX_MAX_PRIORITIES/32];

API

1.tx_thread_ create

UINT  tx_thread_create(TX_THREAD *thread_ptr, 
						CHAR *name_ptr, 
						VOID (*entry_function)(ULONG id), 
						ULONG entry_input,
						VOID *stack_start, 
						ULONG stack_size, 
						UINT priority, 
						UINT preempt_threshold,
						ULONG time_slice, 
						UINT auto_start)

创建线程:

  • 第 1 个参数 thread_ptr 是线程控制块地址。
  • 第 2 个参数 name_ptr 是线程名,这个参数主要是用于调试目的,调试的时候方便看是哪个线程。
  • 第 3 个参数 entry_function 是线程函数地址。当线程从此入口函数返回时,它将处于 Complete
    -State 完成态,并无限挂起。
  • 第 4 个参数 entry_input 是传递给线程的形参。
  • 第 5 个参数 stack_start 栈基地址。
  • 第 6 个参数 stack_size 是栈大小,单位字节,主要被函数嵌套调用和局部变量使用。
  • 第 7 个参数 priority 是线程优先级,范围 0 到(TX_MAX_PRIORITES-1),其中 0 表示最高优先级。
  • 第 8 个参数 preempt_threshold 是抢占阀值,范围 0 到(TX_MAX_PRIORITES-1)。只有高于此
    级别的优先级才可以抢占该线程。此数值必须小于或等于该线程的优先级数值。如果设置为等于该
    线程的优先级数值,将禁用抢占阈值。
  • 第 9 个参数 time_slice 是时间片大小。
  • 第 10 个参数 auto_start 是指定线程是立即启动还是处于挂起状态。
    • TX_AUTO_START(0x01)立即启动。
    • TX_DONT_START(0x00)挂起状态。
      如果指定了 TX_DONT_START,则应用程序以后必须调用 tx_thread_resume 才能运行线程。
  • 返回值:
    • TX_SUCCESS(0x00)成功创建线程。
    • TX_THREAD_ERROR(0x0E)无效的线程控制块指针。指针为 NULL 或任务已创建。
    • TX_PTR_ERROR(0x03)线程函数地址或栈的起始地址无效,通常为 NULL。
    • TX_SIZE_ERROR(0x05)栈区域的大小无效。线程必须至少具有 TX_MINIMUM_STACK 字节
      才能执行。
    • TX_PRIORITY_ERROR(0x0F)无效的线程优先级,该值超出(0 到(TX_MAX_PRIORITIES-1))
      范围。
    • TX_THRESH_ERROR(0x18)指定了无效的抢占阈值。该值的有效优先级必须小于或等于该线程的初始优先级数值。
    • TX_START_ERROR(0x10)无效的 auto_start 参数。
    • TX_CALLER_ERROR(0x13)无效调用。
  • 不允许在中断服务程序中调用,只可以在初始化和线程中调用。
  • 使用抢占阈值将禁用时间片。合法的时间片值范围是 1 到 0xFFFFFFFF(包括 0)。值为
    TX_NO_TIME_SLICE(值为 0)将禁用此线程的时间片。
  • 使用时间分片会导致少量系统开销。由于时间片仅在多个线程共享相同优先级的情况下才有用,因此,具有唯一优先级的线程不要分配时间片。

2.tx_thread_delete

UINT tx_thread_delete(TX_THREAD *thread_ptr)

删除线程:

  • 第 1 个参数填要删除的线程控制块地址。
  • 返回值:
    • TX_SUCCESS(0x00)成功删除线程。
    • TX_THREAD_ERROR(0x0E)无效的TCB指针。
    • TX_DELETE_ERROR(0x11)指定的线程未处于 Terminated 终止态或者 Completed 完成态。
    • TX_CALLER_ERROR(0x13)无效调用者。
  • 不允许在中断服务程序中调用,只可以在线程和定时器组中调用。
  • 只能删除处于 Terminated 终止态或者 Completed 完成态的线程。因此也就不可以在要删除的线程
    中调用此函数,也就是自己删除自己。

3.tx_thread_identify

#define tx_thread_identify   _tx_thread_identify

TX_THREAD  *_tx_thread_identify(VOID)

返回正在执行线程:

  • 返回值:指向当前执行线程的指针。如果没有线程正在执行,返回空指针。如果从ISR调用该服务,则返回值表示在执行中断处理程序之前正在运行的线程。

4.tx_thread_info_get

UINT  tx_thread_info_get( TX_THREAD *thread_ptr, 
							CHAR **name, 
							UINT *state, 
							ULONG *run_count, 
							UINT *priority, 
							UINT *preemption_threshold, 
							ULONG *time_slice, 
							TX_THREAD **next_thread, 
							TX_THREAD **next_suspended_thread)

获取线程信息:

  • 第 1 个参数 thread_ptr 是目标线程的TCB指针。
  • 第 2 个参数 name 是线程名字符串,获取后存储的指针。
  • 第 3 个参数 state 是线程状态,获取后存储的指针。
  • 第 4 个参数 run_count 是线程调度次数,获取后存储的指针。
  • 第 5 个参数 priority 是线程优先级,获取后存储的指针。
  • 第 6 个参数 preemption_threshold 是线程抢占阈值,获取后存储的指针。
  • 第 7 个参数 time_slice 是线程时间片,获取后存储的指针。
  • 第 8 个参数 next_thread 是目标线程下一个创建的线程的TCB指针,获取后存储的指针。
  • 第 9 个参数 next_suspended_thread 是挂起列表中的下一个线程TCB指针,获取后存储的指针。
  • 返回值:
    • TX_SUCCESS(0x00)成功获得线程信息。
    • TX_THREAD_ERROR(0x0E)无效的TCB指针。

5.tx_thread_preemption_change

UINT  tx_thread_preemption_change(  TX_THREAD *thread_ptr, 
									UINT new_threshold, 
									UINT *old_threshold)

更改线程抢占阈值:

  • 第 1 个参数 thread_ptr 是目标线程的TCB指针。
  • 第 2 个参数 new_threshold 新抢占阈值。
  • 第 3 个参数 old_threshold 原抢占阈值的存储指针。
  • 返回值:
    • TX_SUCCESS(0x00)成功更改线程抢占阈值。
    • TX_THRESH_ERROR(0x18)新抢占阈值大于优先级值。

6.tx_thread_priority_change

UINT  tx_thread_priority_change(TX_THREAD *thread_ptr, 
								UINT new_priority, 
								UINT *old_priority)

更改线程优先级:

  • 第 1 个参数 thread_ptr 是目标线程的TCB指针。
  • 第 2 个参数 new_priority新优先级。
  • 第 3 个参数 old_priority原优先级的存储指针。
  • 返回值:
    • TX_SUCCESS(0x00)成功更改线程优先级。
  • 允许在线程和定时器组里面调用。
  • 如果要同时更改抢占阈值,则必须在更改优先级完成后再更改抢占阈值。

7.tx_thread_relinquish

VOID  tx_thread_relinquish(VOID)

转让CPU控制权:

线程可以通过tx_thread_ relinquish自愿地将控制权让给另一个线程。

执行此操作通常是为了实现一种循环调度形式。这个动作是当前正在执行的线程发出的一个协作调用,它暂时放弃对处理器的控制,从而允许具有相同或更高优先级的其他线程执行。

这种技术有时称为协作多线程。

8.tx_thread_resume

UINT  tx_thread_resume(TX_THREAD *thread_ptr)

唤醒线程:

  • 第 1 个参数 thread_ptr 是目标线程的TCB指针。
  • 返回值:
    • TX_SUCCESS(0x00)成功的唤醒线程。
    • TX_SUSPEND_LIFTED(0x19)先前设置的延迟暂停已取消。
    • TX_THREAD_ERROR(0x0E)无效的线程TCB地址。
    • TX_RESUME_ERROR(0x12)指定的线程没有被挂起,或者之前被 tx_thread_suspend 以外的服务挂起。

允许在中断,线程,定时器组和初始化中调用。

9.tx_thread_sleep

UINT tx_thread_sleep(ULONG timer_ticks)

线程延迟:

  • 第 1 个参数 timer_ticks 是设置线程延迟的时钟节拍数。
  • 返回值:
    • TX_SUCCESS(0x00)成功延迟。
    • TX_WAIT_ABORTED (0x1A) ,被其它中断,定时器组或者线程终止运行。
    • TX_CALLER_ERROR (0x13) ,不是在线程中调用。

不允许在中断中调用,仅可以在线程中调用

10.tx_thread_suspend

UINT tx_thread_suspend(TX_THREAD *thread_ptr)

线程无条件挂起:

  • 第 1 个参数 thread_ptr 是目标线程的TCB指针。
  • 返回值:
    • TX_SUCCESS(0x00)成功挂起线程。
    • TX_THREAD_ERROR(0x0E)无效的线程TCB指针。
    • TX_SUSPEND_ERROR(0x14)指定的线程处于终止或完成状态。
    • TX_CALLER_ERROR(0x13)无效调用者。
  • 允许在中断,线程,定时器组和初始化中调用。
  • 如果线程已有条件挂机,则本次无条件挂起将被保存,直到有条件挂起恢复,再执行线程的无条件挂起。若线程已无条件挂起,则请求无效。

11.tx_thread_terminate

UINT tx_thread_terminate(TX_THREAD *thread_ptr)

线程无条件终止:

  • 第 1 个参数 thread_ptr 是目标线程的TCB指针。
  • 返回值:
    • TX_SUCCESS(0x00)成功终止线程。
    • TX_THREAD_ERROR(0x0E)无效的线程TCB指针。
    • TX_CALLER_ERROR(0x13)无效调用者。
  • 不允许在中断中调用,仅可以在线程和定时器组中调用,可以终止自身。
  • 终止后,必须调用函数 tx_thread_reset 复位线程以使其再次执行。
  • 应用程序有责任确保线程处于适合终止的状态。例如,线程不应在关键应用程序处理期间或在其他
    中间件组件内部终止,否则可能会使这种处理处于未知状态。

12.tx_thread_time_slice_change

UINT  _tx_thread_time_slice_change( TX_THREAD *thread_ptr, 
									ULONG new_time_slice, 
									ULONG *old_time_slice)

更改线程时间片:

  • 第 1 个参数 thread_ptr 是目标线程的TCB指针。
  • 第 2 个参数 new_time_slice新时间片。
  • 第 3 个参数 old_time_slice原时间片的存储指针。
  • 返回值:
    • TX_SUCCESS(0x00)成功。
    • TX_THREAD_ERROR:(0x0E)无效的应用程序线程指针。
    • TX_PTR_ERROR: (0x03)指向以前的时间片存储位置的指针无效。
    • TX_CALLER_ERROR:(0x13)无效的服务调用者。
  • 如果已经指定了抢占阈值,那么将禁用该线程的时间片。

你可能感兴趣的:(ThreadX,多线程)