linux多线程(二)

多线程访问出现问题的例子

假设我们现在有1000张票,4个线程来抢。没有线程互斥与同步机制我们来看下会发生什么

void* Routine( void * args)
{
    const char* name = static_cast<const char*> (args);
 while (true)
    {
        if(tickets > 0)
        {
            usleep(1000);
            std::cout << name << " get tickets" << tickets << std::endl;
            tickets--;
        }else
        {
            break;
        }
      
       // sleep(1);
      
    }
    return nullptr;
}
int main()
{
    pthread_t tids[3];
    for(int i = 0 ; i < 3 ;i++)
    {
        char * name = new char[1024];
        snprintf(name,1024,"thread-%d",i+1);
        pthread_create(tids + i,nullptr,Routine,name);
    }
    for(int i = 0 ; i< 3; i++)
    {
        pthread_join(tids[i],nullptr);
    }
    
    return 0;
}

在这里插入图片描述

我们发现tickets 出现了负数 意味着没有票了还在卖~

原因解释

因为cpu的寄存器只有一套,而寄存器里面的数据可以有多套。
有一个前置知识就是 if 判断 和 – 操作都不是原子的。 所谓原子就是一个整体,这个步骤不能再被切割了。 再具体一点就是一条语句只对应一条汇编就是原子的。
有一个关键点就是这里usleep(1000) 三个线程 都有很大概率进入if条件了(因为在–前睡眠了)。但是 我们减减那里虽然可以并行但是没有睡眠概率比较小,就依次减减了。
在这里插入图片描述
如果我们在–前加sleep 加大并行的概率,那么我们的–操作中第二步减数据就被覆盖了,就不会出现每个线程都有效减一的情况。
在这里插入图片描述

解决这个问题

就要引入今天的主题了线程互斥~
我们先介绍几个概念
临界资源:就是是在多线程或多进程环境中,被多个线程或进程共享的一些资源。
临界区:界区是指一段代码或者程序的一个部分,这段代码需要访问共享资源,
对临界资源的保护就是对临界区代码的保护~

互搓锁

为了解决这个问题我们可以对临界区进行加锁~

互斥锁的接口

应用全局的互斥锁解决这个问题

全局锁只需要初始化 不需要destory

pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;
//当你声明一个 pthread_mutex_t 类型的全局变量或静态变量,并使用 PTHREAD_MUTEX_INITIALIZER 初始化时,你实际上已经在编译时为这个互斥锁分配了存储空间并初始化了它。
//这意味着在程序运行之前,互斥锁就已经处于一个有效且可用的状态。
// 就不需要用 pthread_mutex_init 了
void* Routine( void * args)
{
    const char* name = static_cast<const char*> (args);
    while (true)
    {
        pthread_mutex_lock(&gmutex);
        if(tickets > 0)
        {
            usleep(1000);
            std::cout << name << " get tickets" << tickets << std::endl; // 1
            
            tickets--;
            pthread_mutex_unlock(&gmutex); // 1
            
        }else
        {
            pthread_mutex_unlock(&gmutex);
            break;
        }
       // pthread_mutex_unlock(&gmutex); // 2
      // 锁的位置用1 这样 不用 2 是因为如果减到0走了else 这个锁就没释放 其他线程就被阻塞在锁那里了
       
      
    }
    return nullptr;
}

int main()
{
    pthread_t tids[4];
    pthread_mutex_init(&gmutex,nullptr);
    for(int i = 0 ; i < 4 ;i++)
    {
        char * name = new char[1024];
        snprintf(name,1024,"thread-%d",i+1);
        pthread_create(tids + i,nullptr,Routine,name);
    }
    for(int i = 0 ; i< 4; i++)
    {
        pthread_join(tids[i],nullptr);
    }
    return 0;
}

应用局部锁

因为我们为了传局部锁给每个线程和给每个线程名字。所以必须把这两个变量封装这一个类。才能同时都给线程~

class ThreadData
{
public:
    char * _name;
    pthread_mutex_t* _pmutex;
public:
    ThreadData(char* name, pthread_mutex_t* pmutex)
    :_name(name),_pmutex(pmutex)
    {
        
    }

};
void* Routine( void * args)
{
    ThreadData * td = static_cast<ThreadData*> (args);
    while (true)
    {
        pthread_mutex_lock(td->_pmutex);
        if(tickets > 0)
        {
            usleep(1000);
            std::cout << td->_name << " get tickets" << tickets << std::endl; // 1
            
            tickets--;
            pthread_mutex_unlock(td->_pmutex); // 1
            
        }else
        {
            pthread_mutex_unlock(td->_pmutex);
            break;
        }
      //  pthread_mutex_unlock(&mutex); // 2
      
       
      
    }
    return nullptr;
}


int main()
{
    pthread_t tids[4];
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex,nullptr);
    for(int i = 0 ; i < 4 ;i++)
    {
        char * name = new char[1024];
        snprintf(name,1024,"thread-%d",i+1);
        ThreadData td(name,&mutex);
        pthread_create(tids + i,nullptr,Routine,&td);
    }
    for(int i = 0 ; i< 4; i++)
    {
        pthread_join(tids[i],nullptr);
    }
    pthread_mutex_destroy(&mutex);

    return 0;
}

线程加锁注意事项

在这里插入图片描述

从原理的角度认识互斥锁

就是mutex 申请成功就返回,失败就就堵在pthread_mutex_lock() 函数内部。
堵塞在某个函数内部,这个其实我们经常遇见比如我们用的scanf 输入完成前就是堵塞在scanf函数内部

从实现的角度理解互斥锁

xchgb 交换 这条汇编 和我们写的swao不一样 它是用硬件实现的。
在这里插入图片描述
先把 寄存器al的值赋为0,然后把锁(所里面的内容1) 交换。 谁得到这个1就可以返回出pthread_muex_lock函数。

条件变量

直观认识条件变量

有一个vip自习室 只能运行一个人拿到锁进去~

在这里插入图片描述
午饭时间到了小明肚子开始饿了,准备出去吃午饭,于是他就把锁挂在墙上,但是他想这自习室这么好有空调,又安静~别人也想拿到锁进去。于是他放弃干饭,继续学习了。因为它离锁更近,一下就从墙上拿到了。此时小红和小刚就不同意了。
在这里插入图片描述
在这里插入图片描述
为了解决这个问题,图书管理员想了个办法,就是出去就重新排队!
在这里插入图片描述
这个队列就相当于条件变量!
让线程表现出一定的顺序关系称为同步~
一次只让一个线程访问临界资源称为互斥
互斥是为了避免数据的二义性,而同步是让线程访问公共资源更加合理。
图书管理员在哪喊号,就类似于唤醒条件变量下的线程!
这个顺序一定时先进先出吗?不一定 因为signal()操作会选择一个线程从该队列中唤醒。但是,具体选择哪个线程被唤醒通常依赖于底层操作系统调度器的策略,这可能基于先来先服务(FIFO)、优先级、或者其它一些算法。 所以你不能依赖于signal()唤醒特定的线程,除非你的系统明确支持某种特定的唤醒策略,并且你的程序逻辑可以处理任何可能的唤醒顺序

接口

在这里插入图片描述

熟悉接口的测试代码

pthread_mutex_t gmutex= PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t gcond = PTHREAD_COND_INITIALIZER;

void * Wait(void * args)
{
    char *name = (char *)args;
    while(true)
    {
        pthread_mutex_lock(&gmutex);
        pthread_cond_wait(&gcond,&gmutex); 
        std::cout<<name<<std::endl;
       // sleep(1);
        pthread_mutex_unlock(&gmutex);
    }

    return nullptr;
}

int main()
{
    pthread_t tids[5];
    for(int i = 0; i < 5; i++)
    {
        char * name = new char[1024];
        snprintf(name,1024,"thread-%d",i+1);
        usleep(10000);
        pthread_create(tids+i,nullptr,Wait ,(void*)name);
        
    }
    sleep(1);

    while(true)
    {
        pthread_cond_signal(&gcond);
        sleep(1);
    }
    for(auto tid: tids)
    {
        pthread_join(tid,nullptr);
    }
    return 0;
}

解决抢火车票每个线程不均匀的问题

int tickets = 10000;
class ThreadData
{
public:
    char * _name;
    pthread_mutex_t* _pmutex;
    pthread_cond_t * _pcond;
public:
    ThreadData(char* name, pthread_mutex_t* pmutex,pthread_cond_t * pcond)
    :_name(name),_pmutex(pmutex),_pcond(pcond)
    {
        
    }
    ~ThreadData()
    {
        delete[] _name;
    }

};
pthread_cond_t cond[4] = {PTHREAD_COND_INITIALIZER};
int next = 0; // cond 的下标
void* Routine( void * args)
{
    ThreadData * td = static_cast<ThreadData*> (args);
    while (true)
    {
        pthread_mutex_lock(td->_pmutex);
       
        while(td->_pcond != cond+next) // 一定要注意最后唤醒的cond+next下的线程 否则会重新睡眠
        {
            pthread_cond_wait(td->_pcond,td->_pmutex);
           
        }
        if(tickets > 0)
        {
            
            std::cout << td->_name << " get tickets" << tickets << std::endl; // 1
            tickets--;
            next = (++next) % 4;
            pthread_mutex_unlock(td->_pmutex); // 1
        }else
        {
            next = (++next) % 4; // 按顺序退出,唤醒对应的条件变量下的进程
            pthread_mutex_unlock(td->_pmutex);
            break;
        }
      //  pthread_mutex_unlock(&mutex); // 2
    }
    return nullptr;
}


int main()
{
    pthread_t tids[4];
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex,nullptr);
   
    for(int i = 0 ; i < 4 ;i++)
    {
        char * name = new char[1024];
        snprintf(name,1024,"thread-%d",i+1);
        ThreadData *td = new ThreadData(name,&mutex,cond+i);  // important! crucial
        pthread_create(tids + i,nullptr,Routine,td);
        
    }
    int k = 0;
    while(tickets > 0)
    {
        pthread_cond_signal(cond+k);
        k++;
        k %= 4;
    }
    int cur = next;

    for(int i = 0 ; i< 4; i++)
    {
        pthread_cond_signal(cond+cur); // 把next对应的 线程唤醒
                                        // 如果不是则会重新睡眠
        cur = (++cur) % 4;
        usleep(100);
    }

    //std::cout<<"开始 join"<<std::endl;
    for(int i = 0 ; i< 4; i++)
    {
        pthread_join(tids[i],nullptr);
    }
    pthread_mutex_destroy(&mutex);

    return 0;
}

在这里插入图片描述
这样每个线程就表现出了一定的顺序性即同步关系~

生产消费者模型

在这里插入图片描述
这个模型可以解决忙闲不均的问题。比如说供应商生产的很快,消费者消费的很慢,以至于把超市的货架都塞满了,我们让供应商先停止生产。超市做了个缓存,比如说要过年了工厂关门了,但是超市上还是有商品供我们选择。
效率高因为我们我们消费的时候,供应商正在生产,我们消费行为和供应商生产行为(准备发生的数据)并发了。 我们在用商品时(处理数据)和供应商放商品到货架上也并发了。
生产者消费者完成解耦,解耦是指减少生产者和消费者之间的直接依赖关系,使两者可以独立工作,某个生产者或者消费者异常了并不影响对方。

有一个关键点 记忆有一个口诀 321原则
1.一个交易场所(特点数据结构形式存在的一段内存空间)
2.两种角色 生产线程和消费线程
3.三种关系(生产者和生产者,生产者和消费者,消费者和消费者)

消费者和生产者之间是互斥的,比如我正在写hello word,你就来读了,可能你读到的就只有hello。我正在读你就来写了,可能你把之前写的覆盖了,我读到的就是垃圾数据。但是超市空了,消费者就必须阻塞,超市满了生产者就必须阻塞,他们又表现出同步关系。
生产者和生产者之间也是互斥,同行是冤家嘛~ 因为同时往一个队列里面写数据,可能造成覆盖。
消费者和消费者之间也是互斥的,因为例如,如果两个消费者同时尝试从队列中取出一个元素,可能会导致一个元素被取出两次,或者队列状态混乱。

我们用 互斥锁和条件变量来实现

#include"BlockQueue.hpp"
#include<pthread.h>
#include<ctime>
#include<sys/types.h>
#include<stdlib.h>
#include<unistd.h>
#include<iostream>
#include"task.hpp"

void * producer(void *args)
{
    BlockQueue<task_t>* blockqueue = static_cast<BlockQueue<task_t>*>(args);
    srand(time(nullptr)^getpid());
    while(true)
    {
        // 生产

        // int x = rand()%10 + 1;
        // usleep(10000);
        // int y = rand()%10 + 1;
     //   Task a(x,y) ;
        blockqueue->equeue(DownLoad);
        std::cout<<"prodecder:"<<"send task"<<std::endl;
        sleep(1);
    }
    return nullptr;
}

void * consumer(void *args)
{
    BlockQueue<task_t>* blockqueue = static_cast<BlockQueue<task_t>*>(args);
    while(true)
    {
        //消费
        task_t data;
        blockqueue->pop(&data);
        data();
        //data.operator();
       // task_t();
        //std::cout<<"consumer:"<<data.solve()<<std::endl;
    }
    return nullptr;
}
int main()
{
    
    pthread_t c[2],p[2];
 
    BlockQueue<task_t>* blockqueue = new BlockQueue<task_t>();
    for(int i = 0; i < 2; i++)
    {
         pthread_create(c+i,nullptr,consumer,(void*)blockqueue);
         pthread_create(p+i,nullptr,producer,(void*)blockqueue);
    }
    
    for(int i = 0; i < 2; i++)
    {
        pthread_join(*(p+i),nullptr);
        pthread_join(*(c+i),nullptr);
    }

    return 0;
}
#include<queue>
#include<pthread.h>
#include<iostream>
template <class T>
class BlockQueue
{
    int _cap_max;
    std::queue<T> q;
    pthread_mutex_t _mutex;
    pthread_cond_t _c_s; // 把消费者和生产者放在两个不同的条件变量下 
    pthread_cond_t _p_s;// 唤醒控制比较简单
private:
    bool isFull()
    {
        return q.size() == _cap_max;
    }
    bool isEmpty()
    {
        return q.empty();
    }
public:
    BlockQueue()
    :_cap_max(5)
    {
        pthread_mutex_init(&_mutex,nullptr);
        pthread_cond_init(&_c_s,nullptr);
        pthread_cond_init(&_p_s,nullptr);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_c_s);
        pthread_cond_destroy(&_p_s);
    }
   void equeue(const T &in)
   {
        pthread_mutex_lock(&_mutex);
        while(isFull())
        {
       //     std::cout<<"isfull"<<std::endl;
            pthread_cond_wait(&_p_s,&_mutex); // 阻塞的时候释放锁 返回的时候参与锁的竞争
        }
        q.push(in);

        pthread_mutex_unlock(&_mutex); // 先解锁还是先唤醒都可以
        
        								// 如果sinal时消费者没有在wait里,signal没有意思 随
        								//便放
        								
                                       // 如果先唤醒(唤醒有意义的讨论就是在线程阻塞在wait里) 因为锁可能没释放 
                                       //消费者pthread_cond_wait(&_p_s,&_mutex);
                                       //等待在锁那直到生产者解锁 如果直接释放了锁消费者直接运行
                                       
                                       // 如果先解锁 生产者 再唤醒,消费者从条件变量里竞争锁,           
                                       // 生产者从下一次的equeue中的pthread_mutex_lock竞争
                                       //锁 如果生产者直接成功了就完事大吉 没成功也无妨因为此
                                       //时还是空的状态 消费者被条件变量的wait捕获 重新释放锁
        pthread_cond_signal(&_c_s);
   }
   void pop(T * out)
   {
        pthread_mutex_lock(&_mutex);
        while(isEmpty())
        {
            pthread_cond_wait(&_c_s,&_mutex);
        }
        *out = q.front();
        q.pop();
        pthread_mutex_unlock(&_mutex);
        pthread_cond_signal(&_p_s); /// 2 
        
   }
};

信号量

信号量本质是一个计数器,用来描述公共资源的数量~类似于电影票对资源进行预定
刚刚我们把用互斥锁和条件变量的维护的阻塞队列就是整体使用的!

在这里插入图片描述

认识信号量的接口

在这里插入图片描述

测试信号量接口的代码 三个线程按顺序抢票

#include <iostream>
#include <semaphore.h>
#include <string>
#include <unistd.h>
#include <vector>

#include "thread.hpp"
#include "lockerguard.hpp"

using namespace ThreadModel;
int cnt = 1;

int tickets = 10000;
class ThreadData
{
public:
    char *_name;

    sem_t *_psem;

public:
    ThreadData(char *name, sem_t *psem) : _name(name), _psem(psem)
    {
        // 本线程可见 信号量初始值为0
        // sem_init(&_sem,0,0);
    }
    ~ThreadData()
    {
        delete[] _name;
    }
};
sem_t sems[4];
int turn = 1;
void P(sem_t *psem)
{
    sem_wait(psem);
}
void V(sem_t *psem)
{
    sem_post(psem);
}

void *Routine(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    while (true)
    {
        int idx = td->_psem - sems;
        while (idx != turn)
        {
            P(td->_psem); // 第2,3,线程全sleep
        }
        if(tickets > 0)
        {
            tickets--;
            std::cout << td->_name << "get tickets" << tickets << std::endl;
            turn = (++turn) % 3 + 1;
            V(sems+turn);
            
        }else
        {
            turn = (++turn) % 3 + 1;
            V(sems+turn);
            break;
        }
    }

    return nullptr;
}

int main()
{
    pthread_t tids[4];

    for (int i = 0; i < 4; i++)
    {
        sem_init(sems + i, 0, 1);
    }

    for (int i = 1; i <= 3; i++)
    {
        char *name = new char[1024];
        snprintf(name, 1024, "thread-%d", i);
        ThreadData *td = new ThreadData(name, sems + i);
        pthread_create(tids + i, nullptr, Routine, td);
    }

    for (int i = 1; i <= 3; i++)
    {
        pthread_join(tids[i], nullptr);
    }
    for (int i = 1; i <= 3; i++)
    {
        sem_destroy(sems + i);
    }

    return 0;
}

用信号量实现生产消费者模型

我们使用的数据结构是环形队列,因为这样可以不被整体使用。

环形队列

如果没有计数器,我们需要浪费一个格子,才能区分空和满,空:head = tail;满 (tail+ 1)% N = head。巧合的是信号量就相当于计数器了。size =0 时为空 size =N 时为满
在这里插入图片描述
1.队列为空的时候,谁先访问?
我们只能让生产者先生产

2.队列满了谁应该先访问?
让消费者来消费

3 . 不为空又不为满,head,tail 下标一定不在同一个位置 所以可以实现 生产者和消费者同时访问共享资源的不同部分

消费者和生产者认为的资源是不同的,消费者认为生产的数据是资源,生产者认为还剩的空间为资源。所以我们需要两个信号量,一个 sem_data 另一个sem_size

信号量的设置

我们把sem_data 初始化为0 sem_size 初始化为n
验证 最开始如果消费者先动,则sem_data 为 0 P操作时会阻塞等待,从而保证的生产者先生产。
当队列满的时候 sem_data = N ,sem_size = 0, 此时生产者再生产对sem_size进行P操作 则会阻塞等待,保证了队列满的时候 消费者先消费。
当不为空不为满的时候,生产者和消费者控制的head和tail都可以动。
如果head = tail 了就变为满或者空的情况了。
在这里插入图片描述

#include <vector>
#include <pthread.h>
#include <semaphore.h>

template<class T>
class BlockQueue
{
private:
    void P(sem_t* sem)
    {
        sem_wait(sem);
    }
    void V(sem_t *sem)
    {
        sem_post(sem);
    }
public:
    BlockQueue(int cap = 4)
    :_capmax(cap),_end(0),_start(0),_blockqueue(cap)
    {
        sem_init(&sem_data,0,0);
        sem_init(&sem_cap,0,_capmax);
        pthread_mutex_init(&_p_mutex,nullptr);
        pthread_mutex_init(&_c_mutex,nullptr);
    }
    
    ~BlockQueue()
    {
        sem_destroy(&sem_data);
        sem_destroy(&sem_cap);
        pthread_mutex_destroy(&_p_mutex);
        pthread_mutex_destroy(&_c_mutex);
    }
    void equeue(const T& data)
    {
        // 生产者
        P(&sem_cap);//并发的买票
        pthread_mutex_lock(&_p_mutex); // 这里加锁是因为生产者之间要互斥,避免数据覆盖
        _blockqueue[_end] = data; 
        _end++;
        _end %= _capmax; 
        pthread_mutex_unlock(&_p_mutex);
        V(&sem_data); // 空了,假如线程1还没释放信号量 线程2在P等待 直到释放信号量
         //P(&sem_data);            // 如果先释放信号量 可能线程1没解锁 线程2阻塞在锁里 直到线程1释放锁
         // pthread_mutex_unlock(&_p_mutex);   // 如果线程1解锁了 那直接ok


    }

    void pop( T * out)
    {
        // 消费者
        P(&sem_data);
        pthread_mutex_lock(&_c_mutex);// 消费者互斥是防止队列的一份数据给了两个消费者
        *out = _blockqueue[_start];
        _start++;
        _start %= _capmax;
        pthread_mutex_unlock(&_c_mutex); 
        V(&sem_cap);
    }
private:
    std::vector<T> _blockqueue;
    int _start;
    int _end;
    int _capmax;
    sem_t sem_data;
    sem_t sem_cap;
    pthread_mutex_t _c_mutex;
    pthread_mutex_t _p_mutex;
    
};
  • 21
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值