Linux线程同步与互斥(上)

发布于:2025-08-02 ⋅ 阅读:(13) ⋅ 点赞:(0)

目录

前言

1.互斥

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

2.如何解决上述问题

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

4.理解锁

5.锁的封装


前言

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

image-20250615172706982

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

1.互斥

image-20250615172818085

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

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

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

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

// 操作共享变量会有问题的售票系统代码
#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);
}

image-20250615183136247

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

2.如何解决上述问题

上面的代码中

临界区:

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

共享资源是:int ticket =1000;

其他代码都属于非临界区

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

#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);
}

image-20250615184551620

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

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--不是原子性的操作

image-20250615212831730

[^]  我们暂时这么去理解原子性:一条汇编就是原子的 

我们上面的票数减到负数其实主要的问题不是出在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

[^]  pthread_mutex_init的第二个参数为锁属性,我们不用管设为nullptr就行 

加锁规则:尽量加锁的范围粒度要比较细,尽可能不要包含太多的非临界区代码

image-20250616161349990

对临界区进行保护本质其实就是用锁来对临界区进行保护

问题1:如果有线程不遵守我们的规则,那就是一个bug,所有线程必须遵守!!

问题2:枷锁之后,在临界区内部允许线程切换吗?切换了会怎么样?

答:允许切换,但是不会怎么样,因为我当前线程并没有释放锁,该线程持有锁被切换,

其他线程也必须等我被切换回来执行完代码、释放锁了才能展开申请锁的竞争,进而

进入临界区(当然这样就会导致多线程执行代码的速度变慢)

image-20250616165319481

加锁和解锁的本质就是把整个代码块进行原子化,让其他无法中断该线程

4.理解锁

经过上⾯的例⼦,⼤家已经意识到单纯的 i++或者 ++i都不是原⼦的,有可能会有数据⼀致性问题

锁的原理:

  1. 硬件级实现:关闭时钟中断

  2. 软件级实现:

    为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令(只有一条指令保证原子性),该指令的作用是把寄存器和内存单元的数据相交换

    下面是一段锁在汇编的伪代码:

    image-20250616193123421

image-20250616191606705

5.锁的封装

其实在c++中用锁很简单,我们只需要包含#include<mutex.h>头文件,然后定义一个锁被封装好的mutex类的对象,然后就可以用这个对象调用这个mutex类中的lock、unlock接口实现申请锁和解锁等操作啦(我们其实在c++阶段是学过的)

image-20250616195740905

使用c++封装的锁来解决我们上面的抢票数据不一致问题代码:

#include <iostream>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <mutex>
​
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);
}

我们当然也可以自己造个轮子,也跟着封装一个我们自己的锁

Mutex.hpp

#pragma once
#include <pthread.h>
#include <iostream>
​
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

#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#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

我们上面其实实现了RAII风格(智能指针就是利用这个思想的)的互斥锁


网站公告

今日签到

点亮在社区的每一天
去签到