线程的切换

本文介绍了进程与线程的基本概念,强调了一个程序是进程,进程内可包含多个线程。通过示例代码展示了单线程和多线程环境下变量翻转的差异,解释了线程切换的概念。在多线程代码中,通过rtthread库实现了线程的创建和手动切换,以达到并发执行的效果。线程的使用能够提高程序的执行效率。

什么是线程

首先讲一下进程与线程的区别:

1、一个程序就是一个进程,一个程序中的多个任务称为线程

2、一个线程只能属于一个进程,一个进程可以有多个线程

3、线程是操作系统可识别的最小执行和调度单位

4、多线程可提高效率

线程的切换

首先看一下裸机系统实现两个变量轮流翻转

uint32_t flag1;
uint32_t flag2;

void delay(uint32_t count)
{
	for(; count!=0; count--);
}

int main(void)
{
	for(; ;){
	flag1 = 1;
	delay(100);
	flag1 = 0;
	delay(100);	
		
	flag2 = 1;
	delay(100);
	flag2 = 0;
	delay(100);	
	}
}

flag1、flag2必须定义成全局变量才能添加到逻辑分析仪里面观察波形

flag1、flag2在逻辑分析仪中设置成bit的模式才能看到波形,不能用默认的模拟量

代码仿真图如下:

 在多线程中,两个线程不断切换的效果图如下:

 两个变量的波形是完全一样的,就好像CPU同时在干两件事情一样,这才是多线程的意义。虽然两者的波形图一样,但代码的实现方式完全不一样,由原来的顺序执行变成了主动切换,这是根本区别。

代码如下:

#include <rtthread.h>
#include "ARMCM3.h"


/*
*************************************************************************
*                              全局变量
*************************************************************************
*/
rt_uint8_t flag1;
rt_uint8_t flag2;

extern rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX];

/*
*************************************************************************
*                      线程控制块 & STACK & 线程声明
*************************************************************************
*/


/* 定义线程控制块 */
struct rt_thread rt_flag1_thread;
struct rt_thread rt_flag2_thread;

ALIGN(RT_ALIGN_SIZE)
/* 定义线程栈 */
rt_uint8_t rt_flag1_thread_stack[512];
rt_uint8_t rt_flag2_thread_stack[512];

/* 线程声明 */
void flag1_thread_entry(void *p_arg);
void flag2_thread_entry(void *p_arg);

/*
*************************************************************************
*                               函数声明
*************************************************************************
*/
void delay(uint32_t count);

/************************************************************************
  * @brief  main函数
  * @param  无
  * @retval 无
  *
  * @attention
  *********************************************************************** 
  */
int main(void)
{	
	/* 硬件初始化 */
	/* 将硬件相关的初始化放在这里,如果是软件仿真则没有相关初始化代码 */	
	
	/* 调度器初始化 */
	rt_system_scheduler_init();
	
	
	/* 初始化线程 */
	rt_thread_init( &rt_flag1_thread,                 /* 线程控制块 */
	                flag1_thread_entry,               /* 线程入口地址 */
	                RT_NULL,                          /* 线程形参 */
	                &rt_flag1_thread_stack[0],        /* 线程栈起始地址 */
	                sizeof(rt_flag1_thread_stack) );  /* 线程栈大小,单位为字节 */
	/* 将线程插入到就绪列表 */
	rt_list_insert_before( &(rt_thread_priority_table[0]),&(rt_flag1_thread.tlist) );
	
	/* 初始化线程 */
	rt_thread_init( &rt_flag2_thread,                 /* 线程控制块 */
	                flag2_thread_entry,               /* 线程入口地址 */
	                RT_NULL,                          /* 线程形参 */
	                &rt_flag2_thread_stack[0],        /* 线程栈起始地址 */
	                sizeof(rt_flag2_thread_stack) );  /* 线程栈大小,单位为字节 */
	/* 将线程插入到就绪列表 */
	rt_list_insert_before( &(rt_thread_priority_table[1]),&(rt_flag2_thread.tlist) );
	
	/* 启动系统调度器 */
	rt_system_scheduler_start(); 
}

/*
*************************************************************************
*                               函数实现
*************************************************************************
*/
/* 软件延时 */
void delay (uint32_t count)
{
	for(; count!=0; count--);
}

/* 线程1 */
void flag1_thread_entry( void *p_arg )
{
	for( ;; )
	{
		flag1 = 1;
		delay( 100 );		
		flag1 = 0;
		delay( 100 );
		
		/* 线程切换,这里是手动切换 */		
		rt_schedule();
	}
}

/* 线程2 */
void flag2_thread_entry( void *p_arg )
{
	for( ;; )
	{
		flag2 = 1;
		delay( 100 );		
		flag2 = 0;
		delay( 100 );
		
		/* 线程切换,这里是手动切换 */
		rt_schedule();
	}
}

线程控制块类型声明:

struct rt_thread
{
	void        *sp;	          /* 线程栈指针 */
	void        *entry;	          /* 线程入口地址 */
	void        *parameter;	      /* 线程形参 */	
	void        *stack_addr;      /* 线程起始地址 */
	rt_uint32_t stack_size;       /* 线程栈大小,单位为字节 */
	
	rt_list_t   tlist;            /* 线程链表节点 */
};
typedef struct rt_thread *rt_thread_t;

<think>我们正在讨论线程切换的原理及实现机制。根据用户提供的引用和之前讨论的进程创建线程的机制,我们可以从多个层面来解析线程切换。 首先,用户提供的引用中提到: - 引用[1]:Java线程与系统内核线程是一一对应的,系统内核线程调度器负责调度Java线程线程上下文切换会消耗大量资源。 - 引用[3]:在RTOS(实时操作系统)中,线程的堆栈空间是静态分配的连续数组空间,没有MMU(内存管理单元)的情况下,线程切换需要管理这些堆栈空间。 结合之前讨论的线程创建机制,我们知道每个线程都有自己独立的栈空间和线程控制块(TCB)。线程切换发生在操作系统调度器决定将当前正在运行的线程挂起,并选择另一个就绪线程运行的时候。 ### 线程切换的原理及实现机制 #### 1. **触发线程切换的条件** 线程切换通常由以下事件触发: - **主动让出CPU**:线程调用如`sleep()`, `yield()`, 或等待I/O操作、锁、信号量等。 - **时间片用完**:操作系统的时间片轮转调度,当前线程的时间片耗尽。 - **中断发生**:硬件中断(如时钟中断、I/O中断)导致当前线程被挂起,中断处理程序运行后,调度器可能会选择另一个线程运行。 #### 2. **线程切换的步骤(内核级线程)** 当线程切换发生时,操作系统内核会执行以下步骤(引用[1][3]): 1. **保存当前线程的上下文**:将当前线程的CPU寄存器状态(包括程序计数器PC、栈指针SP、通用寄存器等)保存到该线程的TCB中。 $$ \text{保存上下文: } \text{TCB}_{\text{current}} \leftarrow \{ \text{PC}, \text{SP}, \text{R0}, \text{R1}, \dots \} $$ 2. **选择下一个要运行的线程**:调度器从就绪队列中选择一个优先级最高的线程(根据调度算法)。 3. **加载新线程的上下文**:从新线程的TCB中恢复其寄存器状态,包括PC和SP。 $$ \text{恢复上下文: } \{ \text{PC}, \text{SP}, \text{R0}, \text{R1}, \dots \} \leftarrow \text{TCB}_{\text{next}} $$ 4. **切换堆栈指针**:将栈指针SP切换到新线程的栈空间(因为每个线程有自己的栈)。 5. **跳转到新线程的执行点**:通过恢复的程序计数器PC,CPU开始执行新线程的代码。 #### 3. **上下文切换的开销** 线程切换的主要开销在于保存和恢复寄存器的操作,以及缓存失效(Cache Invalidation)带来的影响。因为线程切换后,CPU缓存中可能还保留着旧线程的数据,新线程需要重新加载指令和数据到缓存,这会导致性能下降(引用[1])。 与进程切换不同,线程切换不需要切换虚拟地址空间(即页表),因为同一进程内的线程共享相同的地址空间(引用[1][3])。因此,线程切换的开销远小于进程切换。 #### 4. **用户级线程与内核级线程切换区别** - **内核级线程**:由操作系统内核管理,线程切换需要陷入内核态(通过系统调用),因此会有用户态到内核态的切换开销(引用[1][4])。 - **用户级线程**:由用户空间的线程库(如Java的虚拟线程)管理,线程切换在用户态完成,无需陷入内核,因此开销更小(引用[1][4])。但是,用户级线程的一个阻塞操作会导致整个进程阻塞,因为内核并不知道用户级线程的存在。 #### 5. **线程切换的硬件支持** 现代CPU通常提供硬件支持来加速上下文切换,例如: - **专用的上下文切换指令**:如x86架构的`fxsave`和`fxrstor`指令用于保存和恢复浮点寄存器状态。 - **多核并行**:多核CPU可以同时运行多个线程,减少切换频率。 #### 6. **线程切换的优化** 为了减少线程切换的开销,可以采取以下措施: - **线程池**:复用已创建的线程,避免频繁创建和销毁线程(引用[2])。 - **设置合理的线程优先级**:避免高优先级线程频繁抢占导致切换。 - **减少锁竞争**:过多的锁竞争会导致线程频繁阻塞和切换。 ```mermaid stateDiagram-v2 [*] --> 运行状态 运行状态 --> 就绪状态 : 时间片用完或被抢占 运行状态 --> 阻塞状态 : 等待I/O或锁 阻塞状态 --> 就绪状态 : I/O完成或锁释放 就绪状态 --> 运行状态 : 被调度器选中 ``` > ⚠️ **注意**:在资源受限的系统(如嵌入式系统)中,线程堆栈溢出是一个常见问题(引用[3])。因此,需要合理分配每个线程的栈大小。 §§相关问题§§ 1. 用户级线程与内核级线程切换开销上有何具体差异? 2. 如何通过线程池减少线程创建和切换的开销?(参考引用[2]) 3. 上下文切换中缓存失效对性能的影响有多大? 4. 实时操作系统(RTOS)中的线程切换有何特殊优化? 5. 线程切换与协程切换的区别是什么?</think>### 线程切换的原理及实现机制详解 #### 1. **切换触发条件** 线程切换由操作系统调度器控制,主要触发场景包括: - **时间片耗尽**:每个线程分配固定CPU时间(通常10-100ms),超时后强制切换[^1] - **主动让出**:线程调用`sleep()`, `yield()`, 或等待I/O/锁 - **高优先级抢占**:高优先级线程就绪时立即切换 - **中断发生**:硬件中断(如时钟中断)导致当前线程挂起 #### 2. **切换核心步骤(内核级线程)** ```mermaid sequenceDiagram participant CPU participant 调度器 participant TCB_old participant TCB_new CPU->>调度器: 切换请求(时间片耗尽/中断) 调度器->>TCB_old: 保存上下文 Note over TCB_old: PC/SP/寄存器→TCB 调度器->>TCB_new: 选择就绪线程 调度器->>TCB_new: 加载上下文 Note over TCB_new: TCB→寄存器/PC/SP 调度器->>CPU: 执行新线程 ``` 具体操作流程: 1. **保存当前线程状态** - 将程序计数器(PC)、栈指针(SP)、通用寄存器值存入当前线程的TCB $$ \text{TCB}_{old} \leftarrow \{ \text{PC}, \text{SP}, \text{R0}, \text{R1}, \dots \} $$ 2. **选择新线程** - 调度器从就绪队列选取优先级最高的线程(如Linux的CFS调度器) 3. **加载新线程状态** - 从新线程的TCB恢复寄存器、PC和SP $$ \{ \text{PC}, \text{SP}, \text{R0}, \text{R1}, \dots \} \leftarrow \text{TCB}_{new} $$ 4. **切换地址空间(可选)** - 若新旧线程属不同进程,需切换页表(线程同进程则跳过此步) #### 3. **关键性能特征** - **切换开销构成**: ```mermaid pie title 线程切换耗时分布 “保存/恢复寄存器” : 45 “缓存失效” : 35 “调度算法计算” : 15 “其他” : 5 ``` - **与进程切换对比**: $$ \frac{\text{线程切换开销}}{\text{进程切换开销}} \approx \frac{1}{10} $$ 因线程切换无需更新页表、文件描述符等[^1] #### 4. **优化技术** 1. **用户态切换** - 协程/虚拟线程在用户态切换(如Java19虚拟线程[^1]),避免内核陷入 ```java // Java虚拟线程示例(避免内核切换) Thread virtualThread = Thread.startVirtualThread(() -> { System.out.println("运行在轻量级线程!"); }); ``` 2. **线程池复用** - 通过复用线程减少创建/销毁开销(引用线程池最佳实践[^2]) ```java ExecutorService pool = Executors.newFixedThreadPool(10); // 固定大小线程池 ``` 3. **缓存亲和性调度** - 将线程绑定到特定CPU核心,减少缓存失效(如Linux的`sched_setaffinity`) #### 5. **特殊场景处理** - **嵌入式系统**:无MMU时需静态分配栈空间,栈溢出风险更高[^3] - **实时系统**:采用优先级继承协议避免优先级反转 - **多核同步**:使用内存屏障指令保证缓存一致性 > 💡 **性能提示**:单核CPU线程数超过活跃核心数2-4倍时,切换开销可能抵消并发收益[^1]。监控工具(如`vmstat`的`cs`字段)可检测切换频率。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值