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提供的方法同样可以优化有缺陷的互斥。

相关推荐
知识分享小能手4 小时前
React学习教程,从入门到精通, React 属性(Props)语法知识点与案例详解(14)
前端·javascript·vue.js·学习·react.js·vue·react
茯苓gao6 小时前
STM32G4 速度环开环,电流环闭环 IF模式建模
笔记·stm32·单片机·嵌入式硬件·学习
是誰萆微了承諾7 小时前
【golang学习笔记 gin 】1.2 redis 的使用
笔记·学习·golang
DKPT7 小时前
Java内存区域与内存溢出
java·开发语言·jvm·笔记·学习
aaaweiaaaaaa7 小时前
HTML和CSS学习
前端·css·学习·html
看海天一色听风起雨落8 小时前
Python学习之装饰器
开发语言·python·学习
speop10 小时前
llm的一点学习笔记
笔记·学习
非凡ghost10 小时前
FxSound:提升音频体验,让音乐更动听
前端·学习·音视频·生活·软件需求
ue星空10 小时前
月2期学习笔记
学习·游戏·ue5
萧邀人10 小时前
第二课、熟悉Cocos Creator 编辑器界面
学习