RT-THREAD 内核快速入门(一)线程
RT-THREAD 内核快速入门(二)定时器
RT-THREAD 内核快速入门(四)邮箱,消息队列,信号
RT-THREAD 内核快速入门(五)内存管理与中断管理
基于STM32Cubemx移植Rt-thread-nano
这是这个系列的第三篇,内核快速入门之线程同步,将学习信号量,互斥量,事件对线程进行同步,核心都是围绕这线程的就绪与挂起这两个关键词进行学习,对线程进行管理。
在裸机编程中,我们常使用全局变量标志位对不同的函数进行通知,这种方式特别好理解。但是在多个文件中,有太多全局
不好管理,标志位多了管理会变得比较麻烦,实时性也不高。解决这个问题,我们使用线程间同步来解决这个问题。
信号量是在两个线程中进行同步的方式,就是发送信号的意思。比如简单的收发场景,线程1得到一个数据,需要通知线程2去发送这个数据。但是线程2需要等待线程1得到数据才能发送。
直观进行感受
最简单的信号量,线程1每次对变量+1便通知线程2发送
/*
* 程序清单:最简单的信号量
*
* 线程1每次对变量+1便通知线程2发送
*
*/
#include
#define THREAD_PRIORITY 25
#define THREAD_TIMESLICE 5
/* 指向信号量的指针 */
static rt_sem_t dynamic_sem = RT_NULL;
static rt_uint8_t count = 0;
ALIGN(RT_ALIGN_SIZE)
static char thread1_stack[1024];
static struct rt_thread thread1;
static void rt_thread1_entry(void *parameter)
{
while(1)
{
count++;
rt_kprintf("thread1 sent count\n");
rt_sem_release(dynamic_sem);
rt_thread_mdelay(2000);
}
}
ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
static void rt_thread2_entry(void *parameter)
{
rt_err_t result;
while(1)
{
/* 永久方式等待信号量,获取到信号量,则执行number自加的操作 */
result = rt_sem_take(dynamic_sem, RT_WAITING_FOREVER);
if (result != RT_EOK)
{
rt_kprintf("t2 take a dynamic semaphore, failed.\n");
rt_sem_delete(dynamic_sem);
return;
}
else
{
rt_kprintf("thread2 get count = %d\n",count);
}
}
}
/* 信号量示例的初始化 */
int semaphore_sample()
{
/* 创建一个动态信号量,初始值是0 */
dynamic_sem = rt_sem_create("dsem", 0, RT_IPC_FLAG_FIFO);
if (dynamic_sem == RT_NULL)
{
rt_kprintf("create dynamic semaphore failed.\n");
return -1;
}
else
{
rt_kprintf("create done. dynamic semaphore value = 0.\n");
}
rt_thread_init(&thread1,
"thread1",
rt_thread1_entry,
RT_NULL,
&thread1_stack[0],
sizeof(thread1_stack),
THREAD_PRIORITY-1, THREAD_TIMESLICE);
rt_thread_startup(&thread1);
rt_thread_init(&thread2,
"thread2",
rt_thread2_entry,
RT_NULL,
&thread2_stack[0],
sizeof(thread2_stack),
THREAD_PRIORITY, THREAD_TIMESLICE);
rt_thread_startup(&thread2);
return 0;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(semaphore_sample, semaphore sample);
可以看到线程1优先级比线程2要高,线程2想要运行必须处在就绪状态和线程1需要挂起。从输出的信息可以看见,线程2得不到信号量会挂起,直到线程1释放信号量,线程2处于就绪状态,等待被执行。线程1主动挂起,线程2运行。
通过这个例子就知道一个非常重要的运行规律,理解好这个规律,后面的线程同步与通信就可以很快进行上手。这个规律就是这幅图,这幅图真的十分重要。这里我们用信号量对这系统运行状态图进行分析。
分析前先对之前线程进行一个简单的回顾:
回顾:
rt-thread的运行调度方式是基于优先级的全抢占式多线程调度算法,即在系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的,包括线程调度器自身。
系统根据优先级进行运行,优先级高的得到优先执行权,同优先级按照时间片进行轮流执行。
分析:
分析使用刚刚上面的例子帮助大家理解,线程1优先级比线程2优先级要高,线程1只要不挂起,线程2就不能得到CPU的控制权,就得不到运行。在刚刚的例子中,线程1每次对变量+1便通知线程2发送。线程1先得到运行,之后线程释放信号量,可以简要的这样理解(不够准确,不影响使用),释放信号量之后,就使得线程2从挂起状态转到就绪状态,线程1便挂起。线程2得到控制权,从挂起的位置,也就是获取信号量的地方运行,打印变量。直到第二次运行,信号量值为0(初始化为0,线程1只是发送了一次信号量,使得信号量+1),线程2得不到信号量便挂起,直到下一次线程1释放信号量,线程2转变为就绪状态开始执行获取信号量。
还有一个需要注意的就是初始化的时候,是RT_IPC_FLAG_FIFO还是RT_IPC_FLAG_PRIO方式,这是在多个线程同一个获取信号量的场景,都是针对线程间排队的方式,影响谁先获取同步信号得到激活。
第二种好理解:RT_IPC_FLAG_PRIO方式
线程是通过排队的方式获取的,优先级高的先进行排队,优先级高的等待线程将先获得等待的信号量。当释放信号量的时候先,线程按照优先级排队,优先级高的获取信号量,转变为就绪状态,得到执行。
第一种:RT_IPC_FLAG_FIFO(先进先出方式)
当选择 RT_IPC_FLAG_FIFO,那么等待线程队列将按照先进先出的方式排队,先进入的线程将先获得等待的信号量;
推荐使用第一种方式:第一种是实时调度,更利于使用分析以及调试。
多线程获取同一信号量例程:
/*
* 程序清单:多线程获取同一信号量情况
*
* RT_IPC_FLAG_PRIO 模式,线程1释放信号量,线程2和3获取信号量
*
*/
#include
#define THREAD_PRIORITY 25
#define THREAD_TIMESLICE 5
/* 指向信号量的指针 */
static rt_sem_t dynamic_sem = RT_NULL;
static rt_uint8_t count = 0;
ALIGN(RT_ALIGN_SIZE)
static char thread1_stack[1024];
static struct rt_thread thread1;
static void rt_thread1_entry(void *parameter)
{
while(1)
{
count++;
rt_kprintf("thread1 sent count\n");
rt_sem_release(dynamic_sem);
rt_sem_release(dynamic_sem);
rt_thread_mdelay(2000);
}
}
ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
static char thread3_stack[1024];
static struct rt_thread thread3;
static void rt_thread2_entry(void *parameter)
{
rt_err_t result;
while(1)
{
/* 永久方式等待信号量,获取到信号量,则执行number自加的操作 */
result = rt_sem_take(dynamic_sem, RT_WAITING_FOREVER);
if (result != RT_EOK)
{
rt_kprintf("t2 take a dynamic semaphore, failed.\n");
rt_sem_delete(dynamic_sem);
return;
}
else
{
rt_kprintf("thread2 get count = %d\n",count);
rt_thread_mdelay(5);/*高优先级线程休眠,让低优先级线程运行*/
}
}
}
static void rt_thread3_entry(void *parameter)
{
rt_err_t result;
while(1)
{
/*低优先级得到信号量*/
result = rt_sem_take(dynamic_sem, RT_WAITING_FOREVER);
if (result != RT_EOK)
{
rt_kprintf("t2 take a dynamic semaphore, failed.\n");
rt_sem_delete(dynamic_sem);
return;
}
else
{
rt_kprintf("thread3 get count = %d\n",count);
}
}
}
/* 信号量示例的初始化 */
int semaphore_sample()
{
/* 创建一个动态信号量,初始值是0 */
dynamic_sem = rt_sem_create("dsem", 0, RT_IPC_FLAG_PRIO);
if (dynamic_sem == RT_NULL)
{
rt_kprintf("create dynamic semaphore failed.\n");
return -1;
}
else
{
rt_kprintf("create done. dynamic semaphore value = 0.\n");
}
rt_thread_init(&thread1,
"thread1",
rt_thread1_entry,
RT_NULL,
&thread1_stack[0],
sizeof(thread1_stack),
THREAD_PRIORITY-1, THREAD_TIMESLICE);
rt_thread_startup(&thread1);
rt_thread_init(&thread2,
"thread2",
rt_thread2_entry,
RT_NULL,
&thread2_stack[0],
sizeof(thread2_stack),
THREAD_PRIORITY, THREAD_TIMESLICE);
rt_thread_startup(&thread2);
rt_thread_init(&thread3,
"thread3",
rt_thread3_entry,
RT_NULL,
&thread3_stack[0],
sizeof(thread3_stack),
THREAD_PRIORITY+1, THREAD_TIMESLICE);
rt_thread_startup(&thread3);
return 0;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(semaphore_sample, semaphore sample);
发送通知信号:
这里就比较广了,上面的简单例子就是,又比如传感器线程获取数据,之后通过信号量通知线程处理。
锁:
其实不常用,常用互斥量作为锁,很少用信号量,因为会产生优先级翻转的问题,稍后描述。可以通过锁对需要操作的数据上锁,当线程在访问变量的时候,操作这些锁住的变量(称为临界区)。
生产者,消费者问题:
解决间线程速度不匹配问题,其问题描述是生产与消费速度不匹配的问题。
这里给出一个较为复杂的例程:生产者消费者问题,理解好这个线程就大概能够使用了。
例程:这里使用RTT官方例程,自己结合上面的理解啦,这里就不详细阐述了。大概就是,生产线程会根据还有多少个位置进行产苹果,每次有一个空位,就成产一个苹果(随便啦)。同时还有在生存的时候防备消费者偷吃,因此在生产的时候对生产的苹果进行保护(上锁),在生产完毕之后通知消费者去吃。由于消费者吃的比较慢,生产比较快,因此会生产这会先填满空位,之后消费者就慢慢吃,直到直到打烊(消费者线程退出)。
/*
* Copyright (c) 2006-2018, RT-Thread Development Team
*
* SPDX-License-Identifier: Apache-2.0
*
* Change Logs:
* Date Author Notes
* 2018-08-24 yangjie the first version
*/
/*
* 程序清单:生产者消费者例子
*
* 这个例子中将创建两个线程用于实现生产者消费者问题
*(1)生产者线程将cnt值每次加1并循环存入array数组的5个成员内;
*(2)消费者线程将生产者中生产的数值打印出来,并累加求和
*/
#include
#define THREAD_PRIORITY 6
#define THREAD_STACK_SIZE 512
#define THREAD_TIMESLICE 5
/* 定义最大5个元素能够被产生 */
#define MAXSEM 5
/* 用于放置生产的整数数组 */
rt_uint32_t array[MAXSEM];
/* 指向生产者、消费者在array数组中的读写位置 */
static rt_uint32_t set, get;
/* 指向线程控制块的指针 */
static rt_thread_t producer_tid = RT_NULL;
static rt_thread_t consumer_tid = RT_NULL;
struct rt_semaphore sem_lock;
struct rt_semaphore sem_empty, sem_full;
/* 生产者线程入口 */
void producer_thread_entry(void *parameter)
{
int cnt = 0;
/* 运行10次 */
while (cnt < 10)
{
/* 获取一个空位 */
rt_sem_take(&sem_empty, RT_WAITING_FOREVER);
/* 修改array内容,上锁 */
rt_sem_take(&sem_lock, RT_WAITING_FOREVER);
array[set % MAXSEM] = cnt + 1;
rt_kprintf("the producer generates a number: %d\n", array[set % MAXSEM]);
set++;
rt_sem_release(&sem_lock);
/* 修改array内容,解锁 */
/* 发布一个满位 */
rt_sem_release(&sem_full);
cnt++;
/* 暂停一段时间 */
rt_thread_mdelay(20);
}
rt_kprintf("the producer exit!\n");
}
/* 消费者线程入口 */
void consumer_thread_entry(void *parameter)
{
rt_uint32_t sum = 0;
while (1)
{
/* 获取一个满位 */
rt_sem_take(&sem_full, RT_WAITING_FOREVER);
/* 临界区,上锁进行操作 */
rt_sem_take(&sem_lock, RT_WAITING_FOREVER);
sum += array[get % MAXSEM];
rt_kprintf("the consumer[%d] get a number: %d\n", (get % MAXSEM), array[get % MAXSEM]);
get++;
rt_sem_release(&sem_lock);
/* 释放一个空位 */
rt_sem_release(&sem_empty);
/* 生产者生产到10个数目,停止,消费者线程相应停止 */
if (get == 10) break;
/* 暂停一小会时间 */
rt_thread_mdelay(50);
}
rt_kprintf("the consumer sum is: %d\n", sum);
rt_kprintf("the consumer exit!\n");
}
int producer_consumer(void)
{
set = 0;
get = 0;
/* 初始化3个信号量 */
rt_sem_init(&sem_lock, "lock", 1, RT_IPC_FLAG_FIFO);
rt_sem_init(&sem_empty, "empty", MAXSEM, RT_IPC_FLAG_FIFO);
rt_sem_init(&sem_full, "full", 0, RT_IPC_FLAG_FIFO);
/* 创建生产者线程 */
producer_tid = rt_thread_create("producer",
producer_thread_entry, RT_NULL,
THREAD_STACK_SIZE,
THREAD_PRIORITY - 1, THREAD_TIMESLICE);
if (producer_tid != RT_NULL)
{
rt_thread_startup(producer_tid);
}
else
{
rt_kprintf("create thread producer failed");
return -1;
}
/* 创建消费者线程 */
consumer_tid = rt_thread_create("consumer",
consumer_thread_entry, RT_NULL,
THREAD_STACK_SIZE,
THREAD_PRIORITY + 1, THREAD_TIMESLICE);
if (consumer_tid != RT_NULL)
{
rt_thread_startup(consumer_tid);
}
else
{
rt_kprintf("create thread consumer failed");
return -1;
}
return 0;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(producer_consumer, producer_consumer sample);
互斥量使用和信号量使用起来方式几乎一样,只需要直到它的最常用的场景和他和信号量的区别,由于比较相似,仅对它们的不同做讨论。
不同处:
信号量是多值信号,而互斥量是二值信号,只有0和1也就是说只能获取一次,在释放后只能获取一次。
临界锁
互斥量常用在临界锁,对多个线程需要访问的同一块数据,容易出现的优先级翻转的问题和数据不一致性和不完整性的问题。也就是虽然信号量也能够作为锁,但是会出现优先级翻转问题。
优先级翻转理解:
举一个比如了解优先级翻转的例子,有三个人A,B,C,代表三个线程,A优先级高是老大哥,B优先级中等是2哥(划水的),C优先级低是小弟。A,B,C需要一起干活组装一辆自行车,规则是只有C组装完A才能开始组装,B不干活,划水的。有一个场景:C在组装最后一步的时候,因为某些事情,被B打断了(C挂起了),等到B打断事情结束(B不访问临界区,划水的),到A组装了(A优先级高,A先运行),由于C没完成组装(临界区上锁,C正在访问临界区,正在组装自行车),A需要等待C完成才能组装(A因此挂起),终于又轮到C了,C完成(临界区解锁),A才能组装自行车,完成组装自行车的第一步。发现了吗?A优先级高反而得不到运行,C得到运行,组装效率下降了,老大哥A吃亏了。这就是优先级翻转的问题,如果记住规则就好理解了。
互斥锁:
互斥锁是解决优先级翻转的一个总要的方式(注意中断里面不能使用,等到后面篇章解释RTOS的线程调度的原理就知道了)
在刚刚的例子中,将信号量(临界锁)换成互斥量会变怎么样。继续用刚刚的例子进行解释,用了互斥量就相当于C在组装车的时候变成老大哥,它的优先级与老大哥优先级是一样的(A),这样。回到刚刚被B打断的时候,由于C在组装车的时候,相当于老大哥,优先级比B高,B不能打断C组装自行车,因此C能组装,A也能够顺利组装自行车,最后再到B划水。B不在组装自行车的时候捣乱,组装自行车的效率就高了,优先级也没翻转。
理解完上面的例子:让我们看代码实现,我就不对例程进行分析了,因为以及足够理解了。
例子:
#include
/* 指向线程控制块的指针 */
static rt_thread_t tid1 = RT_NULL;
static rt_thread_t tid2 = RT_NULL;
static rt_thread_t tid3 = RT_NULL;
static rt_mutex_t mutex = RT_NULL;
#define THREAD_PRIORITY 10
#define THREAD_STACK_SIZE 512
#define THREAD_TIMESLICE 5
/* 线程 1 入口 */
static void thread1_entry(void *parameter)
{
/* 先让低优先级线程运行 */
rt_thread_mdelay(100);
/* 此时 thread3 持有 mutex,并且 thread2 等待持有 mutex */
/* 检查 thread2 与 thread3 的优先级情况 */
if (tid2->current_priority != tid3->current_priority)
{
/* 优先级不相同,测试失败 */
rt_kprintf("the priority of thread2 is: %d\n", tid2->current_priority);
rt_kprintf("the priority of thread3 is: %d\n", tid3->current_priority);
rt_kprintf("test failed.\n");
return;
}
else
{
rt_kprintf("the priority of thread2 is: %d\n", tid2->current_priority);
rt_kprintf("the priority of thread3 is: %d\n", tid3->current_priority);
rt_kprintf("test OK.\n");
}
}
/* 线程 2 入口 */
static void thread2_entry(void *parameter)
{
rt_err_t result;
rt_kprintf("the priority of thread2 is: %d\n", tid2->current_priority);
/* 先让低优先级线程运行 */
rt_thread_mdelay(50);
/*
* 试图持有互斥锁,此时 thread3 持有,应把 thread3 的优先级提升
* 到 thread2 相同的优先级
*/
result = rt_mutex_take(mutex, RT_WAITING_FOREVER);
if (result == RT_EOK)
{
/* 释放互斥锁 */
rt_mutex_release(mutex);
}
}
/* 线程 3 入口 */
static void thread3_entry(void *parameter)
{
rt_tick_t tick;
rt_err_t result;
rt_kprintf("the priority of thread3 is: %d\n", tid3->current_priority);
result = rt_mutex_take(mutex, RT_WAITING_FOREVER);
if (result != RT_EOK)
{
rt_kprintf("thread3 take a mutex, failed.\n");
}
/* 做一个长时间的循环,500ms */
tick = rt_tick_get();
while (rt_tick_get() - tick < (RT_TICK_PER_SECOND / 2)) ;
rt_mutex_release(mutex);
}
int pri_inversion(void)
{
/* 创建互斥锁 */
mutex = rt_mutex_create("mutex", RT_IPC_FLAG_PRIO);
if (mutex == RT_NULL)
{
rt_kprintf("create dynamic mutex failed.\n");
return -1;
}
/* 创建线程 1 */
tid1 = rt_thread_create("thread1",
thread1_entry,
RT_NULL,
THREAD_STACK_SIZE,
THREAD_PRIORITY - 1, THREAD_TIMESLICE);
if (tid1 != RT_NULL)
rt_thread_startup(tid1);
/* 创建线程 2 */
tid2 = rt_thread_create("thread2",
thread2_entry,
RT_NULL,
THREAD_STACK_SIZE,
THREAD_PRIORITY, THREAD_TIMESLICE);
if (tid2 != RT_NULL)
rt_thread_startup(tid2);
/* 创建线程 3 */
tid3 = rt_thread_create("thread3",
thread3_entry,
RT_NULL,
THREAD_STACK_SIZE,
THREAD_PRIORITY + 1, THREAD_TIMESLICE);
if (tid3 != RT_NULL)
rt_thread_startup(tid3);
return 0;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(pri_inversion, prio_inversion sample);
运行结果:
事件比上面的信号量和互斥量都好理解,事件可以处理但事件和并发事件。若有一线程:A,A只有遇到事件c和d或者e才去处理。这里比较简单,仅有一点需要注意,事件发生多次只要不去处理,也仅作为一次。由于相对于比较简单,直接上例程,就不分析啦。
例程:也是RTT官网的例程
/*
* Copyright (c) 2006-2018, RT-Thread Development Team
*
* SPDX-License-Identifier: Apache-2.0
*
* Change Logs:
* Date Author Notes
* 2018-08-24 yangjie the first version
*/
/*
* 程序清单:事件例程
*
* 程序会初始化2个线程及初始化一个静态事件对象
* 一个线程等待于事件对象上,以接收事件;
* 一个线程发送事件 (事件3/事件5)
*/
#include
#define THREAD_PRIORITY 9
#define THREAD_TIMESLICE 5
#define EVENT_FLAG3 (1 << 3)
#define EVENT_FLAG5 (1 << 5)
/* 事件控制块 */
static struct rt_event event;
ALIGN(RT_ALIGN_SIZE)
static char thread1_stack[1024];
static struct rt_thread thread1;
/* 线程1入口函数 */
static void thread1_recv_event(void *param)
{
rt_uint32_t e;
/* 第一次接收事件,事件3或事件5任意一个可以触发线程1,接收完后清除事件标志 */
if (rt_event_recv(&event, (EVENT_FLAG3 | EVENT_FLAG5),
RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR,
RT_WAITING_FOREVER, &e) == RT_EOK)
{
rt_kprintf("thread1: OR recv event 0x%x\n", e);
}
rt_kprintf("thread1: delay 1s to prepare the second event\n");
rt_thread_mdelay(1000);
/* 第二次接收事件,事件3和事件5均发生时才可以触发线程1,接收完后清除事件标志 */
if (rt_event_recv(&event, (EVENT_FLAG3 | EVENT_FLAG5),
RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR,
RT_WAITING_FOREVER, &e) == RT_EOK)
{
rt_kprintf("thread1: AND recv event 0x%x\n", e);
}
rt_kprintf("thread1 leave.\n");
}
ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
/* 线程2入口 */
static void thread2_send_event(void *param)
{
rt_kprintf("thread2: send event3\n");
rt_event_send(&event, EVENT_FLAG3);
rt_thread_mdelay(200);
rt_kprintf("thread2: send event5\n");
rt_event_send(&event, EVENT_FLAG5);
rt_thread_mdelay(200);
rt_kprintf("thread2: send event3\n");
rt_event_send(&event, EVENT_FLAG3);
rt_kprintf("thread2 leave.\n");
}
int event_sample(void)
{
rt_err_t result;
/* 初始化事件对象 */
result = rt_event_init(&event, "event", RT_IPC_FLAG_FIFO);
if (result != RT_EOK)
{
rt_kprintf("init event failed.\n");
return -1;
}
rt_thread_init(&thread1,
"thread1",
thread1_recv_event,
RT_NULL,
&thread1_stack[0],
sizeof(thread1_stack),
THREAD_PRIORITY - 1, THREAD_TIMESLICE);
rt_thread_startup(&thread1);
rt_thread_init(&thread2,
"thread2",
thread2_send_event,
RT_NULL,
&thread2_stack[0],
sizeof(thread2_stack),
THREAD_PRIORITY, THREAD_TIMESLICE);
rt_thread_startup(&thread2);
return 0;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(event_sample, event sample);
总结它们之间的联系(异同),就能很快掌握后面的线程同步与通信,下面简单介绍。
又是这张图,这张图,看多看两次都不为过,结合这样图总结一下它们直接的异同。
不同处:
最为核心就是理解好信号量,因为设计线程的运行状态的改变,理解好这个,其他的就可以进行迁移。其次就是注意使用场景了,不在中断获取,每个同步信号的大概使用场景,一定场景下它们能够互相替换,找到最合适当前场景的即可。