Linux系统编程6(线程互斥,锁,同步,生产消费模型)

简介: Linux系统编程6(线程互斥,锁,同步,生产消费模型)

上篇文章介绍完线程的概念后,我们将在这篇文章中初步探讨线程编程以及线程应用中的问题,这篇文章将以抢票系统为例,贯穿整篇文章。笔者将介绍在多线程编程中会出现的问题,什么是同步?什么是互斥?为什么多线程编程常有加锁的概念,什么又是生产者和消费者模型,读完这篇文章,你会得到相应的答案,笔者这里强烈建议各位把文中给出的demo示例自己实现一遍

多线程这部分必须要理论和实操相结合,并不像前面虚拟地址空间,页表这些理论性的知识,只要理解就可以了


初步应用多线程

多线程是一朵带刺的玫瑰,在享受它带来方便的同时要煞费思量多线程带来的问题

接下来咱们以一个实例来刨析使用线程究竟会带来哪些问题,咱们模拟一个抢票的场景,在main函数里创建一个次线程,然后主线程和次线程同时进行抢票,代码如下

#include<pthread.h>
#include<iostream>
#include<unistd.h>
int counter = 100;
void* start_routine(void* args)
{   
    while(counter > 0){
        counter--;
        std::cout << "抢票用户:次线程,剩余票数: " << counter << std::endl;
    }
    return nullptr;
}
int main()
{   
   pthread_t thread;
   pthread_create(&thread, NULL, start_routine, NULL);
    while ( counter > 0){
        counter--;
        std::cout << "抢票用户:主线程, 剩余票数:" << counter << std::endl;
    }
    pthread_join(thread, NULL);
    return 0;
}

代码很简单,看运行结果

可以发现,所有的票都是主线程抢到的,次线程一张票都没有抢到,这是因为次线程还没有创建完的时候,CPU不断执行主线程的循环,把票抢完了

我们要模拟出多个线程间交叉执行的情况,而不是某一个线程把所有的票都抢完了,想要模拟出这个场景,我们就得人为让CPU频繁的调度

所以,我们需要在抢票循环之前加上sleep,当某个线程准备开抢时,先休眠一会,被OS挂起,给其他线程启动的机会。但是我们不敢直接用sleep,因为该函数是以秒为单位的,以CPU的速度,如果休眠1秒,会在这个时间间隙内被其中一个线程全部抢完,所以我们要使用另一个函数——usleep

该函数和sleep一样,不过是以微妙为单位,我们调整参数到毫秒级别,这样两个线程就能够交叉抢票了,如下是修改后的代码

#include<pthread.h>
#include<iostream>
#include<cstdio>
int counter = 100;
void* start_routine(void* args)
{     
    while(true){      
        if (counter > 0){          
            counter--;
            usleep(1234);
            std::cout << "抢票用户:"<< (char*) args << " 剩余票数: " << counter << std::endl;                 
        }
        else break;
    }
    return nullptr;
}
int main()
{   
   pthread_t thread;
   char second_thread[] = "次线程";
   char main_thread[] = "主线程";
   pthread_create(&thread, NULL, start_routine, (void*)second_thread);
   start_routine((void*)main_thread);
   pthread_join(thread, NULL);
   return 0;
}

如下图是运行结果

根据图上信息,可以发现,两个线程确实实现了交叉抢票,但是吧,最后剩余票数出现了-1是什么情况,两个线程正常抢票,都是进行counter > 0才能进入抢票,可为什么就出现了负数了呢?接下来刨析这个问题(再次建议读者完成代码编写,亲自感受这个过程)


互斥

当票数counter只剩1的时候,此时次线程从内存中读取数据,发现counter为1,然后判断counter>0为真,但是一进入循环内,就遇到了usleep,于是次线程会被OS挂起,然后OS唤醒主线程,主线程去内存中加载counter数据时,发现counter的值仍为1,就通过了判断语句,同样的它将遇到usleep,被挂起。之后OS唤醒次线程,次线程开始执行counter-1,然后循环,判断counter为0,就退出了循环,接着主线程被OS唤醒,开始执行counter-1,注意此时的counter已经为0了,减去1之后就是-1,这也是为什么最后主线程的剩余票数会变为-1的原因

可以发现,造成这个问题的根本原因就是因为抢票过程不具备原子性,什么是原子性?当一件事具备了原子性,就说明这件事要么做成,要么失败,没有中间状态可言。回顾一下刚才抢票时,次线程检测到counter为1,它进入了循环,如果它具备原子性,那么对于抢票这件事,要么它抢票成功,counter减为0,要么它抢票失败,counter仍为1,轮到主线程抢票,在其它线程眼里这个过程是一瞬间,没任何中间过程的

但事实是它是有中间状态的,它不可能在物理上的一瞬间就抢到票把counter减为0,它是要逐步执行代码的,执行代码是需要时间的,就单单counter--这个操作,就至少得翻译成三条汇编语句逐条执行,在这段时间内,counter的值仍为1,其它线程就有机会进来了

除了上面出现负票这种情况,还有可能会导致票越抢越多的情况,只不过这种情况很难模拟出来,但它确实存在,故此我们理论上分析出现这种情况的原因

假设现在有100张票,有A,B两个进程一起抢票

这看似一个简单的counter-1操作,背后的CPU至少要分成这三部分完成,上面就是正常抢票过程中,CPU所执行的操作

如上图,因为A被挂起,长时间处于中间状态,B在这段时间内将票抢空,而A被唤醒后,继续执行中断前的操作,导致了票明明被B抢空了,却又变回99这种越抢越多的情况

面对类似抢票这种场景就得保证多个线程在执行任务时要是原子的,互斥是保证原子性的手段之一,正在抢的那个线程要么抢成功,要么抢失败,而在它抢的过程中,固有的中间状态靠互斥性来解决,也就是其他线程与当前正抢票的线程保持互斥,不能打扰它抢票

如何做到互斥?解决方法就是加锁


什么是锁呢?前面提到过,抢票时我们需要对票这种共享资源进行保护,也就是抢票这个事件是要原子的,但是就单单counter--这种操作,得有三条汇编指令才能实现,这就产生了中间状态,锁就是来锁住这个中间状态,大致流程如下

当线程A正在抢票时,它需要申请一把锁,只要它申请锁成功,那么它就可以放心去执行它的抢票代码,其它的线程要想抢票也得先申请锁,但是锁已经被A拿走了,它们只能阻塞等待A释放锁才能够申请到锁

上述过程中,锁将线程A抢票的中间状态锁住了,就能够实现抢票的原子性,在其它线程看来,A抢票就是原子的,就是一瞬间,没有中间过程的。因为A拿到锁正抢票时,其它线程到了就得阻塞等待从而被OS挂起,当它们被唤醒的时候,说明A已抢完票,释放了锁,在它们眼里这可不就是一瞬间的事嘛

明白锁的作用,接下来看看怎么使用锁,锁本质也是一种数据类型,其类型为:pthread_mutex_t,使用锁之前得先申请一个锁

申请一个锁:

pthread_mutex_t  mutex;

申请完锁需要对其进行初始化,使用初始化接口:

pthread_mutex_init( pthread_mutex_t  *mutex );

第二种初始化方法:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

初始化完成之后,就可以使用锁了,使用锁分为加锁和解锁

加锁的接口为:

pthread_mutex_lock( pthread_mutex_t *mutex);

解锁的接口为:

pthread_mutex_unlock( pthread_mutex_t *mutex);

还有一个加锁的接口为:

pthread_mutex_trylock( pthread_mutex_t *mutex);

那这两个加锁接口有什么区别呢?举个例子

有线程A,B共同使用一把锁,当使用pthread_mutex_lock加锁时,A加锁完,B再来就得阻塞等待。而使用pthread_mutex_trylock加锁时,A加锁完,B再来并不会阻塞等待,而是报错返回,因此pthread_mutex_trylock也被称为非阻塞式加锁

那在哪里使用锁呢?在main函数开头就加锁,return前解锁吗?当然不是,我们要明白加锁锁的是一种中间状态,把抢票这个具有过程性的操作给保障成原子性。原本有A,B两个线程同时并行执行,但在对公共资源进行争夺时,就会带来很多的麻烦,采用加锁相当于把这些线程在抢夺资源时排好队,谁先来的谁开始,后面的排队等待,变成一种串行的执行方式

串行排队等待,程序的效率必然会下降,因此在满足公共资源保护的前提下,加锁的范围要尽量的小,接下来通过demo来演示加锁的用法,还是用上面的抢票程序

#include<pthread.h>
#include<iostream>
#include<cstdio>
#include<unistd.h>
int counter = 10;
//创建一个全局锁
pthread_mutex_t mutex;
void* start_routine(void* args)
{     
    while(true){
        //在这里加锁,保护公共资源counter
        pthread_mutex_lock(&mutex);
        if (counter > 0){          
            counter--;
            std::cout << "抢票用户:"<< (char*) args << " 剩余票数: " << counter << std::endl;
            //这里counter--已经执行完毕,公共资源保护完成,可以解锁
            pthread_mutex_unlock(&mutex);
            usleep(1234);                             
        }
        else{
            //这里是程序拿到锁,然而没票了,退出之前要解锁
            pthread_mutex_unlock(&mutex);
            break;
        }   
    }
    return nullptr;
}
int main()
{   
   pthread_t thread;
   char second_thread[] = "次线程";
   char main_thread[] = "主线程";
   //初始化该全局锁
   pthread_mutex_init(&mutex, NULL);
   pthread_create(&thread, NULL, start_routine, (void*)second_thread);
   start_routine((void*)main_thread);
    pthread_join(thread, NULL);
    return 0;
}

由上面的demo可以看出,加锁和解锁的范围还是很短的,运行上面的demo,你会发现程序抢票的过程明显变慢,这是因为线程在抢票时是串行执行。该demo主要为了保护counter,counter--指令执行完,就可以解锁,避免长时间串行,影响程序效率

这里不给出代码运行结果,尝试自己编写运行


深入理解锁

上面锁的用法也看了,锁确实能保护我们的票目counter,但是你有没有想过,多个线程之间虽然老老实实排队去抢票了,但是它们会竞争锁呀,毕竟谁先拿到锁,谁就能优先执行,多个线程都在竞争同一把锁,那锁是什么?没错,锁也是一种公共资源,那么加锁的过程中是不是也有中间状态,那么谁来保护锁呢?

眼前先别黑,不需要谁来保护锁,操作系统给我们保证了加锁过程是原子性的,也就是加锁过程中不存在中间状态,用一条汇编指令就能完成

实现的方法很多,这里笔者讲一个比较通用的方法

下图是加锁的部分伪代码,也是加锁的核心

我们需要有一个场景,还是用刚才抢票的那个demo,有线程A和B同时抢票

这是锁能实现串行执行的原理,不过上述是线程A一路顺畅的拿到锁,有没有可能

线程A刚执行完movb $0, %al就被切走了呢

线程A刚执行到xchgb %al, mutex就被切走了呢

当然有可能,时间中断可能在任意一条汇编执行时就到来,加下来分别模拟一下这样的场景

上图同时包含了前面提到了两种场景,都没有什么问题,伪代码中最关键的还是xchgb指令,给交换提供了一个绝对原子的操作, 执行xchgb指令,该线程禁止被中断或受到其它线程干扰,对于解锁,要求就没那么高,下面是解锁的伪代码


封装锁

直接使用原生接口创建和调用锁,老是感觉不方便,毕竟是学C++的,就得用C++的方式给其原生接口封装一下,更符合现代编程的使用习惯,封装代码如下

#pragma once
#include<iostream>
#include<pthread.h>
#include<string>
#include<cstdio>
class thread_mutex{
public:
    thread_mutex(pthread_mutex_t * mut):_mutex_p(mut)
    {       
         *_mutex_p = PTHREAD_MUTEX_INITIALIZER;     
    }
    void lock()
    {      
        pthread_mutex_lock(_mutex_p);
    }
    void trylock()
    {
        pthread_mutex_trylock(_mutex_p);
    }
    void unlock()
    {
        pthread_mutex_unlock(_mutex_p);
    }
private:
    pthread_mutex_t* _mutex_p;
};
#include<unistd.h>
#include"C++_mutex.hpp"
int counter = 100;
pthread_mutex_t mutex;
void* start_routine(void* args)
{    
    thread_mutex _mutex(&mutex);
    while(true){
        _mutex.lock();
        if (counter > 0){          
            counter--;
            std::cout << "抢票用户:"<< (char*)args << " 剩余票数: " << counter << std::endl;
            _mutex.unlock();
            usleep(1234);                            
        }
        else{
            _mutex.unlock();
            break;
        }  
    }
    return nullptr;
}
int main()
{  
   pthread_t thread;
   char second_thread[] = "次线程";
   char main_thread[] = "主线程";
   pthread_create(&thread, NULL, start_routine, (void*)second_thread);
   start_routine((void*)main_thread);
   pthread_join(thread, NULL);
   return 0;
}

如果觉得解锁用的麻烦,我们可以利用智能指针的思想,写一个析构函数,把解锁接口调用放到析构函数中,等到类的作用域结束自动解锁,这里就不演示了

小技巧:可以用{ }自定义数据类型的的作用域


死锁

锁真是个好东西啊,线程妈妈(进程)再也不怕小线程无序争夺资源,把一切都搞乱了

于是在多线程中,一旦涉及到公共资源争夺,就加一把锁,但是这么一加,就加出了问题

什么问题呢?看标题也明白了——死锁

起因是这样的,现在有公共资源A和B,公共资源A有一把锁,公共资源B也有一把锁,现在有线程1和2,线程1一开始要使用资源A,于是它把锁A给拿走了,线程2一开始要使用资源B,于是它把锁B给拿走了

线程1执行着突然发现需要资源B了,于是它等待线程2把资源B的锁还回来,不然它没法取资源B呀,可是线程2执行着发现需要资源A了,于是它等待线程1把资源A的锁还回来

线程2哪知道线程1还在等它呢?同样线程1也不知道线程2在等它呀,两个人就这样互相等啊等,没有终止,导致程序无法继续执行,这就是死锁问题

可不是只有两个及以上个锁才会造成死锁问题,一个锁也会造成死锁问题,人也会被自己绊倒,更别说一根筋的电脑了

  while(true){
拿这个例子来说,把解锁的接口全给注释了,线程A拿到锁执行完一次循环后,会再次循环,再次申请
加锁,锁本来就在它身上,加锁接口伪代码咱们也看过了,可想而知线程A也将被挂起,且锁也丢了
        _mutex.lock();
        if (counter > 0){          
            counter--;
            std::cout << "抢票用户:"<< (char*)args << " 剩余票数: " << counter << std::endl;
            //_mutex.unlock();
            usleep(1234);                             
        }
        else{
            //_mutex.unlock();
            break;
        }   
    }

避免死锁的方法

出现死锁有四个必要条件

1.互斥(某线程拿到锁,那么其它线程要保持互斥,不能取该锁保护的资源)

2.请求与保持:(死锁时,各线程互相请求对方的锁,互不相让)

3.不剥夺:(各线程平等,无权强行夺取其它线程的锁)

4.环路等待条件 (出现死锁问题组成的无法打破僵局的环路)

避免死锁就是不要同时满足上面的四个必要条件,解决死锁的方法就是打破上面的条件之一

例如使用线程的优先级,死锁时优先级低的线程必须主动释放锁

相关的算法有(死锁检测算法,银行家算法等)


线程同步

什么是线程同步?线程不同步会有什么问题呢?其实咱们前面遇到过这个问题,笔者还解释了大半天,遗忘的同学前往文章开头处查看,用到的代码笔者已复制如下

当时笔者解释了半天,为什么要使用usleep(),因为线程不调度,导致票被主线程这一个线程给抢完了,或者是主线程的竞争能力太强了,它在解锁时同时也是离锁最近的,所以它一解锁,立马就循环再拿锁,次线程是一次锁都摸不到

#include<pthread.h>
#include<iostream>
#include<cstdio>
#include<unistd.h>
int counter = 10;
//创建一个全局锁
pthread_mutex_t mutex;
void* start_routine(void* args)
{     
    while(true){
        //在这里加锁,保护公共资源counter
        pthread_mutex_lock(&mutex);
        if (counter > 0){          
            counter--;
            std::cout << "抢票用户:"<< (char*) args << " 剩余票数: " << counter << std::endl;
            //这里counter--已经执行完毕,公共资源保护完成,可以解锁
            pthread_mutex_unlock(&mutex);
            usleep(1234);                             
        }
        else{
            //这里是程序拿到锁,然而没票了,退出之前要解锁
            pthread_mutex_unlock(&mutex);
            break;
        }   
    }
    return nullptr;
}
int main()
{   
   pthread_t thread;
   char second_thread[] = "次线程";
   char main_thread[] = "主线程";
   //初始化该全局锁
   pthread_mutex_init(&mutex, NULL);
   pthread_create(&thread, NULL, start_routine, (void*)second_thread);
   start_routine((void*)main_thread);
    pthread_join(thread, NULL);
    return 0;
}

你说主线程违背了锁的竞争原则了吗?并没有,它是严格按照锁的竞争规则来的,但是你说这合理吗?这并不合理,主办方把票全部卖给了黄牛,即使你手速逆天,抢票时也只是卡顿一下,然后显示票已卖完

我们要避免票全让一个线程给抢完了,怎么办,简单粗暴的方法就是再加入一个排队系统,刚抢完票的线程不准再抢了,立马去排队,就能让其它的线程有机会抢到票,像这样避免一个线程独占资源而导致其它线程挨饿的就是线程同步(到这里大家应该能体会到,没写过类似的代码,看这部分内容就感觉很虚,一段时间后没有任何印象,所以请务必敲代码,亲自体验这个过程)


生产者与消费者模型

大概了解什么是同步之后,我们来了解一个线程中很重要的模型——生产者与消费者模型

故事来源于生活场景,角色有:顾客,超市,工厂

我们每个人都是顾客,又称为消费者,我们需要商品满足自身生活需求,没有什么好说的。工厂是生产商品的,又称为生产者,有需求,就有人生产嘛,同样没有什么好说的

超市这个角色值得我们聊聊,为什么要有超市呢?

如果顾客直接去工厂买东西不可以吗?顾客作为一个单独的个体,其购买的产品量是很低的,如果每个顾客都来,那每次开机就生产那么一点点东西,赚得钱可能连电费都付不起,同样的,顾客想去买东西,还得等你工厂开机生产,等到生产完,可能天都黑了

可见我们不能把顾客与工厂强关联,需要一个中间区域,在这个中间区域里,顾客随拿随走,工厂可以批量生产有存储区域

可见,超市这个东西就是把顾客和工厂解耦的,如果生产者和消费者强耦合,消费者要等待生产时间,工厂要承担零售成本,有了超市解耦,消费者随拿随走,生产者没有零售成本

大家感觉这个超市的作用熟不熟悉? 不就是咱们前面提到过的缓冲区吗?

顾客消费可以看到超市资源,工厂供货也可以看到超市资源,那么超市不就是公共资源吗?注意指在程序里的公共资源,引入生活中的例子是便于大家理解,但不可把生活中的经验全套入到改模型里来理解该模型


下面看看消费者与生产者之间的关系

消费者与消费者之间:互斥关系

现在超市某个货品只有一个,只有一个消费者可以拿,一个拿了另一个就不能拿了,所以构成互斥关系

生产者与生产者之间:互斥关系

超市的容量是有限的,一个工厂供货了,另一个工厂就不能再供货了,故生产者与生产者之间也构成了互斥关系

生产者与消费者之间:互斥,同步

互斥还是好理解的,生产者正在给超市供货呢,还没开始统计数量呢,消费者直接就进来把商品拿走了,这样是不行的,因此消费者访问公共资源时,生产者就不能访问公共资源,反之亦然,生产者和消费者之间构成互斥关系

假设现在生产者给超市供货,而不断有消费者来购买商品,那么超市和消费者时间都浪费了,也没得到商品。同样,假设没有消费者来超市消费,而不断有生产者前来供货,那么超市和生产者时间都浪费了,也没卖出商品

为了解决这个问题,超市设置了一种同步策略,即有货的时候再通知消费者,没货时消费者不要来,同样,缺货的时候通知生产者,不缺货生产者不要来

上述生产者和消费者模型可以归纳为“321”原则

3种关系(上述的三种关系)

2个角色(生产者线程,消费者线程)

1个交易场所(一段特定结构的缓冲区,即上述超市)

现在有main函数和一个fun函数,main函数调用了fun函数,在正常情况下,main函数调用fun函数之后,fun函数开始执行,而main函数则在等待fun函数执行完毕。同样的main函数还没开始调用fun函数时,fun函数在等待main函数的调用

这里main函数和fun函数就是强耦合的关系,把这两个函数带入到生产者消费者模型

由main函数调用fun函数,所以main函数是参数的提供方(生产者),fun函数是参数的使用方(消费者)如果由两个线程来同时执行这个函数,消费者线程和生产者线程同时开始,同时设立一定的执行条件,当消费者需要数据时,而生产者还没有生产好数据,那么就挂起等待,等到生产者生产好了,满足条件了,就可以唤醒消费者,反之同理


条件变量

什么是条件变量呢? 我们前面提到过,在生产者和消费者模型中,生产者和消费者之间要有互斥和同步的关系,互斥解决方法就是加锁嘛,条件变量就是解决同步的一种常用的方法之一,上面的模型中,超市就是公共资源,消费者消费资源,生产者生产资源,当公共资源没有了,不满足消费者的条件,那么此时消费者不能再来了,老实的挂起等待,反之同理

条件变量维护了一个不满足条件就挂起,满足条件就唤醒的队列,条件变量要和锁一起配合使用,从而满足互斥和同步的关系

所以什么是条件变量呢?其实就是在加锁后,为了维持线程同步,加了一个判断条件,满足条件的可以继续执行,不满足条件的需要挂起等待,条件变量提供了一个队列,这个队列专门用来存放这些不满足条件的线程

消费者线程去超市这个公共资源区去消费,但是此时超市已经没有货物了,所以消费者线程不满足进入超市的条件,去到条件变量里挂起等待,假设此时生产者来了,它检测到超市的货物没有满,可以供货,于是进入了超市这个公共资源区开始供货,同时供货完,它会从条件变量里唤醒一个线程,若该线程是消费者线程,此时判断超市已经有货了,于是它可以进入超市去消费资源了


条件变量的相应接口

条件变量的概念明白了,接下来就看看条件变量如何使用吧。有时你可能会看到笔者在接口和函数之间反复横跳,其实叫接口也好,叫函数也罢,两者是同一个东西,叫函数是从编程语言的角度看待,叫接口是从系统调用的角度来看待的,废话不多说,看接口

条件变量同锁一样,都是一种数据类型,里面包含了各种需要的数据结构

条件变量的数据类型为:pthread_cond_t  

声明一个条件变量: pthread_cond_t  cond;

同样,声明完要进行初始化

初始化:int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);第二个参数填NULL即可

另一种初始化方式:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

int pthread_cond_destroy(pthread_cond_t *cond);

这个比较好理解,就是把你创建的条件变量销毁

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

参数:

cond:要在这个条件变量上等待

mutex:互斥量,后面详细解释

该接口的意思是,如果判断条件不满足,那么就调用该接口,系统会把不满足条件的线程放入到条件变量的等待区中

int pthread_cond_signal(pthread_cond_t *cond);

当满足条件之后,通过调用该接口,系统会通知在条件变量里等待的线程,可以开始执行了,注意该接口只能唤醒一个线程

int pthread_cond_broadcast(pthread_cond_t *cond);

该接口和上面接口的作用是一样的,不过该接口不只是唤醒一个等待区的线程,而是唤醒一批处在等待变量中的线程


条件变量代码实操

废话不多说,直接用起来,下面是测试条件变量的demo

注:为了简便,后面称呼这些接口调用函数只用后缀代替

例如:pthread_cond_wait() 用wait简称,其它接口类似

int counter = 100;
pthread_mutex_t mutex;
pthread_cond_t cond;
void *start_routine(void *args)
{
    thread_mutex _mutex(&mutex);
    while (true)
    {
        _mutex.lock();
        if (counter > 0)
        {
            pthread_cond_wait(&cond, &mutex);
            counter--;
            std::cout << "抢票用户:" << (char *)args << " 剩余票数: " << counter << std::endl;
            _mutex.unlock();
            // usleep(1234);
        }
        else
        {
            _mutex.unlock();
            break;
        }
    }
    return nullptr;
}
int main()
{
    pthread_t thread_1, thread_2;
    cond = PTHREAD_COND_INITIALIZER;
    char name_1[] = "抢票选手1";
    char name_2[] = "抢票选手2";
    pthread_create(&thread_1, NULL, start_routine, (void *)name_1);
    pthread_create(&thread_2, NULL, start_routine, (void *)name_2);
    while (true)
    {
        pthread_cond_signal(&cond);
        sleep(1);
    }
    pthread_join(thread_1, NULL);
    pthread_join(thread_2, NULL);
    return 0;
}

该代码用到了前面对锁的封装,主要思想就是创建两个子线程,两个子线程刚进入抢票就被wait,进入条件变量挂起等待,然后由主线程用signal逐一唤醒挂起的进程,开始抢票


理解条件变量

等等,老是觉得有些不太对劲,其一在使用条件变量的wait时,为什么要把锁传进去。其二我们不是加锁了嘛,既然加锁了就该保持互斥,也就是只有一个线程才能拿到锁,才能遇到wait,然后进入条件变量等待区,为什么上面的demo思想却说让两个子线程都进入条件变量挂起等待,等待主线程逐一将它们唤醒呢

我们的理解并没有错,只是前面没提到在wait时把锁传过去的作用,wait接口把锁传过去就是为了释放锁,因为释放了锁,其它线程才有机会进入公共资源区。假设现在有一个生产者线程和一个消费者线程,此时公共资源区已经没有资源了,所以消费者进来就得把它挂起等待,如果它把锁也带到等待区而不释放,那么生产者就进入不了公共资源区,那程序就卡死了,生产者永远进不去,消费者永远醒不来


基于BlockingQueue的生产者消费者模型

看标题是不是觉得特别深奥,但仔细看不就是基于阻塞队列的生产者消费者模型嘛,啥意思呢?就是建立一个队列,这个队列的大小是固定的,生产者不断往这个队列中写入数据,消费者不断往这个队列拿出数据,当写入速度明显大于拿出速度时,队列将被写满,就会触发条件判断,把生产者都放到条件变量等待区里。同样的,当拿出速度明显大于写入速度导致队列里没有数据了,会触发条件判断,将消费者放入条件变量的等待区里

了解功能大家可以自行实现,请务必自行实现,下面笔者给出一些参考代码

#include<iostream>
#include<pthread.h>
#include<queue>
#include<vector>
#include<ctime>
#include<cstdlib>
#include<unistd.h>
template< class T>
class thread_cond{
public:
    thread_cond()
    {
        _cond = PTHREAD_COND_INITIALIZER;
    }
    void thread_cond_push(T data, pthread_mutex_t *mutex)
    {
        if (_buffer.size() >= _capacity)
        {   
            pthread_cond_wait(&_cond, mutex);
        }
        _buffer.push(data);
        pthread_cond_signal(&_cond); 
    }
    T thread_cond_get(pthread_mutex_t *mutex)
    {
        if (_buffer.size() <= 0)
        {
            pthread_cond_wait(&_cond, mutex);
        }
        T rt_val = _buffer.front();
        _buffer.pop();
        pthread_cond_signal(&_cond);
        return rt_val; 
    }
    ~thread_cond()
    {
        pthread_cond_destroy(&_cond);
    }
private:
    std::queue<T> _buffer;
    pthread_cond_t _cond;
    size_t _capacity = 100;
};
----------------------------------------------------------------------------
pthread_mutex_t mutex;
void* start_get(void *args)
{
    thread_cond<int> * test_cond = (thread_cond<int>*)args;
    while(true)
    {
        pthread_mutex_lock(&mutex);
        int temp = test_cond->thread_cond_get(&mutex);
        std::cout << "线程:"<<pthread_self() << "拿出数据" << temp <<std::endl;
        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
}
void* start_push(void *args)
{
    thread_cond<int> * test_cond = (thread_cond<int>*)args;
    while(true)
    {
        pthread_mutex_lock(&mutex);
        int temp = rand()%100;
        test_cond->thread_cond_push(temp, &mutex);
        std::cout << "线程:"<<pthread_self() << "载入数据" << temp <<std::endl;
        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
}
int main()
{
    thread_cond<int> test_cond;
    pthread_mutex_init(&mutex, NULL);
    std::vector<pthread_t> threads(4);
    pthread_create(&threads[0], NULL, start_get, (void*)&test_cond);
    pthread_create(&threads[1], NULL, start_get, (void*)&test_cond);
    pthread_create(&threads[2], NULL, start_push, (void*)&test_cond);
    pthread_create(&threads[3], NULL, start_push, (void*)&test_cond);
    for (int i = 0; i < 4; i++)
    {
        pthread_join(threads[i], NULL);
    }
    return 0;
}

用模板来编写该类,主要是为了支持泛型,方便日后工作的扩展,现在我们传过去的是int类型的数据,日后可能会是一个自定义类型的类


生产者消费者模型的意义

下图是生产者,消费者模型的简述图

你会不会有这样的疑问,我们引入多线程就是为了实现程序并发,提高工作效率。但是这个生产者消费者模型,消费者想拿数据还是只能加锁一个一个的进入,同样的生产者想放入数据也是只能加锁一个一个的进入,甚至消费者和生产者之间也不能同时进入,那这并发的意义在哪里呢?既然都是一个一个的串入式进入,为何不用单线程呢?

有这样的疑惑是因为我们正在讲述生产者消费者模型,大家都把目光聚焦到这个模型中,但是这个模型仅仅是负责资源的纳入和分配,而不涉及资源的生产和处理

什么意思呢?消费者来到公共资源区是为了拿取资源,而真正消耗时间的是资源的消费过程,但是这个过程并不是在公共资源区内部进行的

同样的,生产者把生产好的数据放入公共资源区,真正消耗时间的是资源的生产过程,这个生产过程也不是在公共资源区进行的

所以多线程并发的意义并不在公共资源区中串型式的拿取增添数据,而是并行式的消费资源和生产资源,加锁是保证资源拿取和增添的安全有效,这个过程并不怎么消耗时间


目录
相关文章
|
3天前
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####
|
6天前
|
数据采集 存储 数据处理
Python中的多线程编程及其在数据处理中的应用
本文深入探讨了Python中多线程编程的概念、原理和实现方法,并详细介绍了其在数据处理领域的应用。通过对比单线程与多线程的性能差异,展示了多线程编程在提升程序运行效率方面的显著优势。文章还提供了实际案例,帮助读者更好地理解和掌握多线程编程技术。
|
5天前
|
API Android开发 iOS开发
深入探索Android与iOS的多线程编程差异
在移动应用开发领域,多线程编程是提高应用性能和响应性的关键。本文将对比分析Android和iOS两大平台在多线程处理上的不同实现机制,探讨它们各自的优势与局限性,并通过实例展示如何在这两个平台上进行有效的多线程编程。通过深入了解这些差异,开发者可以更好地选择适合自己项目需求的技术和策略,从而优化应用的性能和用户体验。
|
10天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
19天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
16天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
19天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
27天前
|
安全 程序员 API
|
20天前
|
安全 Java 编译器
Java多线程编程的陷阱与最佳实践####
【10月更文挑战第29天】 本文深入探讨了Java多线程编程中的常见陷阱,如竞态条件、死锁、内存一致性错误等,并通过实例分析揭示了这些陷阱的成因。同时,文章也分享了一系列最佳实践,包括使用volatile关键字、原子类、线程安全集合以及并发框架(如java.util.concurrent包下的工具类),帮助开发者有效避免多线程编程中的问题,提升应用的稳定性和性能。 ####
46 1
|
23天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####