同步与互斥(一)

目录

 一、举例理解

二、概念

三、互斥锁

四、互斥量的适用场合

五、互斥量函数

5.1 创建

5.2 其他函数

六、互斥量的基本使用


 一、举例理解

          一句话理解同步与互斥:我等你用完厕所,我再用厕所。
        什么叫同步?就是:哎哎哎,我正在用厕所,你等会。
        什么叫互斥?就是:哎哎哎,我正在用厕所,你不能进来。

        同步与互斥经常放在一起讲,是因为它们之的关系很大,“互斥”操作可以使用“同步”来实现。我“等”你用完厕所,我再用厕所。这不就是用“同步”来实现“互斥”吗?

        再举一个例子。在团队活动里,同事A先写完报表,经理B才能拿去向领导汇报。经理B必须等同事A完成报表,AB之间有依赖,B必须放慢脚步,被称为同步。在团队活动中,同事A已经使用会议室了,经理B也想使用,即使经理B是领导,他也得等着,这就叫互斥。经理B跟同事A说:你用完会议室就提醒我。这就是使用"同步"来实现"互斥"。

        假设有A、B两人早起抢厕所,A先行一步占用了;B慢了一步,于是就眯一会;当A用完后叫醒B,B也就愉快地上厕所了。
        在这个过程中,A、B是互斥地访问“厕所”,“厕所”被称之为临界资源。我们使用了“休眠-唤醒”的同步机制实现了“临界资源”的“互斥访问”。
        同一时间只能有一个人使用的资源,被称为临界资源。

        比如任务A、B都要使用串口来打印,串口就是临界资源。如果A、B同时使用串口,那么打印出来的信息就是A、B混杂,无法分辨。所以使用串口时,应该是这样:A用完,B再用;B用完,A再用。

二、概念

        现代操作系统基本都是多任务操作系统,即同时有大量可调度实体在运行。在多任务操作系统中,同时运行的多个任务可能:

  • 都需要访问/使用同一种资源
  • 多个任务之间有依赖关系,某个任务的运行依赖于另一个任务。

【同步】:

  是指散步在不同任务之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。最基本的场景就是:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。比如 A 任务的运行依赖于 B 任务产生的数据。

【互斥】:

  是指散步在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。最基本的场景就是:一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。

怎么独享厕所?自己开门上锁,完事了自己开锁。
        你当然可以进去后,让别人帮你把门:但是,命运就掌握在别人手上了。
使用队列、信号量,都可以实现互斥访问,以信号量为例:
        信号量初始值为1,任务A想上厕所,"take"信号量成功,它进入厕所,任务B也想上厕所,"take"信号量不成功,等待,任务A用完厕所,"give"信号量;轮到任务B使用。

这需要有2个前提:
        任务B很老实,不撬门(一开始不"give"信号量)
        没有坏人:别的任务不会"give"信号量
        可以看到,使用信号量确实也可以实现互斥访问,但是不完美。

使用互斥量可以解决这个问题,互斥量的名字取得很好:
        量:值为0、1
        互斥:用来实现互斥访问
        它的核心在于:谁上锁,就只能由谁开锁。
        很奇怪的是,
FreeRTOS的互斥锁,并没有在代码上实现这点:即使任务A获得了互斥锁,任务B竟然也可以释放互斥锁。谁上锁、谁释放:只是约定。

三、互斥锁

        在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源。这个过程有点类似于,公司部门里,我在使用着打印机打印东西的同时(还没有打印完),别人刚好也在此刻使用打印机打印东西,如果不做任何处理的话,打印出来的东西肯定是错乱的。
 在线程里也有这么一把锁——互斥锁(mutex),互斥锁是一种简单的加锁的方法来控制对共享资源的访问,互斥锁只有两种状态,即上锁( lock )和解锁( unlock )。

多线程的同步与互斥(互斥锁、条件变量、读写锁、自旋锁、信号量)_条件变量和互斥锁-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/daaikuaichuan/article/details/82950711【互斥锁的特点】:

        1. 原子性:把一个互斥量锁定为一个原子操作,这意味着操作系统(或pthread函数库)保证了如果一个线程锁定了一个互斥量,没有其他线程在同一时间可以成功锁定这个互斥量;

        2. 唯一性:如果一个线程锁定了一个互斥量,在它解除锁定之前,没有其他线程可以锁定这个互斥量;

        3. 非繁忙等待:如果一个线程已经锁定了一个互斥量,第二个线程又试图去锁定这个互斥量,则第二个线程将被挂起(不占用任何cpu资源),直到第一个线程解除对这个互斥量的锁定为止,第二个线程则被唤醒并继续执行,同时锁定这个互斥量。

【互斥锁的操作流程如下】:

1. 在访问共享资源后临界区域前,对互斥锁进行加锁;

2. 在访问完成后释放互斥锁导上的锁;

3. 对互斥锁进行加锁后,任何其他试图再次对互斥锁加锁的线程将会被阻塞,直到锁被释放。

四、互斥量的适用场合

        在多任务系统中,任务A正在使用某个资源,还没用完的情况下任务B也来使用的话,就可能导致问题。
        比如对于串口,任务A正使用它来打印,在打印过程中任务B也来打印,客户看到的结果就是A、B的信息混杂在一起。
        这种现象很常见:
4.1 访问外设:刚举的串口例子

4.2 读、修改、写操作导致的问题
        对于同一个变量,比如int a ,如果有两个任务同时写它就有可能导致问题。
        对于变量的修改,C代码只有一条语句,比如: a=a+8; ,它的内部实现分为3步:读出原值、修改、写入。

        我们想让任务A、B都执行add_a函数,a的最终结果是1+8+8=17 。
        假设任务A运行完代码①,在执行代码②之前被任务B抢占了:现在任务A的R0等于1。
        任务B执行完add_a函数,a等于9。
        任务A继续运行,在代码②处R0仍然是被抢占前的数值1,执行完②③的代码,a等于9,这跟预期的17不符合。

4.3 对变量的非原子化访问
        修改变量、设置结构体、在16位的机器上写32位的变量,这些操作都是非原子的。也就是它们的操作过程都可能被打断,如果被打断的过程有其他任务来操作这些变量,就可能导致冲突。

4.4 函数重入
        "可重入的函数"是指:多个任务同时调用它、任务和中断同时调用它,函数的运行也是安全的。可重入的函数也被称为"线程安全"(thread safe)。
        每个任务都维持自己的栈、自己的CPU寄存器,如果一个函数只使用局部变量,那么它就是线程安全的。
        函数中一旦使用了全局变量、静态变量、其他外设,它就不是"可重入的",如果改函数正在被调用,就必须阻止其他任务、中断再次调用它。

        上述问题的解决方法是:任务A访问这些全局变量、函数代码时,独占它,就是上个锁。这些全局变量、函数代码必须被独占地使用,它们被称为临界资源。

        互斥量也被称为互斥锁,使用过程如下:
        互斥量初始值为1
        任务A想访问临界资源,先获得并占有互斥量,然后开始访问
        任务B也想访问临界资源,也要先获得互斥量:被别人占有了,于是阻塞
        任务A使用完毕,释放互斥量;任务B被唤醒、得到并占有互斥量,然后开始访问临界资源
        任务B使用完毕,释放互斥量
        正常来说:在任务A占有互斥量的过程中,任务B、任务C等等,都无法释放互斥量。
        但是FreeRTOS未实现这点:任务A占有互斥量的情况下,任务B也可释放互斥量。

五、互斥量函数

5.1 创建

互斥量是一种特殊的二进制信号量。
使用互斥量时,先创建、然后去获得、释放它。使用句柄来表示一个互斥量。
创建互斥量的函数有2种:动态分配内存,静态分配内存,函数原型如下:

/* 创建一个互斥量,返回它的句柄。
* 此函数内部会分配互斥量结构体
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateMutex( void );
/* 创建一个互斥量,返回它的句柄。
* 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateMutexStatic( StaticSemaphore_t *pxMutexBuffer
);

要想使用互斥量,需要在配置文件FreeRTOSConfig.h中定义:

#define configUSE_MUTEXES 1

5.2 其他函数

        要注意的是,互斥量不能在ISR中使用。各类操作函数,比如删除、give/take,跟一般信号量是一样的。

/*
* xSemaphore: 信号量句柄,你要删除哪个信号量, 互斥量也是一种信号量
*/
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );
/* 释放 */
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );
/* 释放(ISR版本) */
BaseType_t xSemaphoreGiveFromISR(
            SemaphoreHandle_t xSemaphore,
            BaseType_t *pxHigherPriorityTaskWoken);
/* 获得 */
BaseType_t xSemaphoreTake(
            SemaphoreHandle_t xSemaphore,
            TickType_t xTicksToWait);
/* 获得(ISR版本) */
xSemaphoreGiveFromISR(
            SemaphoreHandle_t xSemaphore,
            BaseType_t *pxHigherPriorityTaskWoken);

六、互斥量的基本使用

        “信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作(大家都在semtake的时候,就阻塞在 哪里)。而互斥锁是用在多线程多任务互斥的,一个线程占用了某一个资源,那么别的线程就无法访问,直到这个线程unlock,其他的线程才开始可以利用这个资源。比如对全局变量的访问,有时要加锁,操作完了,在解锁。有的时候锁和信号量会同时使用的”
        也就是说,信号量不一定是锁定某一个资源,而是流程上的概念,比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。而线程互斥量则是“锁住某一资源”的概念,在锁定期间内,其他线程无法对被保护的数据进行操作。在有些情况下两者可以互换。

        互斥锁的本意是谁上锁谁解锁,但是这纯靠你自觉,FreeRTOS中并没有强制规定。

使用互斥量时有如下特点:
        刚创建的互斥量可以被成功"take"
        "take"互斥量成功的任务,被称为"holder",只能由它"give"互斥量;别的任务"give"不成功
在ISR中不能使用互斥量
        本程序创建2个发送任务:故意发送大量的字符。可以做2个实验:
        使用互斥量:可以看到任务1、任务2打印的字符串没有混杂在一起
        不使用互斥量:任务1、任务2打印的字符串混杂在一起

/* 互斥量句柄 */
SemaphoreHandle_t xMutex;
int main( void )
{
    prvSetupHardware();
    /* 创建互斥量 */
    xMutex = xSemaphoreCreateMutex( );
    if( xMutex != NULL )
    {
        /* 创建2个任务: 都是打印
        * 优先级相同
        */
        xTaskCreate( vSenderTask, "Sender1", 1000, (void *)1, 1, NULL );
        xTaskCreate( vSenderTask, "Sender2", 1000, (void *)2, 1, NULL );
        /* 启动调度器 */
        vTaskStartScheduler();
    }
    else
    {
        /* 无法创建互斥量 */
    }
    /* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
    return 0;
}
static void vSenderTask( void *pvParameters )
{
    const TickType_t xTicksToWait = pdMS_TO_TICKS( 10UL );
    static int cnt = 0;
    int task = (int)pvParameters;
    int i;
    char c;
    /* 无限循环 */
    for( ;; )
    {
        /* 获得互斥量: 上锁 */
        xSemaphoreTake(xMutex, portMAX_DELAY);
        printf("Task %d use UART count: %d, ", task, cnt++);
        c = (task == 1 ) ? 'a' : 'A';
        for (i = 0; i < 26; i++)
            printf("%c", c + i);
        printf("\r\n");
        /* 释放互斥量: 开锁 */
        xSemaphoreGive(xMutex);
        vTaskDelay(xTicksToWait);
    }
}

        可以做两个实验:vSenderTask函数的for循环中xSemaphoreTake和xSemaphoreGive这2句代码保留、不保留.
        保留:实验现象如下图1,任务1、任务2的打印信息没有混在一起.
        不保留:实验现象如下图2,打印信息混杂在一起.

如果屏蔽vTaskDelay(),Task1就会一直没有机会。

 在我之前写的FreeRTOS任务中有这样一句话可以解释:

        如果把任务3中的vTaskDelay调用注释掉,那么任务1、任务2根本没有执行的机会,任务1、任务2被"饿死"了(starve)。

当然修改优先级也会造成任务Sender2一直运行: 

 改变优先级也是如此:

 加入修改优先级vTaskPrioritySet修改优先级改成4打印情况如下:

注意这个延时时间,太短了,就算改变了优先级,先执行这个Sender1,等他执行完有一个Delay,但是如果这个时间很短,导致其他任务还是进不来。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值