众所周知,线程是共享地址空间的,线程会共享大部分资源,所以会造成数据不一致问题,这个多线程执行力流被保护的共享资源的代码,就叫作临界资源,而每个线程内部,访问临界资源的代码,就叫做临界区

如下图:

#include 
#include 
#include 
#include 
#include 
int ticket = 100;
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;
		}
	}
}
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);
} 
一次执⾏结果:
thread 4 sells ticket:100
	...
	thread 4 sells ticket:1
	thread 2 sells ticket:0
	thread 1 sells ticket:-1
	thread 3 sells ticket:-2

在上面代码中票已经变成负数了
所以互斥是任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
所以我们要如何去解决上述问题呢?我们就引入锁的概念

锁也是pthread库提供

定义锁

1.静态分配:
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER
2动态分配:

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

参数:
mutex:要初始化的互斥量

attr:NULL

注意:动态分配需要去释放锁:int pthread_mutex_destroy(pthread_mutex_t *mutex);
使用锁
加锁:竞争申请锁,多线程锁被本来就是临界资源,所以申请锁的过程必须是原子的

成功:继续向后运行,访问临界区代码,访问临界资源

失败:阻塞挂起申请执行流

加锁与解锁:

int pthread_mutex_lock(pthread_mutex_t *mutex);

int pthread_mutex_unlock(pthread_mutex_t *mutex);

返回值:成功返回0,失败返回错误号
int pthread_mutex_trylock(pthread_mutex_t *mutex);申请非阻塞版本

锁提供的本质:执行临界区代码由并行转化为串行

锁的原理:

硬件实现:关闭时钟中断

软件实现:为了实现互斥锁操作,大多数体系结构都是供了swap或exchange指令,

该指令的作用是把寄存器和内存单元的数据相交换

我们用swap,exchange将内存中的变量,交换到cpu的寄存器中:

本质是检测al里面的内容看是否>0,不满足条件就挂起,然后让满足条件的去切换回来

面向对象去封装使用:

#pragma once
#include
#include
namespace MutexMudole{
    class Mutex{
        private:
        pthread_mutex_t mutex;
        public:
        Mutex(){
            pthread_mutex_init(&mutex,NULL);
        }
        ~Mutex(){
            pthread_mutex_destroy(&mutex);
        }
        void Lock(){
            pthread_mutex_lock(&mutex);
        }
        void UnLock(){
            pthread_mutex_unlock(&mutex);
        }
    };
}

此时我们就可以直接使用了

但是此时我们能不能让他自己释放呢,而不是手动去释放锁,此时我们就只需要再加一个类就可以完成下面需求

class LockGuard{
        public:
        LockGuard(Mutex& mutex):mutex_(mutex){
            mutex_.Lock();
        }
        ~LockGuard(){
            mutex_.UnLock();
        }
        private:
        Mutex& mutex_;
    };

所以原来的代码就变成了这样(下面),就可以实现加锁操作

#include 
#include 
#include 
#include 
#include
#include
#include 
#include"Mutex.hpp"
using namespace MutexMudole;
//pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;
int ticket = 100;
class ThreadData{
    public:
    std::string name;
    Mutex *lockp;
    ThreadData(const std::string& name,Mutex &lockp):name(name),lockp(&lockp) {}
};
void *route(void *arg)
{
	ThreadData *td=(ThreadData*)arg;
	while ( 1 ) {
		LockGuard guard(*td->lock);//加锁完成。RAII风格的实现互斥锁
		if ( ticket > 0 ) {
			usleep(1000);
			printf("%s sells ticket:%d\n", td->name.c_str(), ticket);
			ticket--;
		} else {
			break;
		}
	}
}
int main( void )
{
	Mutex lock;
	pthread_t t1, t2, t3, t4;
    ThreadData *td1=new ThreadData("thread 1",lock);
    ThreadData *td2=new ThreadData("thread 2",lock);
    ThreadData *td3=new ThreadData("thread 3",lock);
    ThreadData *td4=new ThreadData("thread 4",lock);
	pthread_create(&t1, NULL, route, (void*)td1);
	pthread_create(&t2, NULL, route, (void*)td2);
	pthread_create(&t3, NULL, route, (void*)td3);
	pthread_create(&t4, NULL, route, (void*)td4);
	pthread_join(t1, NULL);
	pthread_join(t2, NULL);
	pthread_join(t3, NULL);
	pthread_join(t4, NULL);
    return 0;
} 

LockGuard guard(*td->lock),此时就只需要去使用这个临时变量就可以完成加锁操作

上面也就是c++里面lock_guard和mutex的底层原理

二.线程同步

同步:

当一个线程高频的申请锁,会造成线程饥饿问题,所以让线程安全的前提下让所有执行流,访问临界资源,按照一定的顺序进行访问资源就叫作:同步

条件变量:

当一个线程互斥地访问其个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。

例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就

需要用到条件变量。

条件变量接口:

他与锁有些相似,但条件变量是用来保证线程同步的,锁是保证线程互斥的

第一个是唤醒指定变量中的所有线程,第二个接口则是唤醒其中一个线程

下面是一个小demo:

#include
#include
#include
#include
#include
pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
#define NUM 5
int cnt=1000;
void* thread_func(void*arg){
    std::string name=(char*)arg;
    while(true){
        pthread_mutex_lock(&lock);
        //直接让线程进行等待
        pthread_cond_wait(&cond,&lock);
        //线程被唤醒后,进行操作
        std::cout<<" wake up"< threads;
    for(int i=0;i<5;i++){
        pthread_t tid;
        std::string name="thread"+std::to_string(i);
       int ret=pthread_create(&tid,NULL,thread_func,(void*)name.c_str());
       if(ret!=0){
           continue;
       }
       threads.push_back(tid);
       sleep(1);
    }
    while(true){
        //pthread_cond_signal(&cond);
        pthread_cond_broadcast(&cond);
        sleep(1);
    }
    for(auto&tid:threads){
        int m=pthread_join(tid,NULL);
    }
    return 0;
}

三.生产者消费者模型:

本质:多线程通信

理解

生产者和生产者:竞争关系,互斥关系

消费者和消费者: 互斥关系

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

2中角色:生产者角色和消费者角色(线程承担)

1个交易场所:以特定结构构成的一直"内存"空间

生产者消费者模型的好处

1.生产过程和消费过程解耦

2.支持忙闲不均

3.提高效率

例子:管道间通信,一个写一个读

代码示例:

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

阻塞队列图:

在多线程编程中阻蒸队列(blockingqueue)是一种常用于实现生产者和消费者模型的数据结构。

其与普通的队列区别在于,当队列为空时,从队列获取元索的操作将会被阻塞,直到队列中被放

入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(上

的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)

阻塞队列是一个容量具有上限的队列。不满足读写条件时,会被阻塞

 #pragma once
#include
#include
#include
#include
const int default_capacity=5;
template
class BlockQueue{
public:
     BlockQueue(int cap=default_capacity):_cap(cap),csleep_num(0),psleep_num(0){
        pthread_mutex_init(&_mutex,NULL);
        pthread_cond_init(&_full_cond,NULL);
        pthread_cond_init(&_empty_cond,NULL);
     }
     void Equeue(const T&val){
        //生产者调用
        pthread_mutex_lock(&_mutex);
        while(IsFull()){
            //阻塞等待
            //当被唤醒时必须还要竞争_mutex这把锁才能向下运行
            psleep_num++;
            //伪唤醒:条件不满足也被唤醒了
            pthread_cond_wait(&_full_cond,&_mutex);
            psleep_num--;
        }
        //使用while再检查一次100%确定不是伪唤醒
            //入队
            q.push(val);
            //通知消费者
            if(csleep_num>0){
                pthread_cond_signal(&_empty_cond);
            }
            pthread_mutex_unlock(&_mutex);
     }
     T Pop(){
        //消费者调用
        pthread_mutex_lock(&_mutex);
        while(IsEmpty()){
            //阻塞等待
            csleep_num++;
            pthread_cond_wait(&_empty_cond,&_mutex);
            csleep_num--;
        }
            //出队
            T val=q.front();
            q.pop();
            //通知生产者
            if(psleep_num>0){
                pthread_cond_signal(&_full_cond);
            }
            pthread_mutex_unlock(&_mutex);
            return val;
     }
     ~BlockQueue(){
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_full_cond);
        pthread_cond_destroy(&_empty_cond);
     }
private:
     bool IsFull(){
        return q.size()==_cap;
     }
     bool IsEmpty(){
        return q.empty();
     }
    std::queue q;
    int _cap;//容量大小
    pthread_mutex_t _mutex;
    pthread_cond_t _full_cond;//生产者唤醒条件变量
    pthread_cond_t _empty_cond;//消费者唤醒条件变量
    int csleep_num;//消费者的个数
    int psleep_num;//生产者的个数
};

生产者消费者提高生产效率体现在 于未来获取任务和处理具体任务,是并发

今天的更新就到这里,如有错误欢迎评论区指出