FreeRTOS学习——同步互斥

FreeRTOS学习------同步互斥

目录

一、概念

1.1 同步

在FreeRTOS中,同步是指任务之间按照某种规则进行协调和按序执行的过程。其目的是保证任务或线程之间的有序交互,使它们能够按照预期的顺序完成各自的操作或实现特定的约束条件。常见的同步场景包括等待其他任务完成、等待某个条件满足、协调任务之间的依赖关系等。

FreeRTOS提供了多种同步机制,例如信号量、互斥量、消息队列等,用于实现任务之间的同步。这些机制可以帮助任务之间进行协作,以确保它们按照一定的顺序、时机和约束进行执行。

同步机制在FreeRTOS中非常重要,因为它们可以确保系统的正确性和稳定性。如果没有同步机制,任务之间可能会出现竞争条件,导致系统行为不可预测。通过使用同步机制,FreeRTOS可以确保任务之间的正确交互,从而提高系统的可靠性和性能。

1.2 互斥

FreeRTOS中,互斥是一种同步机制,用于保护共享资源,确保任务访问这些资源时的原子性,避免数据错误。具体来说,互斥是指在多任务环境中,运行特定代码段时确保数据的一致性和完整性,避免多个任务同时访问和修改共享资源导致错误的发生。它通过互斥量(又称互斥信号量)来实现,互斥量是一种特殊的二值信号量,用于实现对临界资源的独占式处理。任意时刻互斥量的状态只有两种,开锁或闭锁。当互斥量被任务持有时,该互斥量处于闭锁状态,这个任务获得互斥量的所有权。当该任务释放这个互斥量时,该互斥量处于开锁状态,任务失去该互斥量的所有权。当一个任务持有互斥量时,其他任务将不能再对该互斥量进行开锁或持有。持有该互斥量的任务也能够再次获得这个锁而不被挂起,这就是递归访问,也就是递归互斥量的特性。

一句话理解同步与互斥:我等你用完厕所,我再用厕所。

什么叫同步?就是:哎哎哎,我正在用厕所,你等会。

什么叫互斥?就是:哎哎哎,我正在用厕所,你不能进来。

同步与互斥经常放在一起讲,是因为它们之的关系很大, "互斥"操作可以使用"同步"来实现。我"等"你用完厕所,我再用厕所。这不就是用"同步"来实现"互斥"吗?

二、示例------有缺陷的同步

实验目的:计算ul变量累加到1000000需要多长时间

具体实现:

创建2个Task,定义一个全局变量taskFlag,当taskFlag等于1时表示Task1正在运行,当taskFlag等于0时表示Task2正在运行

Task1:

  • Task1中定义一个累加变量ul
  • 第一次运行Task1(开始累加ul)时记录系统此刻tick时间vstartTime
  • 当ul>1000000(ul累加完毕)时记录系统此刻tick时间vendTime
  • 累加完毕后将累加结束标志endFlag置位,将vendTime-vstartTime时间赋值给全局变量vtotleTime

Task2:

  • 判断全局变量endFlag是否置位
  • 若endFlag置位,打印出vtotleTime

实验代码:

c 复制代码
#define mainDELAY_LOOP_COUNT 1000000

volatile TickType_t vtotleTime;
volatile TickType_t vstartTime = 0, vendTime = 0;
volatile bool endFlag = FALSE;
volatile bool endLock = FALSE;
volatile uint8_t taskFlag = 0;

void vTask1( void *pvParameters )
{
	volatile uint32_t ul; /* volatile用来避免被优化掉 */
	
	vstartTime = xTaskGetTickCount();
	/* 打印任务1的信息 */
	printf( "Count start: %d\r\n",vstartTime );	

	/* 任务函数的主体一般都是无限循环 */
	for( ;; )
	{	
		/* 表示Task1在运行 */
		taskFlag = 1;

		/* 延迟一会(比较简单粗暴) */
		if(endLock == FALSE)
		{
			ul++;

			if((ul > mainDELAY_LOOP_COUNT)  && (endFlag != TRUE))
			{
				endFlag = TRUE;
				vendTime = xTaskGetTickCount();
				vtotleTime = vendTime - vstartTime;
				vTaskDelay(10);
			}
		}		
	}
}

void vTask2( void *pvParameters )
{
	volatile uint32_t ul; /* volatile用来避免被优化掉 */
	
	/* 任务函数的主体一般都是无限循环 */
	for( ;; )
	{
		/* 表示Task2在运行 */
		taskFlag = 0;
		if(endFlag == TRUE)
		{
			endFlag = FALSE;
			printf( "Count end: %d\r\nTotle time: %d\r\n",vendTime, vtotleTime);
			endLock = TRUE;	
		}
	}
}

int main( void )
{
	prvSetupHardware();
	
	xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);
	xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, NULL);

	/* 启动调度器 */
	vTaskStartScheduler();

	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
	return 0;
}

运行结果:

实验分析:从taskFlag分析,Task1在累加ul变量时,Task2仍然在运行,仍然耗费CPU资源,理论上分析如果在ul累加期间,使Task2任务挂起,ul从0累加到1000000耗时会减少一半。

三、示例------优化有缺陷的同步

基于"二"中的示例进行优化

优化思路:使用队列通信代替Task2对全局变量endFlag的判断,队列传输数据结构体:

c 复制代码
typedef struct
{
	TickType_t startTime;
	TickType_t endTime;
	TickType_t stopFlag;
}TIME;
  • main中创建一个队列
  • Task2中接收队列,队列中没有数据时Task2阻塞,队列中有数据时打印出endTime-startTime,即ul累加到1000000耗时
  • Task1中累加ul,当ul累加到1000000时将数据结构体通过队列发送给Task2

实验代码:

c 复制代码
#define mainDELAY_LOOP_COUNT 1000000

typedef struct
{
	TickType_t startTime;
	TickType_t endTime;
	TickType_t stopFlag;
}TIME;

void vTask1( void *pvParameters )
{
	TIME time1;
	volatile uint32_t ul; /* volatile用来避免被优化掉 */
	volatile TickType_t buf[10];

	time1.stopFlag = FALSE;
	T1 = xTaskGetTickCount();
	time1.startTime = xTaskGetTickCount();
	/* 打印任务1的信息 */
	printf( "Count start: %d\r\n",time1.startTime);	

	/* 任务函数的主体一般都是无限循环 */
	for( ;; )
	{	
		/* 表示Task1在运行 */
		taskFlag = 1;

		/* 延迟一会(比较简单粗暴) */
			ul++;

			if((ul > mainDELAY_LOOP_COUNT) && time1.stopFlag != TRUE)
			{
				T2 = xTaskGetTickCount();
				time1.endTime = xTaskGetTickCount();
				time1.stopFlag = TRUE;
				xQueueSend(task1Handle, &time1, NULL);
			}		
	}
}

void vTask2( void *pvParameters )
{
	TIME time2;
	volatile uint32_t ul; /* volatile用来避免被优化掉 */

	/* 任务函数的主体一般都是无限循环 */
	for( ;; )
	{
		/* 表示Task2在运行 */
		taskFlag = 0;

		xQueueReceive(task1Handle, &time2, portMAX_DELAY);

		if(time2.stopFlag == TRUE)
		{
			printf( "Count end: %d\r\nTotle time: %d\r\n",time2.endTime, time2.endTime - time2.startTime);
			time2.stopFlag = FALSE;
		}
		

	}
}

int main( void )
{
	prvSetupHardware();
	
	xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);
	xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, NULL);

	task1Handle = xQueueCreate(1,sizeof(TIME));
	/* 启动调度器 */
	vTaskStartScheduler();

	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
	return 0;
}

运行结果:

实验分析:使用队列时,Task2未接收到队列的数据时会进入挂起状态,不会再占用CPU资源,Task1往队列发送数据时,会同时将Task2从挂起状态改变为就绪或者运行状态。

四、示例------有缺陷的互斥

实验目的:不同任务访问相同临界资源(比如全局变量)时有缺陷的互斥

仍然使用"二"中的示例,进行简单的修改,在"二"的代码中Task1累加完ul变量得到累加耗时后使用了vTaskDelay函数使Task1挂起,如果不使用vTaskDelay函数就会出现有缺陷的互斥现象

实验代码:

c 复制代码
#define mainDELAY_LOOP_COUNT 1000000

volatile TickType_t vtotleTime;
volatile TickType_t vstartTime = 0, vendTime = 0;
volatile bool endFlag = FALSE;
volatile bool endLock = FALSE;
volatile uint8_t taskFlag = 0;

void vTask1( void *pvParameters )
{
	volatile uint32_t ul; /* volatile用来避免被优化掉 */
	
	vstartTime = xTaskGetTickCount();
	/* 打印任务1的信息 */
	printf( "Count start: %d\r\n",vstartTime );	

	/* 任务函数的主体一般都是无限循环 */
	for( ;; )
	{	
		/* 表示Task1在运行 */
		taskFlag = 1;

		/* 延迟一会(比较简单粗暴) */
		if(endLock == FALSE)
		{
			ul++;

			if((ul > mainDELAY_LOOP_COUNT)  && (endFlag != TRUE))
			{
				endFlag = TRUE;
				vendTime = xTaskGetTickCount();
				vtotleTime = vendTime - vstartTime;
				//vTaskDelay(10);
			}
		}		
	}
}

void vTask2( void *pvParameters )
{
	volatile uint32_t ul; /* volatile用来避免被优化掉 */
	
	/* 任务函数的主体一般都是无限循环 */
	for( ;; )
	{
		/* 表示Task2在运行 */
		taskFlag = 0;
		if(endFlag == TRUE)
		{
			endFlag = FALSE;
			printf( "Count end: %d\r\nTotle time: %d\r\n",vendTime, vtotleTime);
			endLock = TRUE;	
		}
	}
}

int main( void )
{
	prvSetupHardware();
	
	xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);
	xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, NULL);

	/* 启动调度器 */
	vTaskStartScheduler();

	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
	return 0;
}

运行结果:

实验分析:从运行结果来看,Task2打印了2次,理论上从代码分析,程序运行到Task2的打印时,应该是先将endFlag设置为了FALSE,但是打印了2次说明endFlag的值没有写入成功,单步调试分析一下:

将endFlag值在Watch1中显示,在Task2打印处设置一个断点,全速运行

从代码来看Task1也会修改endFlag的值,在Task1中计算累计时间处再打一个断点,全速运行

再次全速运行到Task2中打印处

再次全速运行再也不会听到断点处,这就是Task2中打印了2次的详细步骤。

缺陷原理:

1、当运行到Task2打印处是,endFlag被更改为FALSE

2、但是Task2还未来得及更改完endLock就切换到了Task1

3、Task1中又将endFlag更改为TRUE

4、切换到Task2时再次打印了一遍

5、这次在切换到Task1之前修改完了endLock的值

6、再切换到Task1时不会再更改endFlag的值了

修改代码逻辑可以避免Task2打印2次:

修改前:

c 复制代码
void vTask2( void *pvParameters )
{
	volatile uint32_t ul; /* volatile用来避免被优化掉 */
	
	/* 任务函数的主体一般都是无限循环 */
	for( ;; )
	{
		/* 表示Task2在运行 */
		taskFlag = 0;
		if(endFlag == TRUE)
		{
			endFlag = FALSE;
			printf( "Count end: %d\r\nTotle time: %d\r\n",vendTime, vtotleTime);
			endLock = TRUE;	
		}
	}
}

修改后:

C 复制代码
void vTask2( void *pvParameters )
{
	volatile uint32_t ul; /* volatile用来避免被优化掉 */
	
	/* 任务函数的主体一般都是无限循环 */
	for( ;; )
	{
		/* 表示Task2在运行 */
		taskFlag = 0;
		if(endFlag == TRUE)
		{
            endLock = TRUE;	
			endFlag = FALSE;
			printf( "Count end: %d\r\nTotle time: %d\r\n",vendTime, vtotleTime);
		}
	}
}

这样修改可以避免Task2打印2次,因为在打印时Task2已经完成了对endLock和endFlag的修改,但是同样存在缺陷,原因是:

C语言中1条给全局变量的赋值语句并不是程序的最小运行单位,C语言的本质是汇编,从Task2的汇编码可以看到,将endFlag的值赋值为0分为3个步骤:

假设在赋值过程中运行完汇编第二步后就切换了任务,其他任务对该临界资源也进行了修改,再切换到当前任务时该被修改的临界资源又被修改了。因此这种修改也并不是万无一失的。

补充一个办法:当前任务需要修改临界资源时,现将系统所有中断关闭,暂停任务调度和中断,修改完临界资源后再将中断恢复,恢复任务调度和中断。

但是这种方法关闭中断也对系统有一定的风险!

五、总结

正确使用互斥与同步,FreeRTOS提供的方法是安全可靠的,比如队列、信号量、互斥量、任务通知等等,就像"三、优化有缺陷的同步"一样,使用FreeRTOS提供的方法同样可以优化有缺陷的互斥。

相关推荐
西岸行者10 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意10 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码10 天前
嵌入式学习路线
学习
毛小茛10 天前
计算机系统概论——校验码
学习
babe小鑫10 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms10 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下10 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。10 天前
2026.2.25监控学习
学习
im_AMBER10 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J10 天前
从“Hello World“ 开始 C++
c语言·c++·学习