Linux-线程互斥

**摘要:**本文从多线程共享资源与临界区的概念出发,说明并发访问临界资源会导致数据不一致问题,并由此引出互斥与原子性的含义。文章以抢票程序为例,剖析 ticket 变量被并发访问时出现负数票的成因,进而介绍 Linux 下互斥量 mutex 的初始化、加锁与解锁接口及使用方式,最后结合 swap/exchange 指令讲解互斥锁的底层实现原理,并指出单纯互斥可能带来的饥饿问题,为后续线程同步内容做铺垫。

1.相关背景概念

我们之前说过:线程的大部分资源是共享的,我们把这种多执行流可以同时访问的资源叫做共享资源;至于进程间通信,基本上的目的也是为了让进程间形成共享资源。

**不管是多进程还是多线程,它们都是多执行流,我们把它们访问的公共资源叫做共享资源;我们把这种被多执行流保护起来的资源叫做临界资源。**如:管道是临界资源,因为它是多执行流的共享资源且还做了对应的保护!

我们把在每个线程内部访问临界资源的代码叫做:临界区。

因为存在临界区,所以必定存在非临界区,如:线程内部都有独立的栈结构,它们在栈创建的临时变量都是在自己的栈空间形成,这个临时变量叫做:非临界资源,定义这些变量的代码叫做非临界区!

此外,我们运行之前的多线程代码的时候,也会出现信息错乱的时候:

这种错乱在只有1秒的间隔打印的情况下是不多的,如果我们设置的是不休眠打印,那么这个错乱会加剧!

出现这个问题的原因是:当多个执行流访问同一个显示器文件时,显示器本质是文件,向显示器打印本质是往显示器文件写入,一旦多个执行流访问同一个显示器,那么显示器本身就是共享资源,为什么打印时错乱了,是因为多执行流执行自己的打印临界区代码时访问统一显示器的时候,显示器没有被保护起来,所以打印的信息出现了错乱!

我们把这种并发导致临界资源出现异常/写入错乱/覆盖的问题就是多线程同时必须面临的问题,为什么?因为多线程大部分资源是共享的!

如果我们在访问临界区和临界资源的时候,如果我在自己的线程new/malloc一个全局对象,这个全局对象在地址空间是能被其他线程看到的,能看到不代表能访问临界区,访问临界资源造成异常并不是多线程间共享了资源,而是共享资源的同时,多线程还进行并发访问导致的,因此,共享资源不一定会导致数据不一致问题,访问共享资源才会导致数据不一致的问题!

怎么解决这种问题?

多线程和多进程间要解决方案分两类:互斥和同步!这篇博客先讲互斥!

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

现实生活中,互斥的例子很常见,如:去ATM机取钱的时候,每个ATM机都用小房子包围起来,ATM机站在资源角度属于共享资源,我们在ATM机外面做的事情如:吃饭,这就属于访问非临界区代码,如果我想取钱了进入ATM机内部,此时就需要访问共享资源了,而我们进入ATM机房子里的时候其他的人就没法进来了,这就是互斥!只要互斥了,就能保证只有一个执行流访问临界区!

在互斥期间,从开始访问临界资源到结束访问都没有其他执行流来干扰该执行流访问,这个工作叫做互斥,本质是:把多线程访问资源由并发执行转变为串行执行,因此互斥带来的意义是:可以保护共享资源的安全,但是它也会带来负面影响:可能降低程序的运行效率!

因此,我们对于互斥需要合理的使用而不能滥用!

当我们在ATM机取钱的时候,对外面的人来讲,他知道现在敲门是没有意义的,他必须在门口等待你把钱取完,自己在进ATM机才有意义,因此对站在门外的人来讲,只有两种状态对他自己是有意义的:ATM机没人访问,小房间的门是打开的;之前访问ATM机的人出来了。因此,站在外面人的角度:你取钱的动作是原子的,要么不取:ATM机没有人访问;要么取完。在你访问ATM机期间我是对你产生不了任何影响的,所以我们把你取钱的动作叫做原子性!

原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成!

原子性并不是特性,而是一种结果,是因为有互斥的存在而铸造了我们访问资源的原子性!

2.互斥量mutex

互斥量是用来完成保护的一种数据对象以及配套的方法,就可以保证我们对资源进行访问。

需要理解这个互斥量,我们需要反向理解:如果一个共享资源没有被保护,会存在什么情况?

2.1反向理解

之前我们多线程同时往显示器打印导致的错乱这也算一个情况,但是我们还是说一个异常现象更好理解:

如果我定义了一个全局变量ticket代表抢票逻辑的还有多少张票没有被抢:

cpp 复制代码
int ticket = 100;

然后我们创建若干组线程模拟抢票,每个线程都会执行同一种抢票方法:

cpp 复制代码
//抢票方法
void *route(void *arg)
{
    char *id = (char *)arg;
    while (1)
    {
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
        }
        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);
}

其中所谓的ticket就是共享资源,它是一个全局资源,这个共享资源会被所有线程并发访问,并发访问会导致很多问题:有线程觉得票数100,有线程线程觉得票数0,甚至把票抢成负数!

如果运行代码开始有:

结束有:

但是我们的代码设置了如果ticket<=0就退出啊,为什么不执行,也就是说我们的票被抢成负数了!

ticket是共享资源,如果没有保护,我们看到出现了:数据不一致的问题!

①为什么会出现这种问题?

②如何解决?

①我们观察代码发现,一共有三处访问了共享资源:

第一个是判断的时候访问了共享资源,第二个是打印的时候访问了共享资源,第三个是--访问了共享资源,因此,如果我们把红框的代码保护起来了,这部分代码叫做临界区:

为什么抢票会抢到负数?主要是在if(ticket>0)这里!

我们定义的任何变量,对象最终都会在物理内存中存储,ticket>0是逻辑运算,得到的是真和假对票数进行--的叫做算数运算,这两者都是必须由CPU来完成,因此CPU主要帮我们解决算数运算和逻辑运算的问题!数据在内存中,算数却在CPU中,注定了完整的运算或执行某些指令至少且必须要经历三个阶段:Ⅰ取指令或取数据(从内存到CPU);Ⅱ分析指令(做什么运算);Ⅲ执行指令。

第一步需要把数据从内存拷贝到寄存器当中,第二步,把这些数据放到运算器进行运算(算术和逻辑),并产生结果返回!

CPU执行这些过程一定会以线程或进程为载体执行!上面的视角是计算机组成的视角,下面的视角是操作系统的视角,我们要从两个视角来看!

把数据从内存搬到寄存器,在内存里,数据是共享资源,寄存器本质属于一个进程或线程,它对应硬件上下文,线程的硬件上下文有一个特点:当它运行时,它可以访问自己寄存器的数据,但当一个线程要进行切换的时候,线程要把自己的上下文保存,寄存器只有一份,但是寄存器内部的数据是多份的,每一个线程各一份。我们把数据从内存拷贝到寄存器当中,本质是把该变量的内容变成私有化内容。因此把数据拷贝到寄存器本质:如果是今天的例子就是把ticket变量的数字由共享数据变成一个线程的私有化数据!

如线程A和线程B,线程A要执行判断,它把100读到寄存器里,判断出来ticket>0,过一会后线程在usleep的过程中时间片到了,此时线程A就被切换走了,它还需要保存硬件上下文代表当前已经执行到的位置,因此,线程A带走了ticket为100,操作系统调度线程B进行运算,但是线程B在执行相同代码时,执行到ticket--才把时间片用完,假设线程调度极不公平,线程B一直在调度,直到线程B的ticket减到0才退出,线程A换回来之后发现ticket为100,因为线程A当时执行到的地方是usleep,然后继续执行,而不是回退到原来的判断ticket>0这里,此时线程A就会执行ticket--,就会又执行一次把数据从内存到寄存器上,重新获取一次数据,此时拿到的ticket是0,然后就--使得ticket变为-1。

当然,如果是上述代码,打印在--之前,是一个线程执行了--,导致另一个线程执行打印的时候打印-1,即:ticket运行的最终结果为-3,而执行--导致的ticket值和打印--后的ticket值并不是同一个线程!

而我们会在本来if条件不满足的情况下打印的结果是不用if的时候ticket变量,而是要重新取内存里的内存数据,即判断的时候是>0,但是重新取内存的时候ticket<0了!

这里的ticket--本身是原子的吗,即ticket--是安全的吗?

它是不安全的,ticket--虽然看似只有一条指令,经过编译器汇编后会形成三条指令

首先是把数据从内存搬到CPU,再是减减,最后把数据从CPU搬到内存,如果刚好执行到sub线程就被切换走了;此时线程B来了,它执行速度很快,把ticket减到0然后他就退出了,线程A切换回来的时候,不是先执行sub进行--,而是要恢复上下文,他把自己的100恢复到寄存器里,然后进行100的--,变为99,再把值写回到ticket里,这就相当于线程B把100刚减到0,线程A转头回来把ticket由0变为了99!因此汇编问题是不安全的!

那么票数减到负数问题是ticket>0的逻辑判断导致的还是ticket--导致的?

两个都有问题,但是是ticket>0的逻辑判断为主要问题!因为ticket--并发只会导致ticket变大,因此这个ticket--可能导致不同的人买到同一张票(因为实际上的ticket应当比100大),主要是判断ticket的值出问题,因为是他才导致后面ticket一直--,如果他能判断好的话就不会出现后面的问题了!

ticket是票数,他本质是全局变量式的计数器,不是线程安全的,会有并发问题!

因此,导致这个问题的原因是:多线程并发访问了临界区的代码导致数据不一致的问题!

②怎么解决?

要解决这个问题,本质是对:对代码进行保护,只要保护起来了就是临界区。(没有保护的多线程同时访问同一个代码的不算临界区)

因此pthread库提出了互斥锁的概念,这个互斥锁可编程,在Linux中,互斥锁叫做互斥量!

2.2互斥量的接口

初始化互斥量有两种方法:

①静态分配:

cpp 复制代码
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

②动态分配:

cpp 复制代码
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const
pthread_mutexattr_t *restrict attr);
参数:
mutex:要初始化的互斥量
attr:NULL
cpp 复制代码
//定义互斥量(锁),锁是全局的
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;

所有线程想保护自己,应当先申请锁,而不是先执行代码!

互斥量加锁与解锁:

cpp 复制代码
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
cpp 复制代码
//抢票方法
void *route(void *arg)
{
    char *id = (char *)arg;
    while (1)
    {
        /* 要保护的区域起点 */
        //访问之前加锁
        pthread_mutex_lock(&gmutex);
        //临界区代码
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
            /* 要保护区域终点 */
            //解锁
            pthread_mutex_unlock(&gmutex);
        }
        else
        {
            //这里也要做一个
            pthread_mutex_unlock(&gmutex);
            break;
        }
    }
    return nullptr;
}

运行的时候速度会变慢,但是抢票不会重复:

但是有一些问题:①gmutex自己不是全局变量吗,怎么保证自己的安全?②所有的线程都要申请锁吗,还是个别线程需要申请?③如果线程进入临界区,线程会被切换吗,会因为切换导致并发问题吗?

①互斥锁自己是原子的,至于为什么,后面会讲底层实现!

②所有线程都必须遵守申请锁的规则!如果故意不这样做,这就会触发并发问题!申请成功的线程会继续向后运行,如果申请失败,线程会被阻塞!

③会被切换!举个例子:去自习室,自习室门上有一把锁和钥匙,当第一个人来的时候就可以用钥匙开锁,开完锁之后这个人就会把门从里面反锁,其他人也进不来,因为没钥匙;哪怕第一个人中途上厕所了,只要他不把钥匙给别人,那么这个自习室别人还是进不来,因为他拿不到钥匙,因此自习室叫做CPU资源,钥匙是锁资源,每个人都相当于一个线程,在自习时,哪怕你人没在自习室里,其他人也进不来,相当于你切换了,但是这个锁你并没有释放,其他人照样进不来,因此进入到临界区,线程会被切换,但是不会因为切换而导致并发问题,因为你是持有锁被切换的,即你拿着锁但是没有释放这个锁,其他线程不能运行只能阻塞等待你直到你把锁资源释放,因此这就是此时的效率比较慢的原因!

因此,我们可以认为:在加锁和解锁之间的代码对于当前在临界区运行的线程来讲,执行这部分代码的过程是原子的,不会被外部线程打扰!因此,原子性是加锁之后的结果!

互斥锁现在是定义在全局的,如果我们定义的锁是(由主线程)在栈上开辟的,此时的锁不是全局的也不是静态的,因此,需要主线程进行初始化,并在所有锁用完之后进行销毁:

cpp 复制代码
//销毁互斥量
int pthread_mutex_destroy(pthread_mutex_t *mutex);
cpp 复制代码
// 操作共享变量会有问题的售票系统代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

//还有多少张票未抢
int ticket = 100;

//抢票方法
void *route(void *arg)
{
    char *id = (char *)arg;
    while (1)
    {
        /* 要保护的区域起点 */
        //访问之前加锁
        pthread_mutex_lock(&gmutex);
        //临界区代码
        if (ticket > 0)
        {
            //usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
            /* 要保护区域终点 */
            //解锁
            pthread_mutex_unlock(&gmutex);
        }
        else
        {
            //这里也要做一个
            pthread_mutex_unlock(&gmutex);
            break;
        }
    }
    return nullptr;
}
//主线程执行方法
int main(void)
{
    //栈上定义锁
    pthread_mutex_t lock;
    //对锁初始化
    pthread_mutex_init(&lock,nullptr);
    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);
    //等所有的线程把锁用完了统一销毁
    pthread_mutex_destroy(&lock);
}

但是我们还需要给每个线程传递锁,因此就可以定义结构体/类来传递:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include<iostream>
#include<string>

//还有多少张票未抢
int ticket = 100;
//定义互斥量(锁),锁是全局的
//pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;

struct data
{
    std::string name;//名字
    pthread_mutex_t* lockp;//锁
};

//抢票方法
void *route(void *arg)
{
    data *id = static_cast<data*>(arg);
    while (1)
    {
        /* 要保护的区域起点 */
        //访问之前加锁
        pthread_mutex_lock(id->lockp);
        //临界区代码
        if (ticket > 0)
        {
            //usleep(1000);
            std::cout << id->name << " sells ticket:" << ticket << std::endl;
            ticket--;
            /* 要保护区域终点 */
            //解锁
            pthread_mutex_unlock(id->lockp);
        }
        else
        {
            //这里也要做一个
            pthread_mutex_unlock(id->lockp);
            break;
        }
    }
    return nullptr;
}
//主线程执行方法
int main(void)
{
    //栈上定义锁
    pthread_mutex_t lock;
    //对锁初始化
    pthread_mutex_init(&lock,nullptr);
    //创建一系列结构体
    data d1 = {"thread 1",&lock};
    data d2 = {"thread 2",&lock};
    data d3 = {"thread 3",&lock};
    data d4 = {"thread 4",&lock};
    pthread_t t1, t2, t3, t4;
    //创建线程执行抢票
    pthread_create(&t1, nullptr, route, (void *)&d1);
    pthread_create(&t2, nullptr, route, (void *)&d2);
    pthread_create(&t3, nullptr, route, (void *)&d3);
    pthread_create(&t4, nullptr, route, (void *)&d4);
    //等待线程退出
    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);
    //等所有的线程把锁用完了统一销毁
    pthread_mutex_destroy(&lock);
}

为什么我们加锁和解锁放在while循环内部而不放在while循环外部,这是因为:加锁和解锁时,囊括的临界区尽量是最小集,我们不能把非临界区的代码也囊括进去,虽然可以囊括,但是范围越小,可以在一定程度上增加或减少线程之间串行的概率,可以在一定程度上提高效率!

锁是临界资源,如何保证加锁和解锁时的安全?

我们需要保证加锁和解锁设计为原子的,如何保证原子性,如何设计锁的?

实际上,设计锁有很多种方法,有偏硬件的实现方案,我们执行自己的代码,操作的时候不是原子的,是因为我们的代码其实要分为很多行代码去执行且会被时钟中断还有就是时间片到了会切换,如果我们执行代码期间不被任何外部事物打扰的情况下,那么我执行无限个指令它都是原子的。

如何做到程序在执行的过程中不被打扰,即我们的程序为什么会被打扰?

我们现在用到的操作系统是分时操作系统,线程在执行期间,如果时钟中断一直处于开启的状态,那就随时有可能会因为时间片到达而被切换走,因此这就带来一个很好的实现原子性的方案:关闭(时钟)中断!

还有一种方案就是用软件实现,比如:互斥锁就是实现原子性的方案。C/C++在编译时会编译形成多条语句,一行代码会形成很多行汇编指令,一条汇编是原子的,取指令本身是原子的。

此外,我们发现抢票的计数器可以被多个线程同时拷贝,因此这会导致同一个变量在不同线程的上下文就会出现多份,这也是会出现并发问题的本质原因。

我们当代的x86计算机往往会给我们提供两条指令,其中这两条作用和地位是相等的:

bash 复制代码
swap
exchange

这个指令的特点是:把寄存器里的内容和内存里的内容做交换!最后这两个指令都会形成同一种汇编语句,加锁和解锁的过程:

所谓的互斥锁我们把它当做一个整型变量即可,我们把这一把锁当做变量值,这个值是1:

如果两个线程竞争这一把锁,如果线程A先运行,那么会先执行先申请加锁的逻辑,加锁成功会继续往后运行,加锁失败就被挂起,第一步:

把0写到寄存器里(mov:move),我们之前说过寄存器里的数据是私有数据,此时的0就是私有的。

这是实现加锁的核心代码,它做exchange %al和mutex,它做的是把寄存器的值和mutex值互换,交换之后mutex的内容自动清零,寄存器里的值就变为1了,交换的本质:是把1这个数字以非拷贝的方式从共享变成私有!

然后执行:

如果刚好在准备执行这个判断的时候,线程被切换走了,就需要保存当前执行到哪里和寄存器里对应的数据。

此时线程B也来了,然后它要做加锁操作,因此它会和线程A一样和内存做交换:

此时的线程B拿自己在寄存器里的值为0和mutex进行交换,此时的mutex内容已经被线程A清零了,因此实际上的线程B并没有拿到任何锁资源,然后执行到判断发现此时al寄存器里内容等于0,因此就挂起等待了!此时我们称之为线程B竞争锁失败了,也就是说,谁先执行了这个指令:

就能得到锁的资源,哪怕刚好在寄存器内部的mutex交换出去时间片就到了,也没事,还是A得到了锁资源!

因此,什么叫做锁:是看哪个线程最先把锁里面的mutex(1)交换到自己的上下文,因为1只有一个,所以谁先拿到这个1谁就能获取到这个锁!因为这个1只有一个,所以根本不怕其他线程能拿到。

怎么解锁?

我们只需要把1写到mutex里就行!

回到最开始的例子:如果我自习到一半不想自习了,刚准备把钥匙重新挂到自习室的门上,但是我转念一想,门口这么多人,如果我想回来自习就没办法进自习室了,然后又进自习室学了几分钟,反正就是你今天实际上呆在自习室的时间并没有多少,反而把时间花在了周而复始把锁从墙上拿下放回的操作了,此时我们的自习室就没有得到充分的利用!

这种情况有没有错?

没错,因为规则是这样定的,谁先拿到钥匙谁就能进自习室,而这个自习室不能两个人及以上进入,也没有造成什么问题,没有什么数据不一致或导致严重的后果,但是它特别不合理,因为它可能会导致效率低下的问题,比如,我们上面的抢票代码运行起来只有一个线程能一直抢票:

也就是说所有的票都被一个人抢了,其他人肯定不干,你手速快没毛病,但是也要分给其他人一杯羹啊!

因此,我们可以自己设计一下,隔一段时间放一下票,并且不要让所有的线程碰到票为0就退了,那么在没有余票的情况下,这四个线程就只能竞争锁完了之后再释放锁,这不是吃饱了没事干吗,它们在高频做着没有意义的事情,白白浪费CPU资源!

因此,如果我们做互斥功能是完全不足够的,它可以用来做短期的事情,如果是重复的做需要依赖特定条件去完成访问控制的话,因此光有互斥锁它没错,但是它不合理!因为你自己离锁近,所以你的竞争优势非常大,这会导致其他线程长时间申请不到锁,其他线程就无法执行后续代码,造成饥饿问题!

因此,短期的完成资源的派发用互斥锁是可以的,但是长期下来长期下来一定要让每个线程都公平获取到对应的资源,减少饥饿问题。

因此,自习室管理员还新增了几条规定:①凡是从自习室出来的人归还钥匙,且规划钥匙之后,不能立即申请;②外部的人,必须要排好队,出来的人,必须要排到队列尾部,不能插队;③每次在自习室待的人数<=1(原规则)。

这新增的规则是在临界资源安全的情况下,让不同的线程,访问临界资源,具有一定的顺序性 ---- 这种顺序性我们叫做:线程同步!

线程同步下篇博客讲解,这里提供概念!

2.3互斥量的封装

锁的封装很简单:

cpp 复制代码
#include<iostream>
#include<mutex>
#include<pthread.h>
#include <stdio.h>
#include <stdlib.h>

//锁类
class Mutex
{
public:
    Mutex()
    {
        //初始化锁
        pthread_mutex_init(&_lock,nullptr);
    }
    //加锁
    void Lock()
    {
        pthread_mutex_lock(&_lock);
    }
    //解锁
    void UnLock()
    {
        pthread_mutex_unlock(&_lock);
    }
    //析构
    ~Mutex()
    {
        //删除锁
        pthread_mutex_destroy(&_lock);
    }
private:
    pthread_mutex_t _lock;//锁(实际上就是整型变量)
};

//锁的使用

//还有多少张票未抢
int ticket = 100;
//实例化对象
Mutex lock;

//抢票方法
void *route(void *arg)
{
    char *id = (char *)arg;
    while (1)
    {
        /* 要保护的区域起点 */
        //访问之前加锁
        //系统方法
        //pthread_mutex_lock(&gmutex);
        //我们方法
        lock.Lock();
        //临界区代码
        if (ticket > 0)
        {
            //usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
            /* 要保护区域终点 */
            //解锁
            //系统方法
            //pthread_mutex_unlock(&gmutex);
            //我们方法
            lock.Lock();
        }
        else
        {
            //这里也要做一个
            //pthread_mutex_unlock(&gmutex);
            lock.Lock();
            break;
        }
    }
    return nullptr;
}
//主线程执行方法
int main(void)
{
    //对锁初始化
    //pthread_mutex_init(&lock,nullptr);
    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);
    //等所有的线程把锁用完了统一销毁
    //系统方法
    //pthread_mutex_destroy(&lock);
    //我们等系统析构即可
}

但是如果是这样的话没必要,我们需要自己创建方法:

cpp 复制代码
class LockGuard
{
public:
    LockGuard(Mutex* mutexp)
    :_mutexp(mutexp)
    {
        //自动加锁
        _mutexp->Lock();
    }
    ~LockGuard()
    {
        //自动解锁
        _mutexp->UnLock();
    }
private:
    Mutex* _mutexp;//锁地址
};

以后我们在新线程使用加锁解锁这样写就行:

cpp 复制代码
void *route(void *arg)
{
    char *id = (char *)arg;
    while (1)
    {
        //这样创建就能自动加锁解锁了
        LockGuard Lockguard(&lock);
        //临界区代码
        if (ticket > 0)
        {
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
            /* 要保护区域终点 */
        }
        else
        {
            break;
        }
    }
    return nullptr;
}

因为我们的代码自动会加锁解锁,因为while每次循环的类对象会自动调用构造和析构!

这种就是RAII风格的加锁!

但是如果这样的话就不能做其他的事情了,while循环被锁死只能做加锁解锁了!

因此这样写:

cpp 复制代码
// 抢票方法
void *route(void *arg)
{
    char *id = (char *)arg;
    while (1)
    {
        // 临界区代码
        {
            LockGuard Lockguard(&lock);
            if (ticket > 0)
            {
                printf("%s sells ticket:%d\n", id, ticket);
                ticket--;
            }
            else
            {
                break;
            }
        }
        //非临界区代码
        //做其他事情...
    }
    return nullptr;
}

后续我们在线程同步也有可能用这段封装!

相关推荐
香芋芋圆1 小时前
iD-editor 详解|OSM官方在线编辑器,基于D3.js的轻量地图编辑器
开发语言·javascript·编辑器
赵小哥要冲啦1 小时前
社区邻里互助小程序从 0 到 1 复盘:需求拆解与信任机制设计
java·小程序·需求分析
xiaogai_gai1 小时前
采购业务退款单同步实战:从钉钉审批流到金蝶云星空付款单
java·数据库·钉钉
二哈赛车手2 小时前
新人笔记---hermes定时功能设计,具体讲解以及可优化点
java·人工智能·笔记·spring·ai·typescript
ZWZhangYu2 小时前
从 Dify 实践理解大模型上下文缓存
java·后端·spring
小熊恋旧2 小时前
Python 面向对象编程:方法、属性、继承与多态详解
开发语言·python
云边云科技_云网融合2 小时前
面向汽车零部件制造业场景的解决方案 | 70 秒:一台车下线的时间里,藏着五张网
开发语言·汽车·php
Evand J2 小时前
【PMSM滤波例程2】PMSM未知负载转矩观测EKF。MATLAB例程(负载增广EKF与实际仿真结果)。附下载链接
开发语言·matlab·电机
小羊没烦恼!2 小时前
关于分布式锁原理的一些学习与思考-redis分布式锁,zookeeper分布式锁
java·服务器·网络·学习·负载均衡