欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Rt-Thread学习笔记-----信号量(五)

程序员文章站 2024-02-22 18:32:04
...

线程间同步

1、什么是线程间同步?
同步是指按预定的先后次序进行运行,线程同步是指多个线程通过特定的机制来控制线程之间的执行顺序,也可以说是在线程之间通过同步建立起执行顺序的关系,如果没有同步,那线程之间将是无序的。

每一个服务进程的运行,都包含若干进程(Thread),线程是调度的基本单位,进程则是资源拥有的基本单位。线程有自己的私有数据,比如栈和寄存器,同时与其它线程共享相同的虚拟内存和全局变量等资源,当多个线程同时读写同一份共享资源的时候,会引起冲突,这时候就需要引入线程同步机制使各个线程排队一个一个的对共享资源进行操作,而不是同时进行。
Rt-Thread学习笔记-----信号量(五)
1.线程同步其实实现的是线程排队

2.防止线程同步访问共享资源造成冲突。

3.变量需要同步,常量不需要(常量存放于方法区)。

4.多个线程访问共享资源的代码有可能是同一份代码,也有可能是不同的代码;无论是否执行同一份代码,只要这些线程的代码访问同一份可变的共享资源,这些线程之间就需要同步。

2、为什么要进行线程间同步?
例如一项工作中的两个线程:一个线程从传感器中接收数据并且将数据写到共享内存中,同时另一个线程周期性的从共享内存中读取数据并发送去显示,下图描述了两个线程间的数据传递:
Rt-Thread学习笔记-----信号量(五)
如果对共享内存的访问不是排他性的,那么各个线程间可能同时访问它,这将引起数据一致性的问题。例如,在显示线程试图显示数据之前,接收线程还未完成数据的写入,那么显示将包含不同时间采样的数据,造成显示数据的错乱。将传感器数据写入到共享内存块的接收线程 #1 和将传感器数据从共享内存块中读出的线程 #2 都会访问同一块内存。为了防止出现数据的差错,两个线程访问的动作必须是互斥进行的,应该是在一个线程对共享内存块操作完成后,才允许另一个线程去操作,这样,接收线程 #1 与显示线程 #2 才能正常配合,使此项工作正确地执行。

在多线程实时系统中,一项工作的完成往往可以通过多个线程协调的方式共同来完成,那么多个线程之间如何 “默契” 协作才能使这项工作无差错执行?下面举个例子说明。

例如一项工作中的两个线程:一个线程从传感器中接收数据并且将数据写到共享内存中,同时另一个线程周期性的从共享内存中读取数据并发送去显示,下图描述了两个线程间的数据传递:

信号量基本概念

信号量(Semaphore)是一种实现线程间通信的机制,实现线程之间同步或临界资源的互斥访问,常用于协助一组相互竞争的线程来访问临界资源。在多线程系统中,各线程之间需要同步或互斥实现临界资源的保护,信号量功能可以为用户提供这方面的支持。
在操作系统中,我们使用信号量的目的是为了给临界资源建立一个标志,信号量表示了该临界资源被占用情况。这样,当一个线程在访问临界资源的时候,就会先对这个资源信息进行查询,从而在了解资源被占用的情况之后,再做处理,从而使得临界资源得到有效的保护。

**PS:**相当于在裸机编程中这样使用过一个变量:用于标记某个事件是否发生,或者标志一下某个东西是否正在被使用,如果是被占用了的或者没发生,我们就不对它进行操作。(类似Flag标志位)

信号量分为二值及计数型。

二值信号量:
在嵌入式操作系统中二值信号量是线程间、线程与中断间同步的重要手段。为什么叫二值信号量呢?因为信号量资源被获取了,信号量值就是 0,信号量资源被释放,信号量值就是 1,把这种只有 0和 1 两种情况的信号量称之为二值信号量。

比如某个线程需要等待一个标记,那么线程可以在轮询中查询这个标记有没有被置位,这样子做,就会很消耗 CPU 资源,其实根本不需要在轮询中查询这个标记,只需要使用二值信号量即可,当二值信号量没有的时候,线程进入阻塞态等待二值信号量到来即可,当得到了这个信号量(标记)之后,在进行线程的处理即可,这样子么就不会消耗太多资源了,而且实时响应也是最快的。

再比如某个线程使用信号量在等中断的标记的发生,在这之前线程已经进入了阻塞态,在等待着中断的发生,当在中断发生之后,释放一个信号量,也就是我们常说的标记,当它退出中断之后,操作系统进行线程的调度,如果这个线程能够运行,系统就会把等待这个线程运行起来,这样子就大大提高了我们的效率。

举例说明来理解二值信号量:
1、线程与线程之间同步
有两个线程任务T1、T2。T1为温度传感器1S采集一次外界温度。T2为LCD显示采集到的温度,理论上也是1S变化一次温度值。如果LCD的屏幕刷新率为10ms,那么此时的温湿度的数据还没更新,液晶屏根本无需刷新,只需要在 1s 后温湿度数据更新的时候刷新即可。那么在裸机中,经常会用到轮询方式,即一直在刷新LCD屏幕,即使没有到下一个温度值到来,再不需要更新数值时,也要刷新。这样CPU资源会被LCD刷新占有,影响资源的合理使用。

如果液晶屏刷新的周期是 10s 更新一次,那么温湿度的数据都变化了 10 次,液晶屏才来更新数据,那拿这个产品有啥用,根本就是不准确的,所以,还是需要同步协调工作,在温湿度采集完毕之后,进行液晶屏数据的刷新,这样子,才是最准确的,并且不会浪费 CPU的资源。

2、线程与中断之间同步
二值信号量在线程与中断同步的应用场景:我们在串口接收中,我们不知道啥时候有数据发送过来,有一个线程是做接收这些数据处理,总不能在线程中每时每刻都在线程查询有没有数据到来,那样会浪费 CPU 资源,所以在这种情况下使用二值信号量是很好的办法,当没有数据到来的时候,线程就进入阻塞态,不参与线程的调度,等到数据到来了,释放一个二值信号量,线程就立即从阻塞态中解除,进入就绪态,然后运行的时候处理数据,这样子系统的资源就会很好的被利用起来。

信号量接口函数

1、创建和删除信号量

//创建
 rt_sem_t rt_sem_create(const char *name,
                        rt_uint32_t value,
                        rt_uint8_t flag);
//删除
rt_err_t rt_sem_delete(rt_sem_t sem);

当调用信号量创建函数时,系统将先从对象管理器中分配一个 semaphore 对象,并初始化这个对象,然后初始化父类 IPC 对象以及与 semaphore 相关的部分。在创建信号量指定的参数中,信号量标志参数决定了当信号量不可用时,多个线程等待的排队方式。当选择 RT_IPC_FLAG_FIFO(先进先出)方式时,那么等待线程队列将按照先进先出的方式排队,先进入的线程将先获得等待的信号量;当选择 RT_IPC_FLAG_PRIO(优先级等待)方式时,等待线程队列将按照优先级进行排队,优先级高的等待线程将先获得等待的信号量。下表描述了该函数的输入参数与返回值:
Rt-Thread学习笔记-----信号量(五)
2、初始化和脱离信号量
初始化
对于静态信号量对象,它的内存空间在编译时期就被编译器分配出来,放在读写数据段或未初始化数据段上,此时使用信号量就不再需要使用 rt_sem_create 接口来创建它,而只需在使用前对它进行初始化即可。初始化信号量对象可使用下面的函数接口:

rt_err_t rt_sem_init(rt_sem_t       sem,
                    const char     *name,
                    rt_uint32_t    value,
                    rt_uint8_t     flag)

当调用这个函数时,系统将对这个 semaphore 对象进行初始化,然后初始化 IPC 对象以及与 semaphore 相关的部分。信号量标志可用上面创建信号量函数里提到的标志。下表描述了该函数的输入参数与返回值:
Rt-Thread学习笔记-----信号量(五)
脱离信号量就是让信号量对象从内核对象管理器中脱离,适用于静态初始化的信号量。脱离信号量使用下面的函数接口:

rt_err_t rt_sem_detach(rt_sem_t sem);

使用该函数后,内核先唤醒所有挂在该信号量等待队列上的线程,然后将该信号量从内核对象管理器中脱离。原来挂起在信号量上的等待线程将获得 - RT_ERROR 的返回值。下表描述了该函数的输入参数与返回值:
Rt-Thread学习笔记-----信号量(五)
3、获取信号量
线程通过获取信号量来获得信号量资源实例,当信号量值大于零时,线程将获得信号量,并且相应的信号量值会减 1,获取信号量使用下面的函数接口:

rt_err_t rt_sem_take (rt_sem_t sem, rt_int32_t time);

在调用这个函数时,如果信号量的值等于零,那么说明当前信号量资源实例不可用,申请该信号量的线程将根据 time 参数的情况选择直接返回、或挂起等待一段时间、或永久等待,直到其他线程或中断释放该信号量。如果在参数 time 指定的时间内依然得不到信号量,线程将超时返回,返回值是 - RT_ETIMEOUT。下表描述了该函数的输入参数与返回值:
Rt-Thread学习笔记-----信号量(五)
4、释放信号量
释放信号量可以唤醒挂起在该信号量上的线程。释放信号量使用下面的函数接口:

rt_err_t rt_sem_release(rt_sem_t sem);

例如当信号量的值等于零时,并且有线程等待这个信号量时,释放信号量将唤醒等待在该信号量线程队列中的第一个线程,由它获取信号量;否则将把信号量的值加 1。下表描述了该函数的输入参数与返回值:
Rt-Thread学习笔记-----信号量(五)

二值信号量的运作机制

创建二值信号量,为创建的信号量对象分配内存,并把可用信号量初始化为用户自定义的个数, 二值信号量的最大可用信号量个数为 1。

信号量获取,从创建的信号量资源中获取一个信号量,获取成功返回正确。否则线程会等待其它线程释放该信号量,超时时间由用户设定。当线程获取信号量失败时,线程将进入阻塞态,系统将线程挂到该信号量的阻塞列表中。在二值信号量无效的时候,假如此时有线程获取该信号量的话,那么线程将进入阻塞状态,具体见图
Rt-Thread学习笔记-----信号量(五)

假如某个时间中断/线程释放了信号量,其过程具体见图 2,那么,由于获取无效信号量而进入阻塞态的线程将获得信号量并且恢复为就绪态,其过程具体见图3
Rt-Thread学习笔记-----信号量(五)

二值信号量同步实验

信号量同步实验是在 RT-Thread 中创建了两个线程,一个是获取信号量线程,一个是释放互斥量线程,两个线程独立运行,获取信号量线程是一直在等待信号量,其等待时间是 RT_WAITING_FOREVER,等到获取到信号量之后,线程处理完毕时它又马上释放信号量。

编写thread1和thread2的入口函数。Thread1以RT_WAITING_FOREVER的方式获取一个信号量,操作完后进程内容释放信号量。此时while循环应该继续运行的,但是由于thread1释放信号量之后,thread2以RT_WAITING_FOREVER的方式获取到了信号量,使信号量的value为0,thread1便停止在rt_sem_take(dynamic_sem,RT_WAITING_FOREVER)处。Thread2操作完进程内容之后,释放了一个信号量,Thread1获取到该信号量,继续运行。

释放互斥量线程利用延时模拟占用信号量,延时的这段时间,获取线程无法获得信号量,等到释放线程使用完信号量,然后释放信号量,此时释放信号量会唤醒获取线程,获取线程开始运行,然后形成两个线程间的同步,若是线程正常同步,则在串口打印出信息

/*
******************************************************************
*                           二值信号量部分
******************************************************************
*/
/*
******************************************************************
*                               变量
******************************************************************
*/
/* 定义线程控制块 */
static rt_thread_t receive_thread = RT_NULL;
static rt_thread_t send_thread = RT_NULL;
/* 定义信号量控制块 */
static rt_sem_t test_sem = RT_NULL;

/************************* 全局变量声明 ****************************/
/*
 * 当我们在写应用程序的时候,可能需要用到一些全局变量。
 */
uint8_t ucValue [ 2 ] = { 0x00, 0x00 };
/*
*************************************************************************
*                             函数声明
*************************************************************************
*/
static void receive_thread_entry(void* parameter);
static void send_thread_entry(void* parameter);
/*
*************************************************************************
*                             线程定义
*************************************************************************
*/

static void receive_thread_entry(void* parameter)
{	
  /* 任务都是一个无限循环,不能返回 */
  while(1)
	{
		rt_sem_take(test_sem,	/* 获取信号量 */
                RT_WAITING_FOREVER); 	/* 等待时间:一直等 */
		if ( ucValue [ 0 ] == ucValue [ 1 ] )
		{ 			
			rt_kprintf ( "Successful\n" );			
		}
		else
		{
			rt_kprintf ( "Fail\n" );			
		}
		rt_sem_release(	test_sem	);   //释放二值信号量 
		
		rt_thread_delay ( 1000 );  					      //每1s读一次		
  }
  
  
}

static void send_thread_entry(void* parameter)
{	
    /* 任务都是一个无限循环,不能返回 */
    while (1)
    {
			rt_sem_take(test_sem,				/* 获取信号量 */
                  RT_WAITING_FOREVER);	 		/* 等待时间:一直等 */		
		ucValue [ 0 ] ++;		
		rt_thread_delay ( 100 );        	 	/* 延时100ms */		
		ucValue [ 1 ] ++;		
		rt_sem_release(	test_sem	);			//释放二值信号量
		rt_thread_yield();  					//放弃剩余时间片,进行一次任务切换	
    }
}


static int Binary_Semaphore(void)
{
    /* 
	 * 开发板硬件初始化,RTT系统初始化已经在main函数之前完成,
	 * 即在component.c文件中的rtthread_startup()函数中完成了。
	 * 所以在main函数中,只需要创建线程和启动线程即可。
	 */
	rt_kprintf("这是一个RTT二值信号量同步实验!\n");
  rt_kprintf("同步成功则输出Successful,反之输出Fail\n");
   /* 创建一个信号量 */
	test_sem = rt_sem_create("test_sem",/* 信号量名字 */
                     1,     /* 信号量初始值,默认有一个信号量 */
                     RT_IPC_FLAG_FIFO); /* 信号量模式 FIFO(0x00)*/
  if (test_sem != RT_NULL)
    rt_kprintf("信号量创建成功!\n\n");
    
	receive_thread =                          /* 线程控制块指针 */
    rt_thread_create( "receive",            /* 线程名字 */
                      receive_thread_entry, /* 线程入口函数 */
                      RT_NULL,              /* 线程入口函数参数 */
                      512,                  /* 线程栈大小 */
                      3,                    /* 线程的优先级 */
                      20);                  /* 线程时间片 */
                   
    /* 启动线程,开启调度 */
   if (receive_thread != RT_NULL)
        rt_thread_startup(receive_thread);
    else
        return -1;
    
  send_thread =                            /* 线程控制块指针 */
    rt_thread_create( "send",              /* 线程名字 */
                      send_thread_entry,   /* 线程入口函数 */
                      RT_NULL,             /* 线程入口函数参数 */
                      512,                 /* 线程栈大小 */
                      2,                   /* 线程的优先级 */
                      20);                 /* 线程时间片 */
                   
    /* 启动线程,开启调度 */
   if (send_thread != RT_NULL)
        rt_thread_startup(send_thread);
    else
        return -1;		
}

下载验证:
Rt-Thread学习笔记-----信号量(五)

计数型信号量的运作机制

计数型信号量与二值信号量其实都是差不多的,一样用于资源保护,不过计数信号量则允许多个线程获取信号量访问共享资源,但会限制线程的最大数目。访问的线程数达到信号量可支持的最大数目时,会阻塞其他试图获取该信号量的线程,直到有线程释放了信号量。这就是计数型信号量的运作机制,虽然计数信号量允许多个线程访问同一个资源,但是也有限定,比如某个资源限定只能有 3 个线程访问,那么第 4 个线程访问的时候,会因为获取不到信号量而进入阻塞,等到有线程(比如线程 1)释放掉该资源的时候,第 4个线程才能获取到信号量从而进行资源的访问,其运作的机制具体见图 19-4。
Rt-Thread学习笔记-----信号量(五)
计数信号量实验
计数型信号量实验是模拟停车场工作运行。在创建信号量的时候初始化 5 个可用的信号量,并且创建了两个线程:一个是获取信号量线程,一个是释放信号量线程,两个线程独立运行,获取信号量线程是通过按下 K1 按键进行信号量的获取,模拟停车场停车操作,其等待时间是 0,在串口调试助手输出相应信息。释放信号量线程则是信号量的释放,释放信号量线程也是通过按下 K2 按键进行信号量的释放,模拟停车场取车操作,在串口调试助手输出相应信息

/*
******************************************************************
*                           计数信号量部分
******************************************************************
*/
/*
******************************************************************
*                               变量
******************************************************************
*/
/* 定义线程控制块 */
static rt_thread_t count_receive_thread = RT_NULL;
static rt_thread_t count_send_thread = RT_NULL;
/* 定义消息队列控制块 */
static rt_sem_t count_test_sem = RT_NULL;

/************************* 全局变量声明 ****************************/
/*
 * 当我们在写应用程序的时候,可能需要用到一些全局变量。
 */
/*
*************************************************************************
*                             函数声明
*************************************************************************
*/
static void count_receive_thread_entry(void* parameter);
static void count_send_thread_entry(void* parameter);

/*
*************************************************************************
*                             线程定义
*************************************************************************
*/

static void count_receive_thread_entry(void* parameter)
{		
  rt_err_t uwRet = RT_EOK;
  /* 任务都是一个无限循环,不能返回 */
  while(1)
	{
		if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )       //如果KEY2被单击
		{
			/* 获取一个计数信号量 */
      uwRet = rt_sem_take(count_test_sem,	
                          0); 	/* 等待时间:0 */
			if ( RT_EOK == uwRet ) 
				rt_kprintf( "KEY1被单击:成功申请到停车位。\n" );
			else
				rt_kprintf( "KEY1被单击:不好意思,现在停车场已满!\n" );							
		}
		rt_thread_delay(20);     //每20ms扫描一次		
  }
}

static void count_send_thread_entry(void* parameter)
{	
	rt_err_t uwRet = RT_EOK;
    /* 任务都是一个无限循环,不能返回 */
  while (1)
  {
		if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON )       //如果KEY2被单击
		{
			/* 释放一个计数信号量 */
			uwRet = rt_sem_release(count_test_sem);    
			if ( RT_EOK == uwRet ) 
				rt_kprintf ( "KEY2被单击:释放1个停车位。\n" );	
			else
				rt_kprintf ( "KEY2被单击:但已无车位可以释放!\n" );					
		}
		rt_thread_delay(20);     //每20ms扫描一次		
  }
}


static int Count_semaphore(void)
{
    /* 
	 * 开发板硬件初始化,RTT系统初始化已经在main函数之前完成,
	 * 即在component.c文件中的rtthread_startup()函数中完成了。
	 * 所以在main函数中,只需要创建线程和启动线程即可。
	 */
	rt_kprintf("这是一个RTT计数信号量实验!\n");
  rt_kprintf("车位默认值为5个,按下K1申请车位,按下K2释放车位!\n\n");
   /* 创建一个信号量 */
	count_test_sem = rt_sem_create("count_test_sem",		/* 计数信号量名字 */
                     5,     								/* 信号量初始值,默认有5个信号量 */
                     RT_IPC_FLAG_FIFO); 					/* 信号量模式 FIFO(0x00)*/
  if (count_test_sem != RT_NULL)
    rt_kprintf("计数信号量创建成功!\n\n");
    
	count_receive_thread =                          /* 线程控制块指针 */
    rt_thread_create( "count_receive",              /* 线程名字 */
                      count_receive_thread_entry,   /* 线程入口函数 */
                      RT_NULL,             			/* 线程入口函数参数 */
                      512,                 			/* 线程栈大小 */
                      3,                   			/* 线程的优先级 */
                      20);                 			/* 线程时间片 */
                   
    /* 启动线程,开启调度 */
   if (count_receive_thread != RT_NULL)
        rt_thread_startup(count_receive_thread);
    else
        return -1;
    
  count_send_thread =                          	  /* 线程控制块指针 */
    rt_thread_create( "count_send",               /* 线程名字 */
                      count_send_thread_entry,    /* 线程入口函数 */
                      RT_NULL,             		  /* 线程入口函数参数 */
                      512,                 		  /* 线程栈大小 */
                      2,                   		  /* 线程的优先级 */
                      20);                	      /* 线程时间片 */
                   
    /* 启动线程,开启调度 */
   if (count_send_thread != RT_NULL)
        rt_thread_startup(count_send_thread);
    else
        return -1;	
}

Rt-Thread学习笔记-----信号量(五)
rtthread信号量工程

总结

信号量:信号量是用来解决线程同步和互斥的通用工具,和互斥量类似,信号量也可用作资源互斥访问,但信号量没有所有者的概念,在应用上比互斥量更广泛。信号量比较简单,不能解决优先级翻转问题,但信号量是一种轻量级的对象,比互斥量小巧、灵活。因此在很多对互斥要求不严格的系统中(或者不会造成优先级翻转的情况下),经常使用信号量来管理互斥资源。简而言之,信号量就是一个信号,类似于我们平常自己设定的标志位。通过这个信号的状态(0或者非0)来表征当前线程的状态(是否可以运行)。每次线程申请一次信号量,信号量变量的数值会减一,反之,释放一个信号量,信号量变量的数值加一。

邮箱:邮箱服务是实时操作系统中一种典型的任务间通信方法,通常开销比较低,效率较高,每一封邮件只能容纳固定的4字节内容(针对32位处理系统,刚好能够容纳一个指针)。这里需要注意的是,邮箱中每一封邮件的容量是有限的,因此,如果信号量小于4个字节,那么可以直接利用邮件传达信息,否则,需要利用邮件传送保存信息的变量指针。

相关标签: RTOS