Linux 线程互斥

1.相关概念

临界资源:多线程执行流共享的资源就叫做临界资源

临界区:每个线程内部,访问临界资源的代码,就叫做临界区

互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用

原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。

2. 使用多线程模拟抢票(问题引出)

在日常生活中,有很多关于抢票的时候,那么这时候票就是共享资源,我们去抢这些票,我们就相当于是一个一个的线程需要去抢这些票,对于有限的资源来说,线程之间也是有竞争存在的。

代码示例:设置线程为10个,每个线程都在0.01秒后就对ticket--,当票对于0的时候就线程就退出。

cpp 复制代码
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<vector>
#include<unistd.h>
#include<pthread.h>
using namespace std;
#define NUM 10
int ticket=1000;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               
struct ThreadData
{
public:
    ThreadData(int num)
    {
        threadname="thread-"+to_string(num);
    }
public:
    string threadname;
};
void*GetTicket(void*args)
{
    ThreadData*td=static_cast<ThreadData*>(args);
    const char*name=td->threadname.c_str();
    while(true)
    {
        if(ticket>0)
        {
            usleep(10000);
            printf("who=%s,get a ticket%d\n",name,ticket);
            ticket--;
        }
        else
        {
            break;
        }
    }
    delete td;
    return nullptr;
}
int main()
{
    //创建多线程
    vector<pthread_t> tids;
    for(int i=0;i<NUM;i++)
    {
        pthread_t tid;
        ThreadData*td=new ThreadData(i);
        pthread_create(&tid,nullptr,GetTicket,td);
        tids.push_back(tid);
    }
    for(auto i:tids)
    {
        pthread_join(i,nullptr);
    }
    return 0;
}

运行结果:

奇怪的事情发生了,不应该啊,票对于0的时候,就不应该在执行ticket--的操作了啊,为什么ticket最后成了负数,怎么还是有线程在ticket等于0的时候执行了ticket--的操作。

2.1 解答问题(提出互斥)

1.首先我们知道这10个线程都处于一个进程中,所以对于ticket这个变量,所有的线程都看得到的,也可以对变量进行操作,这个变量就是被多个线程共享的共享资源。

2.每一个线程都是有时间片的,当时间片到了就会从cpu上面剥离下来,保存上下文,将其他线程放入到cpu上面执行。

3.变量的++,--操作在代码上面是好像是只有一行代码,但是在cpu的眼里面,要执行对变量++或者--的操作是3次操作的。

也就是如上的3步。

所以当线程将一个变量--,首先需要从内存中读取读取这个内存的值,也就是1000,然后放入到cpu上面执行的时候,将这个变量加载到寄存器上面,如果这个时候这线程的时间片到了,那么这线程就需要将寄存器上面的数据,也就是自己的上下文数据(执行到哪一步,变量的值是多少这些)带走,可以理解为就是从寄存器上面拷贝了一份带走。

下一个线程(线程2)过来了,照样从物理内存中读取数据,要注意,线程1是没有完成到第三步,也就是说物理内存里面的值还是1000,所以线程2读取到的还是1000,继续执行这三步,但是线程2的运气很好,并没有被打断,执行了完整的三步。

假设线程2执行了2次完整的三步,将物理内存里面的变量减到了998,之后时间片到达了。这个时候线程1继续来执行了,线程1需要恢复自己的上下文,将变量1000重新加载到寄存器上面。

之后将1000进行--操作了之后,将物理内存中存储改为1000,这个时候线程1和线程2就会出现了数据不一致问题,就会出现线程2都抢了2张票了,过一会(线程1执行完了),票还有999张?所以多线程的并发是存在问题的。

回归到代码中来:为什么会出现0和负数的情况。

cpp 复制代码
void*GetTicket(void*args)
{
    ThreadData*td=static_cast<ThreadData*>(args);
    const char*name=td->threadname.c_str();
    while(true)
    {
        if(ticket>0)
        {
            usleep(10000);
            printf("who=%s,get a ticket%d\n",name,ticket);
            ticket--;
        }
        else
        {
            break;
        }
    }
    delete td;
    return nullptr;
}

代码中存在10个线程,一个线程进入到while循环中,这个时候俄国ticket刚刚好为1的时候,进入了if判断,这个时候需要调用sleep系统调用,休眠0.01秒,这个时候线程被切换走了,其他的线程访问到的ticket还是1,都可以通过if的判断对ticket--,printf和ticket--都是需要重新从物理内存中来获取ticket变量的值的,这个时候获取到的很有可能就是别的线程已经完成了ticket--之后的值了,线程2对ticket--,执行完成后ticket为0,线程3 if判断的时候还是1,printf打印出来的已经是0了,继续进行--,变成-1,线程4打印出来-1,继续--......。

所以就出现了0和负数的情况,要如何解决多个执行流的并发问题呢?---锁!!!

3.线程互斥

3.1 锁的初始化和销毁

pthread_mutex_t 就是锁的类型。

对于pthread_mutex_init的第二个参数为锁的属性,不关心。

3.2 上锁和解锁

3.3 锁的介绍

故事1:假设有一个自习室,这个自习室里面有一把锁,谁先到了就可以从里面把门锁了,等我学习完了之后,别人才可以进来使用,没有抢到的同学只能在门外等着了。

对应上面的代码,那些线程就是一个一个的同学,它们谁先来了拿到锁就可以先进去学习,其他人就只能在门口等待,也就是没有拿到锁的线程会阻塞在加锁的那一行 ,那个自习室就相当于共享资源 ,锁的存在就很好的让只有一名同学可以访问到共享资源,来使临界区的代码串行运行。但是这样子也会导致其他同学的学习时间就减少了,所以加锁的本质就是一种用时间来换安全的做法。

故事2:这个时候有一些同学想要一直占着学习室,就想办法了,自己买了一把锁,从外面把学习室给锁上去了,别人来了也进不去。

所以加锁的时候就应该保证大家要竞争的是同一把锁,也就是要保证多个线程看到的是同一把锁,竞争的也是同一把锁。

局部锁:需要创建和销毁。

全局锁:整个进程运行期间都存在,使用宏 PTHREAD_MUTEX_INITIALIZER 来初始化,不需要 destroy,进程退出系统自动回收资源

3.4 锁的使用

代码1:使用锁来保护共享资源ticket

确保多线程看到的是同一把锁呢?很简单,在主线程中定义局部锁,将局部锁的地址传入给线程函数要接收的threadData类对象中,即在threadData类中添加类型为pthread_mutex_t*指针类型的成员变量lock_,并且在构造函数中对lock_进行初始化,这样多个线程拿到的一定是同一把锁,使用的是同一个锁初始化。

cpp 复制代码
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<vector>
#include<unistd.h>
#include<pthread.h>
using namespace std;
#define NUM 8
int ticket=10000;
// pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
struct ThreadData
{
public:
    ThreadData(int num,pthread_mutex_t *mutex)
    {
        threadname="thread-"+to_string(num);
        lock=mutex;
    }
public:
    string threadname;
    pthread_mutex_t *lock;
};
void*GetTicket(void*args)
{
    ThreadData*td=static_cast<ThreadData*>(args);
    const char*name=td->threadname.c_str();
    while(true)
    {
        pthread_mutex_lock(td->lock);
        if(ticket>0)
        {
            printf("who=%s,get a ticket%d\n",name,ticket);
            
            ticket--;
            pthread_mutex_unlock(td->lock);
        }
        else
        {
            pthread_mutex_unlock(td->lock);
            break;
        }
    }
    delete td;
    return nullptr;
}
int main()
{
    //创建多线程
    pthread_mutex_t lock;
    pthread_mutex_init(&lock,nullptr);
    vector<pthread_t> tids;
    for(int i=0;i<NUM;i++)
    {
        pthread_t tid;
        ThreadData*td=new ThreadData(i,&lock);
        pthread_create(&tid,nullptr,GetTicket,td);
        tids.push_back(tid);
    }
    for(auto i:tids)
    {
        pthread_join(i,nullptr);
    }
    pthread_mutex_destroy(&lock);
    return 0;
}

小问题1:

cpp 复制代码
void*GetTicket(void*args)
{
    ThreadData*td=static_cast<ThreadData*>(args);
    const char*name=td->threadname.c_str();
    while(true)
    {
        pthread_mutex_lock(td->lock);
        if(ticket>0)
        {
            printf("who=%s,get a ticket%d\n",name,ticket);
            
            ticket--;
            // pthread_mutex_unlock(td->lock);
        }
        else
        {
            // pthread_mutex_unlock(td->lock);
            break;
        }
        pthread_mutex_unlock(td->lock);
    }
    delete td;
    return nullptr;
}

如果像上面这样子写行不行,还少写一行代码,是不可以的这个时候当有线程拿到锁,判断的时候票是等于0的时候,就会直接break,就执行不到释放锁的操作了,其他线程就会一直阻塞住,导致死锁。

**代码2:**也可以定义全局锁。

这时候锁成为一个全局变量,这时候锁在定义的时候采用上图传入宏PTHREAD_MUTEX_INITIALIZER进行初始化即可,即全局的锁不需要调用pthread_mutex_init进行初始化,并且全局的锁也不需要手动销毁,而是自动销毁,即不需要调用接口pthread_mutex_destroy

那么此时threadData中的成员变量就不需要pthread_mutex_t*类型的指针lock了,并且构造函数中也不再需要对这个指针进行初始化了

cpp 复制代码
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<vector>
#include<unistd.h>
#include<pthread.h>
using namespace std;
#define NUM 8
int ticket=10000;

pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER; 
struct ThreadData
{
public:
    ThreadData(int num)
    {
        threadname="thread-"+to_string(num);
    }
public:
    string threadname;
};
void*GetTicket(void*args)
{
    ThreadData*td=static_cast<ThreadData*>(args);
    const char*name=td->threadname.c_str();
    while(true)
    {
        pthread_mutex_lock(&lock);
        if(ticket>0)
        {
            printf("who=%s,get a ticket%d\n",name,ticket);
            
            ticket--;
            pthread_mutex_unlock(&lock);
        }
        else
        {
            pthread_mutex_unlock(&lock);
            break;
        }
    }
    delete td;
    return nullptr;
}
int main()
{
    //创建多线程
    vector<pthread_t> tids;
    for(int i=0;i<NUM;i++)
    {
        pthread_t tid;
        ThreadData*td=new ThreadData(i);
        pthread_create(&tid,nullptr,GetTicket,td);
        tids.push_back(tid);
    }
    for(auto i:tids)
    {
        pthread_join(i,nullptr);
    }
    return 0;
}

两段代码的运行结果:

可以看到果然没有出现了0和负数的情况了。

3.5 关于锁的其他理论

1.锁本身也是共享资源。因为锁也是需要被大家共同去竞争的,多个线程就是要去申请和释放同一个锁。

2.申请和释放锁的操作必须是原子的。

3.互斥:保证同一时刻只有一个线程进入临界区,访问共享资源。解决的是"竞争"问题,防止数据被并发修改导致错乱。

4.线程的"饥饿问题":但是如果锁分配不合理(如一个线程长时间持有锁),容易导致其他线程"饥饿"(一直拿不到锁,无法执行)。我们上面的代码中就出现了线程的"饥饿问题"。

可以看到都是线程6去执行抢票,其他线程就长期处于挂起的状态,为什么线程6可以一直抢到锁呢?因为线程6在抢到锁之后,释放了锁,它离锁更近,所以下一次去申请锁也就更加的方便了。

要注意:互斥不等于饥饿,饥饿是锁使用不当的结果,不是互斥的必然产物。在适合纯互斥的场景下,使用互斥是正确的选择。

  1. 同步:为了使线程可以平等的去执行,需要让多个线程(或进程)按照预定的顺序执行,协调它们的工作步调。

在申请锁的表现就是:

  1. 外面来的线程必须排队(按顺序申请锁)。

  2. 释放锁的线程不能立即重新申请,必须排到队尾。

6.临界区:访问共享资源的那段代码,同一时间只允许一个线程执行。

一定要知道:线程在临界区中是可以被切换的!!!线程切换是由操作系统调度器控制的,与是否在临界区无关。当持有锁的线程被切换出去时,它是带着锁一起被切走的。所以其他的线程也无法进入。

3.6 锁的原理

之间在ticket--时说到了,ticket--的操作是有三步的,之间的每一步都有可能被切换走,那么上锁的过程就不会被切换走吗?锁是怎么做到的?怎么实现原子性的?

首先要明白其实锁不是什么很复杂的对象,在内存中无非就是一个变量。

上锁的实现:首先上锁的实现大概是分为了3条汇编语句

第一句:将寄存器eax中的al的值置为0,al可以看作就是一个比特位,不是0就是1。

第二句:交换物理内存中锁与al的值进行交换

第三句:判断寄存器中al的值是否大于0,是的话就相当于获得了锁,返回,不会被挂起。

解锁的实现:将物理内存中的锁变量重新置为1。

谈谈场景1(上锁):

如果线程1想要获取锁的话,执行了第一行的代码,也就是把al存储的值置为了0,之后时间片到了,线程1就带走自己的上下文并且将al的值自己复制了一份,之后被切换走了。

之后线程2来了,但是它很幸运,执行完了完整的代码,执行了上步代码,将物理内中锁置为了0。也就相对于获得了锁,去执行临界区里面的代码,但是当线程的时间片到来,线程就会发生切换,不管是否存储与临界区内,所以线程2就被切换走了,也是复制了一份al的值1,带着自己的上下文被切换走了。

这个时候,线程1回来了,线程1把存储的al值返回到al上面,将上下文恢复,继续执行接下来的代码,将al的值与物理内存的锁变量交换值,注意这个时候锁已经是0了,所以0和0交换,al寄存器里面的值还是0,所以线程1就被挂起了,其他线程过来即使执行了完整代码,但是物理内存的锁变量已经为0了,与al交换也还是0,被挂起。

其实就关键的就是谁能够执行第二步的代码,也就是可以将物理内存的1置换,也就相对于获得了锁,就算执行完了第二步完了之后马上就被切换了,但是也拷贝了一份al的值,当继续执行的时候,al值恢复上来还是为1。

**谈谈场景2(解锁):**解锁只有一条汇编语句,也就是原子的,唤醒阻塞等待锁一般所有操作系统来进行的,这里的解锁操作是所有的线程都可以进行的,但是95%的情况都是拥有锁的线程解锁的,因为解锁的步骤是在加锁的代码后面的,基本上应该是拥有锁的线程更快的执行到。

相关推荐
小白同学_C14 小时前
Lab4-Lab: traps && MIT6.1810操作系统工程【持续更新】 _
linux·c/c++·操作系统os
今天只学一颗糖14 小时前
1、《深入理解计算机系统》--计算机系统介绍
linux·笔记·学习·系统架构
不做无法实现的梦~16 小时前
ros2实现路径规划---nav2部分
linux·stm32·嵌入式硬件·机器人·自动驾驶
默|笙18 小时前
【Linux】fd_重定向本质
linux·运维·服务器
陈苏同学18 小时前
[已解决] Solving environment: failed with repodata from current_repodata.json (python其实已经被AutoDL装好了!)
linux·python·conda
“αβ”18 小时前
网络层协议 -- ICMP协议
linux·服务器·网络·网络协议·icmp·traceroute·ping
不爱学习的老登19 小时前
Windows客户端与Linux服务器配置ssh无密码登录
linux·服务器·windows
小王C语言20 小时前
进程状态和进程优先级
linux·运维·服务器
xlp666hub20 小时前
【字符设备驱动】:从基础到实战(下)
linux·面试
弹幕教练宇宙起源21 小时前
cmake文件介绍及用法
android·linux·c++