FreeRTOS学习——同步互斥

2023-12-16 16:45:12

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

实验代码:

#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的判断,队列传输数据结构体:

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

实验代码:

#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函数就会出现有缺陷的互斥现象
实验代码:

#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次:

修改前:

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;	
		}
	}
}

修改后:

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

在这里插入图片描述

文章来源:https://blog.csdn.net/studyingdda/article/details/135033841
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。