Linux线程同步与互斥(上)

目录

前言

1.互斥

1.先来见一种现象(数据不一致问题)

2.如何解决上述问题

3.理解为什么数据会不一致&&认识加锁的接口

4.理解锁

5.锁的封装


前言

在前面对线程的概念和控制的学习过程中,我们知道了线程是共享地址空间的,也就是会共享大部分资源,那么这个时候就会产生新的问题------并发访问,最直观的感受就是每次运行得出的结果值大概率不一致,这种执行结果不一致的现象是非常致命,因为它具有随机性,即结果可能是对的,也可能是错的,无法可靠的完成任务

为了解决这一问题,我们要引入新的解决方案------同步和互斥,我们先来讲互斥!

1.互斥

1.先来见一种现象(数据不一致问题)

• ⼤部分情况,线程使⽤的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程⽆法获得这种变量。

• 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。

• 多个线程并发的操作共享变量,会带来⼀些问题,比如说下面的一段模拟抢票的实验代码

cpp 复制代码
// 操作共享变量会有问题的售票系统代码
#include <iostream>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
​
int ticket = 100;
​
void *route(void *arg)
{
    char *id = (char *)arg;
    while (1)
    {
        if (ticket > 0) // 1.判断
        {
            usleep(1000);                               // 模拟抢票化的时间
            printf("%s sells ticket:%d\n", id, ticket); // 2.模拟抢到了票
            ticket--;                                   // 3.票数--
        }
        else
        {
            break;
        }
    }
    return nullptr;
}
​
int main(void)
{
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, NULL, route, (void *)"thread 1");
    pthread_create(&t2, NULL, route, (void *)"thread 2");
    pthread_create(&t3, NULL, route, (void *)"thread 3");
    pthread_create(&t4, NULL, route, (void *)"thread 4");
​
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
}

可以看到结果都把票干到负数了,这在现实中可是一件很糟糕的事情,比如说高铁明明只有200个座位,却有201的人抢到了票,这个人是没有位置的,说明多个线程并发的操作共享变量,会带来⼀些问题

2.如何解决上述问题

上面的代码中

临界区:

cpp 复制代码
while (1)
    {
        if (ticket > 0) // 1.判断
        {
            usleep(1000);                               // 模拟抢票化的时间
            printf("%s sells ticket:%d\n", id, ticket); // 2.模拟抢到了票
            ticket--;                                   // 3.票数--
        }
        else
        {
            break;
        }
    }

共享资源是:int ticket =1000;

其他代码都属于非临界区

我们要想办法保护临界区:通过在临界区中前后加锁可以保护起来!

cpp 复制代码
#include <iostream>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
​
int ticket = 100;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 对锁进行初始化
​
void *route(void *arg)
{
    char *id = (char *)arg;
    while (1)
    {
        pthread_mutex_lock(&lock);
        if (ticket > 0) // 1.判断
        {
            usleep(1000);                               // 模拟抢票化的时间
            printf("%s sells ticket:%d\n", id, ticket); // 2.模拟抢到了票
            ticket--;                                   // 3.票数--
            pthread_mutex_unlock(&lock);
        }
        else
        {
            pthread_mutex_unlock(&lock);
            break;
        }
    }
    return nullptr;
}
​
int main(void)
{
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, NULL, route, (void *)"thread 1");
    pthread_create(&t2, NULL, route, (void *)"thread 2");
    pthread_create(&t3, NULL, route, (void *)"thread 3");
    pthread_create(&t4, NULL, route, (void *)"thread 4");
​
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
}

可以从结果看到,此时就不会出现票数为负的情况了,顺利解决数据不一致的问题

3.理解为什么数据会不一致&&认识加锁的接口

首先我们需要知道的是ticket--不是原子性的操作,它会被汇编代码转换成三条指令

• load :将共享变量ticket从内存加载到寄存器中

• update : 更新寄存器⾥⾯的值,执⾏-1操作

• store :将新值,从寄存器写回共享变量ticket的内存地址

比如:

0xFF00 载入 ebx ticket

0xFF02 减少 ebx 1

0xFF04 写回 0x1111 ebx

假设我们有A、B两线程,ticket初始是100,在cpu调度A线程执行到0xFF04时要发生线程切换,此时需要保存A的上下文数据:ebx(ticket)为99,cpu的pc指针保存0xFF04地址,然后cpu开始调度B线程,B线程运气很好,在循环执行让ticket减到1之后刚好才要被切换,保存上下文之后cpu又重新调度A,此时pc指针保存的0xFF04地址是要执行写回内存的指令,那么这个时候的ticket又回到了99,这就发生了数据不一致问题,也说明了ticket--不是原子性的操作

\^\] 我们暂时这么去理解原子性:一条汇编就是原子的 我们上面的票数减到负数其实主要的问题不是出在ticket--这个操作,而是出战if条件判断ticket\>0这一操作上,对于ticket值是否大于0做判断也是一种计算(**逻辑计算,得到的是布尔值**),执行时先载入cpu,再判断;那么此时如果有3个线程,ticket此时为1,都完成1的载入后被切走了(因为加了休眠的时间,导致线程没来及做--操作就让下一个线程进来了),后面按顺序唤醒线程时时并行判断都是1就允许进入了,三个线程此时串行载入ticket,执行ticket--然后再写回内存使得ticket此时从1-\>0-\>-1-\>-2就变成-2了 上面的问题告诉了我们:**全局资源没有加保护,可能会有并发问题------线程安全问题**,同时要形成上面的问题需要在多线程中,制造更多的并发、更多的切换,切换的时间点:1.时间片到了 2.阻塞式IO 3.sleep等等...;选择新的线程时间点:从内核态返回用户态的时候,进行检查 要解决以上问题,需要做到三点: > • 代码必须要有互斥⾏为:当代码进⼊临界区执⾏时,不允许其他线程进⼊该临界区。 > > • 如果多个线程同时要求执⾏临界区的代码,并且临界区没有线程在执⾏,那么只能允许⼀个线程进⼊该临界区。 > > • 如果线程不在临界区中执⾏,那么该线程不能阻⽌其他线程进⼊临界区 **要做到这三点,本质上就是需要⼀把锁 ------pthread_mutex_t(互斥锁/互斥量)** ![屏幕截图 2025-06-16 165944](https://i-blog.csdnimg.cn/img_convert/35d88f2301fe28a67dfa5bea47b7e221.png) \[\^\] pthread_mutex_init的第二个参数为锁属性,我们不用管设为nullptr就行 加锁规则:尽量加锁的范围粒度要比较细,尽可能不要包含太多的非临界区代码 ![image-20250616161349990](https://i-blog.csdnimg.cn/img_convert/c17bfc73423257760ee83b3c72c30437.png) **对临界区进行保护本质其实就是用锁来对临界区进行保护** 问题1:如果有线程不遵守我们的规则,那就是一个bug,所有线程必须遵守!! 问题2:枷锁之后,在临界区内部允许线程切换吗?切换了会怎么样? 答:允许切换,但是不会怎么样,因为我当前线程并没有释放锁,该线程持有锁被切换, 其他线程也必须等我被切换回来执行完代码、释放锁了才能展开申请锁的竞争,进而 进入临界区(当然这样就会导致多线程执行代码的速度变慢) ![image-20250616165319481](https://i-blog.csdnimg.cn/img_convert/acb6e0cecf71f728f359da57db89d5c7.png) **加锁和解锁的本质就是把整个代码块进行原子化,让其他无法中断该线程** ### 4.理解锁 经过上⾯的例⼦,⼤家已经意识到单纯的 i++或者 ++i都不是原⼦的,有可能会有数据⼀致性问题 锁的原理: 1. 硬件级实现:关闭时钟中断 2. 软件级实现: 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令(只有一条指令保证原子性),该指令的作用是把寄存器和内存单元的数据相交换 下面是一段锁在汇编的伪代码: ![image-20250616193123421](https://i-blog.csdnimg.cn/img_convert/5b44700f5b4a106d83faf3983eef0dd8.png) ![image-20250616191606705](https://i-blog.csdnimg.cn/img_convert/ce4b092a1617ed16cd1d31404a88b313.png) ### 5.锁的封装 其实在c++中用锁很简单,我们只需要包含#include\头文件,然后定义一个锁被封装好的mutex类的对象,然后就可以用这个对象调用这个mutex类中的lock、unlock接口实现申请锁和解锁等操作啦(我们其实在c++阶段是学过的) ![image-20250616195740905](https://i-blog.csdnimg.cn/img_convert/6297401367823791fa320478587f151d.png) 使用c++封装的锁来解决我们上面的抢票数据不一致问题代码: ```cpp #include #include #include #include #include #include ​ int ticket = 100; // pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 对锁进行初始化 std::mutex lock; ​ void *route(void *arg) { char *id = (char *)arg; while (1) { // pthread_mutex_lock(&lock); lock.lock(); if (ticket > 0) // 1.判断 { usleep(1000); // 模拟抢票化的时间 printf("%s sells ticket:%d\n", id, ticket); // 2.模拟抢到了票 ticket--; // 3.票数-- // pthread_mutex_unlock(&lock); lock.unlock(); } else { // pthread_mutex_unlock(&lock); lock.unlock(); break; } } return nullptr; } ​ int main(void) { pthread_t t1, t2, t3, t4; pthread_create(&t1, NULL, route, (void *)"thread 1"); pthread_create(&t2, NULL, route, (void *)"thread 2"); pthread_create(&t3, NULL, route, (void *)"thread 3"); pthread_create(&t4, NULL, route, (void *)"thread 4"); ​ pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_join(t3, NULL); pthread_join(t4, NULL); } ``` ![](https://i-blog.csdnimg.cn/direct/17e686ffb1f84710b73e33b4b63fa722.png) **我们当然也可以自己造个轮子,也跟着封装一个我们自己的锁** Mutex.hpp ```cpp #pragma once #include #include ​ namespace MutexModle { class Mutex { public: Mutex() { pthread_mutex_init(&_mutex, nullptr); } ​ // 申请锁 void Lock() { // pthread_mutex_lock成功返回0,失败返回错误码 int n = pthread_mutex_lock(&_mutex); if (n != 0) { std::cerr << "申请锁失败" << std::endl; return; } } ​ // 解锁 void Unlock() { int n = pthread_mutex_unlock(&_mutex); if (n != 0) { std::cerr << "解锁失败" << std::endl; return; } } ​ ~Mutex() { pthread_mutex_destroy(&_mutex); } ​ private: pthread_mutex_t _mutex; }; ​ // 实现RAII风格的互斥锁 class LockGuard { public: LockGuard(Mutex &mutex) : _mutex(mutex) { _mutex.Lock(); } ​ ~LockGuard() { _mutex.Unlock(); } ​ private: Mutex &_mutex; }; } ``` TestMutex.cc ```cpp #include #include #include #include #include "Mutex.hpp" using namespace MutexModle; ​ int ticket = 100; ​ // 我们自己封装的锁类 Mutex lock; ​ void *route(void *arg) { char *id = (char *)arg; while (1) { // 申请锁 // lock.Lock(); // 通过LockGuard类构造对象调用构造函数中的申请锁代码实现自动加锁 // 这就是RAII风格的互斥锁的实现 LockGuard guard(lock); ​ if (ticket > 0) // 1.判断 { usleep(1000); // 模拟抢票化的时间 printf("%s sells ticket:%d\n", id, ticket); // 2.模拟抢到了票 ticket--; // 3.票数-- // 解锁 // lock.Unlock(); // 通过guard临时对象出作用域会自动调用析构函数进行自动解锁 } else { // lock.Unlock(); // 通过guard临时对象出作用域会自动调用析构函数进行自动解锁 break; } } return nullptr; } ​ int main(void) { pthread_t t1, t2, t3, t4; pthread_create(&t1, NULL, route, (void *)"thread 1"); pthread_create(&t2, NULL, route, (void *)"thread 2"); pthread_create(&t3, NULL, route, (void *)"thread 3"); pthread_create(&t4, NULL, route, (void *)"thread 4"); ​ pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_join(t3, NULL); pthread_join(t4, NULL); ​ return 0; } ``` 结果当然也是显而易见的成功解决数据不一致问题啦! ![image-20250616204109527](https://i-blog.csdnimg.cn/img_convert/7b3601abdd2ccd521abb5f3a13feeb5c.png) 我们上面其实实现了RAII风格(智能指针就是利用这个思想的)的互斥锁