Thread Management
In daily life, when we want to complete a big task, we usually break it down into multiple simple and easy-to-solve small problems. When the small problems are solved one by one, the big problem will be solved. In a multi-threaded operating system, developers are also required to break down a complex application into multiple small, schedulable, and serialized program units. When the tasks are divided reasonably and executed correctly, this design can enable the system to meet the performance and time requirements of the real-time system. For example, let the embedded system perform such a task. The system collects data through sensors and displays the data on the display. In a multi-threaded real-time system, this task can be broken down into two subtasks, as shown in the figure below. One subtask continuously reads sensor data and writes the data to shared memory. The other subtask periodically reads data from shared memory and outputs the sensor data to the display.
In RT-Thread, the program entity corresponding to the above subtask is the thread. The thread is the carrier for implementing tasks. It is the most basic scheduling unit in RT-Thread. It describes the operating environment for the execution of a task and also describes the priority level of the task. Important tasks can be set with relatively high priorities, while unimportant tasks can be set with lower priorities. Different tasks can also be set with the same priority and run in turns.
When a thread is running, it thinks that it is running in an exclusive CPU mode. The running environment of the thread is called context, which refers to various variables and data, including all register variables, stacks, memory information, etc.
This chapter will be divided into 5 sections to introduce RT-Thread thread management. After reading this chapter, readers will have a deeper understanding of RT-Thread's thread management mechanism, such as: what states does a thread have, how to create a thread, why there are idle threads, and other questions, and they will have a clear answer in their minds.
The main function of RT-Thread thread management is to manage and schedule threads. There are two types of threads in the system, namely system threads and user threads. System threads are threads created by the RT-Thread kernel, and user threads are threads created by applications. Both types of threads will allocate thread objects from the kernel object container. When the thread is deleted, it will also be deleted from the object container, as shown in the figure below. Each thread has important properties, such as thread control block, thread stack, entry function, etc.
RT-Thread's thread scheduler is preemptive. Its main job is to find the highest priority thread from the list of ready threads to ensure that the highest priority thread can be run. Once the highest priority task is ready, it can always get the right to use the CPU.
When a running thread makes a thread with a higher priority than it meet the running conditions, the current thread's right to use the CPU is deprived, or given up, and the high-priority thread immediately obtains the right to use the CPU.
If the interrupt service routine makes a high-priority thread meet the running conditions, when the interrupt is completed, the interrupted thread is suspended and the high-priority thread starts running.
When the scheduler schedules a thread switch, it first saves the current thread context. When switching back to this thread, the thread scheduler restores the context information of the thread.
In RT-Thread, the thread control block is represented by the structure struct rt_thread. The thread control block is a data structure used by the operating system to manage threads. It stores some thread information, such as priority, thread name, thread status, etc. It also contains a linked list structure for connecting threads, a thread waiting event set, etc. The detailed definition is as follows:
Among them, init_priority is the thread priority specified when the thread is created, and it will not be changed during the thread running process (unless the user executes the thread control function to manually adjust the thread priority). When the thread exits, cleanup will be called back by the idle thread once to perform the user-set cleanup and other tasks. The last member user_data can be used by the user to attach some data information to the thread control block to provide a similar implementation method for thread private data.
RT-Thread threads have independent stacks. When switching threads, the context of the current thread will be stored in the stack. When the thread is to be resumed, the context information is read from the stack for restoration.
The thread stack is also used to store local variables in functions: local variables in functions are requested from the thread stack space; local variables in functions are initially allocated from registers (ARM architecture), and when this function calls another function, these local variables will be placed on the stack.
For the first time the thread runs, you can manually construct this context to set up some initial environments: entry function (PC register), entry parameters (R0 register), return location (LR register), and current machine running status (CPSR register).
The growth direction of the thread stack is closely related to the chip architecture. Versions before RT-Thread 3.1.0 only support the stack growing from high addresses to low addresses. For the ARM Cortex-M architecture, the thread stack can be constructed as shown in the following figure.
The thread stack size can be set in this way. For MCUs with relatively large resources, a larger thread stack can be appropriately designed. You can also set a larger stack initially, for example, specify a size of 1K or 2K bytes, and then use the list_thread command in FinSH to view the size of the stack used by the thread during thread execution. Through this command, you can see the maximum stack depth used by the thread from the time the thread starts running to the current time point, and then add an appropriate margin to form the final thread stack size, and finally modify the stack space size.
During the running process of a thread, only one thread is allowed to run in the processor at the same time. From the perspective of the running process, threads have different running states, such as initial state, suspended state, ready state, etc. In RT-Thread, a thread contains five states, and the operating system will automatically adjust its state dynamically according to its running situation. The five states of threads in RT-Thread are shown in the following table:
state | describe |
Initial state | When a thread is just created and has not yet started running, it is in the initial state; in the initial state, the thread does not participate in scheduling. This state is defined in the RT-Thread macro as RT_THREAD_INIT |
Ready state | In the ready state, threads are queued according to their priority, waiting to be executed; once the current thread has finished running and gives up the processor, the operating system will immediately look for the highest priority ready thread to run. This state is defined in the RT-Thread macro as RT_THREAD_READY |
Running status | The thread is currently running. In a single-core system, only the thread returned by the rt_thread_self() function is in the running state; in a multi-core system, there may be more than one thread in the running state. This state is defined in the RT-Thread macro as RT_THREAD_RUNNING |
Suspended state | Also called blocking state. It may be suspended waiting due to unavailable resources, or the thread may be suspended due to active delay for a period of time. In the suspended state, the thread does not participate in scheduling. The macro definition of this state in RT-Thread is RT_THREAD_SUSPEND |
Closed state | When the thread finishes running, it will be in the closed state. The closed thread does not participate in the thread scheduling. This state is defined in the macro of RT-Thread as RT_THREAD_CLOSE |
The priority of an RT-Thread thread indicates the priority of a thread being scheduled. Each thread has a priority. The more important a thread is, the higher the priority it should be given, and the greater the possibility that the thread will be scheduled.
RT-Thread supports up to 256 thread priorities (0~255). The smaller the value, the higher the priority, and 0 is the highest priority. In some systems with tight resources, you can choose to support only 8 or 32 priorities according to the actual situation; for the ARM Cortex-M series, 32 priorities are generally used. The lowest priority is assigned to the idle thread by default and is generally not used by users. In the system, when a thread with a higher priority than the current thread is ready, the current thread will be swapped out immediately, and the high-priority thread will preempt the processor to run.
Each thread has a time slice parameter, but the time slice is only valid for ready threads with the same priority. When the system schedules ready threads with the same priority using a time slice round-robin scheduling method, the time slice plays a role in constraining the single running time of the thread. Its unit is one system tick (OS Tick). For details, see the "Clock Management" section. Assume that there are two ready threads A and B with the same priority. The time slice of thread A is set to 10, and the time slice of thread B is set to 5. Then, when there is no ready thread with a higher priority than A in the system, the system will switch back and forth between threads A and B, and execute thread A for 10 ticks each time and thread B for 5 ticks each time, as shown in the following figure.
The entry in the thread control block is the entry function of the thread, which is the function that the thread implements the expected function. The entry function of the thread is designed and implemented by the user, and generally has the following two code forms:
- Infinite loop mode:
In real-time systems, threads are usually passive: this is determined by the characteristics of real-time systems. Real-time systems usually always wait for external events to occur and then provide corresponding services:
Threads seem to have no factors that limit program execution, and it seems that all operations can be executed. However, as a real-time system, a real-time system with clear priorities, if a program in a thread falls into an infinite loop operation, then threads with lower priorities than it will not be able to be executed. Therefore, one thing that must be noted in a real-time operating system is that the thread cannot fall into an infinite loop operation, and there must be an action to give up the right to use the CPU, such as calling a delay function in the loop or actively suspending. The purpose of designing such an infinite loop thread by the user is to allow this thread to be scheduled and run by the system loop all the time and never deleted.
- Sequential execution or limited loop mode:
For example, simple sequential statements, do while() or for() loops, etc. These threads will not loop or will not loop forever. They are "one-shot" threads and will be executed to completion. After execution, the thread will be automatically deleted by the system.
A thread is an execution scenario. The error code is closely related to the execution environment, so each thread is equipped with a variable to save the error code. The error codes of threads are as follows:
RT-Thread provides a series of operating system call interfaces to switch the thread state back and forth between these five states. The conversion relationship between several states is shown in the following figure:
The thread enters the initial state (RT_THREAD_INIT) by calling the function rt_thread_create/init(); the thread in the initial state enters the ready state (RT_THREAD_READY) by calling the function rt_thread_startup(); the thread in the ready state enters the running state (RT_THREAD_RUNNING) after being scheduled by the scheduler; when the thread in the running state calls rt_thread_delay(), rt_sem_take(), rt_mutex_take(), rt_mb_recv() and other functions or fails to obtain resources, it will enter the suspended state (RT_THREAD_SUSPEND); if the thread in the suspended state still fails to obtain resources after waiting for timeout or because other threads release resources, it will return to the ready state. If the thread in the suspended state calls the rt_thread_delete/detach() function, it will change to the closed state (RT_THREAD_CLOSE); and if the thread in the running state ends, the rt_thread_exit() function will be executed at the end of the thread to change the state to the closed state.
As mentioned above, system threads are threads created by the system, and user threads are threads created by user programs calling thread management interfaces. System threads in the RT-Thread kernel include idle threads and main threads.
The idle thread is the lowest priority thread created by the system, and the thread state is always in the ready state. When there are no other ready threads in the system, the scheduler will schedule to the idle thread, which is usually an infinite loop and can never be suspended. In addition, the idle thread also has its special purpose in RT-Thread:
If a thread has finished running, the system will automatically delete the thread: the rt_thread_exit() function will be automatically executed, first the thread will be deleted from the system ready queue, then the thread's state will be changed to the closed state, and it will no longer participate in system scheduling, and then it will be hung in the rt_thread_defunct zombie queue (a thread queue whose resources have not been recycled and is in the closed state). Finally, the idle thread will recycle the resources of the deleted thread.
The idle thread also provides an interface to run the user-set hook function, which will be called when the idle thread is running. It is suitable for processing power management, watchdog feeding, etc. The idle thread must have a chance to be executed, that is, other threads are not allowed to be stuck in while(1) all the time, and must call a blocking function; otherwise, operations such as thread deletion and recycling will not be executed correctly.
When the system starts, the system will create the main thread, whose entry function is main_thread_entry(). The user's application entry function main() actually starts from here. After the system scheduler is started, the main thread starts running. The process is shown in the figure below. Users can add their own application initialization code in the main() function.
The previous two sections of this chapter have explained the concept of the functions and working mechanisms of threads. I believe that everyone is no longer unfamiliar with threads. This section will go deep into the various interfaces of RT-Thread threads and provide some source code to help readers understand threads at the code level.
The following figure describes the related operations of threads, including: creating/initializing threads, starting threads, running threads, and deleting/detaching threads. You can use rt_thread_create() to create a dynamic thread and use rt_thread_init() to initialize a static thread. The difference between dynamic threads and static threads is that for dynamic threads, the system automatically allocates stack space and thread handles from the dynamic memory heap (you can use create to create dynamic threads only after initializing the heap), while for static threads, the user allocates stack space and thread handles.
For a thread to become an executable object, the kernel of the operating system must create a thread for it. A dynamic thread can be created through the following interface:
When this function is called, the system allocates a thread handle from the dynamic heap memory and allocates the corresponding space from the dynamic heap memory according to the stack size specified in the parameter. The allocated stack space is aligned according to the RT_ALIGN_SIZE method configured in rtconfig.h. The parameters and return values of thread creation rt_thread_create() are shown in the following table:
parameter | describe |
name | The name of the thread; the maximum length of the thread name is specified by the macro RT_NAME_MAX in rtconfig.h, and the excess part will be automatically truncated |
entry | Thread entry function |
parameter | Thread entry function parameters |
stack_size | Thread stack size in bytes |
priority | The priority of the thread. The priority range depends on the system configuration (RT_THREAD_PRIORITY_MAX macro definition in rtconfig.h). If 256 priority levels are supported, the range is from 0 to 255. The smaller the value, the higher the priority. 0 represents the highest priority. |
tick | The time slice size of the thread. The unit of the time slice (tick) is the clock beat of the operating system. When there are threads of the same priority in the system, this parameter specifies the maximum length of time that a thread can run in one scheduling. When this time slice runs out, the scheduler automatically selects the next ready thread of the same priority to run |
return | —— |
thread | The thread is created successfully and the thread handle is returned |
RT_NULL | Thread creation failed |
For some threads created using rt_thread_create(), when they are no longer needed or when an error occurs during operation, we can use the following function interface to completely delete the thread from the system:
After calling this function, the thread object will be removed from the thread queue and deleted from the kernel object manager. The stack space occupied by the thread will also be released, and the reclaimed space will be reused for other memory allocations. In fact, using the rt_thread_delete() function to delete the thread interface only changes the corresponding thread state to the RT_THREAD_CLOSE state and then puts it into the rt_thread_defunct queue; the actual deletion action (releasing the thread control block and releasing the thread stack) needs to be completed by the idle thread the next time the idle thread is executed. The parameters and return values of the thread deletion rt_thread_delete() interface are shown in the following table:
parameter | describe |
thread | The thread handle to delete |
return | —— |
RT_EOK | Thread deleted successfully |
-RT_ERROR | Failed to delete thread |
Note
Note: The rt_thread_create() and rt_thread_delete() functions are only valid when the system dynamic heap is enabled (that is, the RT_USING_HEAP macro is defined).
Thread initialization can be completed using the following function interface to initialize the static thread object:
The thread handle (or thread control block pointer) and thread stack of static threads are provided by the user. Static threads refer to thread control blocks and thread running stacks that are generally set as global variables, which are determined and allocated during compilation, and the kernel is not responsible for dynamically allocating memory space. It should be noted that the stack head address provided by the user needs to be aligned to the system (for example, 4-byte alignment is required on ARM). The parameters and return values of the thread initialization interface rt_thread_init() are shown in the following table:
parameter | describe |
thread | Thread handle. The thread handle is provided by the user and points to the corresponding thread control block memory address. |
name | The name of the thread; the maximum length of the thread name is specified by the RT_NAME_MAX macro defined in rtconfig.h, and the excess part will be automatically truncated |
entry | Thread entry function |
parameter | Thread entry function parameters |
stack_start | Thread stack start address |
stack_size | The thread stack size in bytes. In most systems, stack space address alignment is required (for example, ARM architecture requires alignment to 4-byte addresses) |
priority | The priority of the thread. The priority range depends on the system configuration (RT_THREAD_PRIORITY_MAX macro definition in rtconfig.h). If 256 priority levels are supported, the range is from 0 to 255. The smaller the value, the higher the priority. 0 represents the highest priority. |
tick | The time slice size of the thread. The unit of the time slice (tick) is the clock beat of the operating system. When there are threads of the same priority in the system, this parameter specifies the maximum length of time that a thread can run in one scheduling. When this time slice runs out, the scheduler automatically selects the next ready thread of the same priority to run |
return | —— |
RT_EOK | Thread creation successful |
-RT_ERROR | Thread creation failed |
For threads initialized with rt_thread_init(), using rt_thread_detach() will detach the thread object from the thread queue and kernel object manager. The thread detach function is as follows:
The parameters and return values of the thread detachment interface rt_thread_detach() are shown in the following table:
parameter | describe |
thread | The thread handle, which should be the thread handle initialized by rt_thread_init. |
return | —— |
RT_EOK | Thread detached successfully |
-RT_ERROR | Thread detachment failed |
This function interface corresponds to the rt_thread_delete() function. The object operated by the rt_thread_delete() function is the handle created by rt_thread_create(), while the object operated by the rt_thread_detach() function is the thread control block initialized by the rt_thread_init() function. Similarly, the thread itself should not call this interface to detach from the thread itself.
The created (initialized) thread state is in the initial state and has not entered the scheduling queue of the ready thread. We can call the following function interface to put the thread into the ready state after the thread is initialized/created successfully:
When this function is called, the thread state will be changed to the ready state and placed in the corresponding priority queue to wait for scheduling. If the priority of the newly started thread is higher than that of the current thread, it will be switched to this thread immediately. The parameters and return values of the thread startup interface rt_thread_startup() are shown in the following table:
parameter | describe |
thread | Thread handle |
return | —— |
RT_EOK | Thread started successfully |
-RT_ERROR | Thread start failed |
During the running of the program, the same section of code may be executed by multiple threads. During execution, the handle of the currently executing thread can be obtained through the following function interface:
The return value of this interface is shown in the following table:
return | describe |
thread | The handle of the currently running thread |
RT_NULL | Failed, the scheduler has not started yet |
When the current thread's time slice is used up or the thread actively requests to give up the processor resources, it will no longer occupy the processor, and the scheduler will select the next thread of the same priority to execute. After the thread calls this interface, the thread is still in the ready queue. The thread gives up the processor using the following function interface:
After calling this function, the current thread first deletes itself from the ready priority thread queue, then hangs itself at the end of the priority queue linked list, and then activates the scheduler to perform thread context switching (if there is only one thread with the current priority, this thread continues to execute without context switching).
The rt_thread_yield() function is similar to the rt_schedule() function, but the system behaves completely differently when there are other ready threads of the same priority. After executing the rt_thread_yield() function, the current thread is swapped out, and the next ready thread of the same priority will be executed. After executing the rt_schedule() function, the current thread is not necessarily swapped out. Even if it is swapped out, it will not be placed at the end of the ready thread list. Instead, the highest priority thread in the system will be selected for execution (if there is no thread with a higher priority than the current thread in the system, then after executing the rt_schedule() function, the system will continue to execute the current thread).
In actual applications, we sometimes need to delay the current thread for a period of time and restart it after the specified time arrives. This is called "thread sleep". Thread sleep can use the following three function interfaces:
These three function interfaces have the same function. Calling them can make the current thread suspend for a specified period of time. After this period of time, the thread will be awakened and enter the ready state again. This function accepts a parameter that specifies the sleep time of the thread. The parameters and return values of the thread sleep interface rt_thread_sleep/delay/mdelay() are shown in the following table:
parameter | describe |
tick/ms | The thread sleep time: The parameter tick passed to sleep/delay is in units of 1 OS Tick; the parameter ms passed to mdelay is in units of 1ms; |
return | —— |
RT_EOK | Operation successful |
When a thread calls rt_thread_delay(), the thread will be suspended actively; when calling functions such as rt_sem_take() and rt_mb_recv(), the thread will be suspended if resources are unavailable. If the resource that the thread is waiting for times out (exceeds the set waiting time), the thread will no longer wait for these resources and return to the ready state; or, when other threads release the resources that the thread is waiting for, the thread will also return to the ready state.
Thread suspension uses the following function interface:
The parameters and return values of the thread suspension interface rt_thread_suspend() are shown in the following table:
parameter | describe |
thread | Thread handle |
return | —— |
RT_EOK | Thread suspended successfully |
-RT_ERROR | The thread suspend failed because the thread is not in the ready state. |
Note
Note: It is a very dangerous behavior for a thread to try to suspend another thread, so RT-Thread has strict usage restrictions on this function: this function can only be used to suspend the current thread (that is, suspend itself), and cannot be used to suspend thread B in thread A. And after suspending the thread itself, you need to call rt_schedule()
the function immediately to manually switch the thread context. This is because when thread A tries to suspend thread B, thread A does not know what program thread B is running. Once thread B is using kernel objects such as mutexes and semaphores that affect and block other threads (such as thread C), if other threads are also waiting for this kernel object at this time, then thread A's attempt to suspend thread B will cause starvation of other threads (such as thread C), seriously endangering the real-time performance of the system.
Resuming a thread means making the suspended thread re-enter the ready state and putting the thread into the system's ready queue; if the resumed thread is at the first place in the highest priority list among all ready threads, the system will switch the thread context. Thread resumption uses the following function interface:
The parameters and return values of the thread recovery interface rt_thread_resume() are shown in the following table:
parameter | describe |
thread | Thread handle |
return | —— |
RT_EOK | Thread resumed successfully |
-RT_ERROR | Thread recovery failed because the thread state is not RT_THREAD_SUSPEND |
When you need to perform some other control on the thread, such as dynamically changing the thread priority, you can call the following function interface:
The parameters and return values of the thread control interface rt_thread_control() are shown in the following table:
Function parameters | describe |
thread | Thread handle |
cmd | Instruction control command |
arg | Control parameters |
return | —— |
RT_EOK | Control execution is correct |
-RT_ERROR | fail |
Indicator control command cmd currently supports the following commands:
RT_THREAD_CTRL_CHANGE_PRIORITY: dynamically change the thread priority;
RT_THREAD_CTRL_STARTUP: Start running a thread, equivalent to the rt_thread_startup() function call;
RT_THREAD_CTRL_CLOSE: Close a thread, equivalent to the rt_thread_delete() or rt_thread_detach() function call.
The idle hook function is the hook function of the idle thread. If the idle hook function is set, the idle hook function can be automatically executed to do other things when the system executes the idle thread, such as the system indicator light. The interface for setting/deleting the idle hook is as follows:
The input parameters and return values of the idle hook function rt_thread_idle_sethook() are shown in the following table:
Function parameters | describe |
hook | Set the hook function |
return | —— |
RT_EOK | Set up for success |
-RT_EFULL | Setup failed |
The input parameters and return values of the idle hook delete function rt_thread_idle_delhook() are shown in the following table:
Function parameters | describe |
hook | Deleted hook function |
return | —— |
RT_EOK | Deleted successfully |
-RT_ENOSYS | Deletion failed |
Note
Note: The idle thread is a thread that is always in the ready state, so the hook function must ensure that the idle thread will not be in the suspended state at any time. For example, functions that may cause the thread to suspend, such as rt_thread_delay() and rt_sem_take(), cannot be used. In addition, since malloc, free and other memory-related functions use semaphores as critical section protection, such functions are not allowed to be called in the hook function!
During the operation of the entire system, the system is in the process of thread running, interrupt triggering - responding to interrupts, switching to other threads, or even switching between threads, or the system context switching is the most common event in the system. Sometimes users may want to know what kind of thread switching occurred at a certain moment. You can set a corresponding hook function by calling the following function interface. This hook function will be called when the system thread switches:
The input parameters for setting the scheduler hook function are shown in the following table:
Function parameters | describe |
hook | Indicates a user-defined hook function pointer |
The declaration of the hook function hook() is as follows:
The input parameters of the scheduler hook function hook() are shown in the following table:
Function parameters | describe |
from | Indicates the thread control block pointer that the system wants to switch out |
to | Indicates the thread control block pointer to which the system is switching |
Note
Note: Please write your hook function carefully. Any mistake may cause the whole system to malfunction (in this hook function, calling system API is basically not allowed, and the currently running context should not be suspended).
The following is an application example in the Keil simulator environment.
This example creates a dynamic thread and a static thread. After the static thread completes its task and is automatically recycled by the system, the dynamic thread with a lower priority can start running and printing information.
Note: RT-Thread 5.0 and later versions have
ALIGN
changed the keyword tort_align
, so be careful when using it.
The simulation results are as follows:
When thread 2 counts to a certain value, it will be completed, thread 2 will be automatically deleted by the system, and the counting will stop. Only then will thread 1 print the count.
Note
Note: Regarding thread deletion: Most threads are executed in a loop and do not need to be deleted; for threads that can be completed, RT-Thread will automatically delete the thread after the thread is completed, and the deletion action is completed in rt_thread_exit(). Users only need to understand the function of this interface, and it is not recommended to use this interface (this interface can be called by other threads or in the timer timeout function to delete a thread, but this is rarely used).
This example creates two threads and prints the count during execution, as shown in the following code:
The simulation results are as follows:
From the running count results, we can see that the running time of thread 2 is half of that of thread 1.
When the thread performs scheduling switching, scheduling will be executed. We can set a scheduler hook so that we can do some extra things when the thread switches. This example prints the switching information between threads in the scheduler hook function, as shown in the following code:
Note: RT-Thread5.0 and higher versions
struct rt_thread
move the name member of the structure to parent. When using it, the code needs to bethread->name
changed fromthread->parent.name
, otherwise the compilation will report an error!
The simulation results are as follows:
From the simulation results, we can see that when switching threads, the scheduler hook function is working normally and has been printing thread switching information, including switching to idle threads. You can use scheduler_del
the scheduler hook function to cancel.
Last updated