什么是临界区?
在多线程实时系统中,多个线程操作/访问同一块区域(代码),这块代码就称为临界区
。 例如一项工作中的两个线程:一个线程从传感器中接收数据并且将数据写到共享内存中,同时另一个线程周期性的从共享内存中读取数据并发送去显示,下图描述了两个线程间的数据传递:
如果对共享内存的访问不是排他性的,那么各个线程间可能同时访问它,这将引起数据一致性的问题。例如,在显示线程试图显示数据之前,接收线程还未完成数据的写入,那么显示将包含不同时间采样的数据,造成显示数据的错乱。
临界区的问题
在 RT-Thread 里面,这个临界段最常出现的就是对全局变量的操作。下面我们以一个全局变量为例演示多线程系统中的临界区问题:
#include <rtthread.h>
#include <rtdevice.h>
#include <board.h>
#define TEST1_STACK_SIZE 512
#define TEST1_THREAD_PRIORITY 25
#define TEST1_TIMESLICE 2
#define TEST2_STACK_SIZE 512
#define TEST2_THREAD_PRIORITY 24
#define TEST2_TIMESLICE 1
uint32_t gulTmp = 0;
/* test1线程入口函数 -------------------------------------------------------------------------*/
static void test1_thread_entry(void *parameter)
{
uint16_t i = 0;
rt_kprintf("gulTmp = %d\n", gulTmp);
for (i = 0; i < 10000; i++)
{
gulTmp++;
}
rt_kprintf("gulTmp = %d\n", gulTmp);
}
/* test2线程入口函数 -------------------------------------------------------------------------*/
static void test2_thread_entry(void *parameter)
{
rt_thread_delay(1);
gulTmp++;
}
/* 主函数 ----------------------------------------------------------------------------------*/
int main(void)
{
/* 定义线程句柄 */
rt_thread_t tid;
/* 创建动态test1线程 :优先级 25 ,时间片2个系统滴答,线程栈512字节 */
tid = rt_thread_create("test1_thread",
test1_thread_entry,
RT_NULL,
TEST1_STACK_SIZE,
TEST1_THREAD_PRIORITY,
TEST1_TIMESLICE);
/* 创建成功则启动动态线程 */
if (tid != RT_NULL)
{
rt_thread_startup(tid);
}
/* 创建动态test1线程 :优先级 24 ,时间片1个系统滴答,线程栈512字节 */
tid = rt_thread_create("test2_thread",
test2_thread_entry,
RT_NULL,
TEST2_STACK_SIZE,
TEST2_THREAD_PRIORITY,
TEST2_TIMESLICE);
/* 创建成功则启动动态线程 */
if (tid != RT_NULL)
{
rt_thread_startup(tid);
}
return 0;
}
创建两个测试线程test1、test2,这两个线程都有对全局变量gulTmp
进行+1操作,编译、下载、运行得到的结果为:
我们期望gulTmp的结果为10000,而实际得到的gulTmp的值却为10001,那是因为test2线程的优先级高于test1线程,因此test2线程优先执行,test2线程首先挂起1个时间片,test2挂起期间内核调度执行test1线程,在1个时间片之后,test2线程被唤醒,此时test2线程的优先级最高的,test2线程打断test1线程,所以最终临界区变量gulTmp的结果为10001。
从以上结果中可以看到, 当公共资源在多个线程中公用时,如果缺乏必要的保护错误,最后的输出结果可能与预期的结果完全不同。
IPC机制
为了解决这样的问题,RT-Thread引入了IPC机制 (Inter-Process Communication):
其核心思想都是: 在访问临界区的时候只允许一个 (或一类) 线程运行。进入/退出临界区的方法有:关闭中断
和调度器上锁
。 我们可通过这两种简单的途径来禁止系统调度,防止线程被打断,从而保证临界区不被破坏。
1、关闭中断
线程中关闭中断保护临界区的结构如下:
void test1_thread_entry(void* parameter)
{
rt_base_t level;
while(1)
{
/* 关闭中断*/
level = rt_hw_interrupt_disable();
/* 以下是临界区*/
. . . .
/* 关闭中断*/
rt_hw_interrupt_enable(level);
}
}
所有线程的调度都是建立在中断的基础上的,拿 CM3 核来举例:在 cm3 处理器上,所有的调度条件满足后(不管是在任务还是在中断中)系统会触发pendsv 中断, 在 pensv 中断中去执行调度工作。所以,当我们关闭中断后,系统将不能再进行调度,线程自身也自然不会被其他线程抢占了。
2、调度器上锁
锁住调度器以保护临界区的结构如下:
void test1_thread_entry(void* parameter)
{
. . .
while(1)
{
/* 调度器上锁,上锁后,将不再切换到其他线程,仅响应中断 */
rt_enter_critical();
/* 以下进入临界区 */
. . . .
/* 调度器解锁 */
rt_exit_critical();
}
}
把调度器锁住也能让当前运行的任务不被换出,直到调度器解锁。但和关闭中断有一点不相同的是,对调度器上锁,系统依然能响应外部中断,中断服务例程依然有可能被运行。所以在使用调度器上锁的方式来做任务同步时,需要考虑好, 任务访问的临界资源是否会被中断服务例程所修改,如果可能会被修改,那么将不适合采用此种方式作为同步的方法。