Linux多线程

多线程

如何创建多线程?

vector<pthread_t> tids;//存储线程的tid
for (int i = 1; i <= NUM; i++)
{
    pthread_t tid;
    pthread_create(&tid, nullptr, routine, nullptr);
    tids.push_back(tid);
}

这里的线程都共用一个函数

线程安全

为什么会出现线程切换不安全的问题?
线程在对某个变量进行进行修改的过程是怎么样的?
线程切换是怎么切换的?
当一个线程在运行时,它会把它的数据先加载到CPU中,然后在CPU对这些数据进行一系列操作后,再由CPU将数据写回到内存中。当线程切换时,这个线程就会把它的上下文数据保存到该线程对应的硬件上下文中,注意此时并不是把上下文数据写回到内存,而是保存到它的硬件上下文中,然后下一个线程将他的数据再加载到CPU中,如果此时这两个线程对同一个变量操作,例如第一个线程读取a时,a = 10在它还没有操作时,就被切换了,那么a =10就被保存到线程1的硬件上下文数据中,然后线程2也对变量a做操作,操作完之后为5,然后再写回到内存中,此时在切换为线程1,但是此时线程1是直接将它的数据从硬件上下文中切换过来的,所以他此时的a仍是10,这样就导致线程在切换的同时,出现了数据不一致问题,也就有可能引发一系列线程不安全的问题。
CPU先从内存中读取,在CPU中操作完之后再把操作后的数据写回到内存中,当被切换时,就把CPU中的上下文数据以拷贝的方式拷贝到自己对应的硬件上下文中,当被切回时,就直接将硬件上下文中的数据切到CPU中

线程互斥指的是在多个线程间对临界资源进行争抢访问时有可能会造成数据二义,因此通过保证同一时间只有一个线程能够访问临界资源的方式实现线程对临界资源的访问安全性
在这里插入图片描述
举例:多人买票-----多线程抢票

#include <stdio.h>
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <vector>

using namespace std;

#define NUM 10 // 创建几个线程

int tickets = 1000;

class ThreadData
{
public:
    ThreadData(int number)
    {
        thread_name = "thread-" + to_string(number);
    }

public:
    string thread_name;
};

void *GetTickets(void *args)
{
    ThreadData *data = static_cast<ThreadData *>(args);
    const char *name = data->thread_name.c_str();
    while (true)
    {

        if (tickets > 0)
        {
            usleep(100000);//进来后先不要让它打印
            //cout << name << " : " << tickets << endl;
            printf("%-9s: %d\n", name, tickets);
            tickets--;//计算的时候会重新从内存读取数据     
        }
        else
        {
            break;
        }
        
    }
    cout << name << "  quit....." << endl;
    return nullptr;
}

int main()
{

    vector<pthread_t> tids; // 存储线程的tid
    vector<ThreadData *> thread_datas;
    for (int i = 1; i <= NUM; i++)
    {
        pthread_t tid;
        ThreadData *data = new ThreadData(i);
        thread_datas.push_back(data);
        pthread_create(&tid, nullptr, GetTickets, thread_datas[i - 1]);
        tids.push_back(tid);
    }

    // for(int i = 0; i < NUM; i++)
    // {
    //     cout << thread_datas[i]->thread_name << ":" << tids[i] << endl;
    // }
    // cout << "main: " << pthread_self() << endl;

    for (auto &tid : tids)
    {
        pthread_join(tid, nullptr);
    }

    for (auto &data : thread_datas)
    {
        delete data;
    }
    return 0;
}

usleep这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
下面就是一个运行时出错的例子
在这里插入图片描述

对于上面的这种情况该如何解决呢?我们只需通过给它加锁就可以实现,在讲解锁之前,我们先来学习一下它的周边概念

线程互斥

临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部访问临界资源的代码段就叫做临界区
互斥:任何时刻,互斥保证只有一个执行流进入临界区,去访问临界资源。
原子性:不被任何调度机制打断的操作,该操作只有两态,要么完成,要么未开始。也就是它在底层的汇编语言执行指令只有一条

互斥量(互斥锁)

互斥量:Linux中互斥量又称为互斥锁,是一种用来保护临街资源的的特殊变量,它可以处于锁定(lock)状态,也可以处于解锁(unlocked)状态。
互斥锁实现原理:就是在内存中有一个变量mutex,设它此时值为1,当某个线程访问临界资源时,就让它和寄存器中的某个变量交换,让mutex变为0,寄存器中的值变为1,当其他线程再来访问这个临街资源时,发现这个互斥量mutex值为0,那么他就会被阻塞到等待锁队列中去等待。下面我们来学习一下互斥量的接口,创建,加锁,解锁,销毁等。
mutex简单理解就是一个0/1的计数器,用于标记资源访问状态:
0表示已经有执行流加锁成功,资源处于不可访问,
1表示未加锁,资源可访问

pthread_mutex_t

多线程使用pthread_mutex_t类型的变量来表示互斥量,对于互斥量的初始化,有静态初始化和动态初始化

静态初始化

只需将PTHREAD_MUTEX_INITIALIZER赋给变量即可,通过这种方式初始化的锁,不用再通过pthread_mutex_init也不用通过pthread_mutex_destroy去释放

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

动态初始化(pthread_mutex_init)

pthread_mutex_init函数
在这里插入图片描述
参数:
mutex:要初始化的互斥量
attr:设为nullptr即可

pthread_mutex_lock

线程试图锁定互斥量的过程称之为加锁
在这里插入图片描述
这里对互斥量加锁的接口有两个分别是pthread_mutex_lock和pthread_mutex_trylock,这两个函数的区别是pthread_mutex_lock会阻塞式等待直到互斥量可用为止,pthread_mutex_trylock则会尝试加锁,一般会立即返回

pthread_mutex_unlock

解锁时线程将互斥量由锁定状态变为解锁状态

pthread_mutex_destroy

销毁互斥量(互斥锁)

死锁

死锁是指一组进程中的各个进程均占有不会释放的资源,但因互相申请被其它进程所占用的不会释放的资源而处于的一种永久等待状态。我们以两个线程之间造成的死锁问题进行举例,例如线程A,B,此时线程A,B各分别拥有一个临界资源a, b,且对这两个临界资源我们都添加了锁来进行保护,然后线程A在执行过程中需要临界资源b,线程B在执行过程中需要临界资源a,在A去申请资源b的时候发现资源正在被线程B占用,因此线程A只能等待,直到线程B不再占用资源b,同理B也需要等到线程A不再占用资源a,但是线程A,B现在都不去释放他们所拥有的资源,造成线程A,B只能去等待的情况,我们称之为死锁。一个线程在去申请一个被互斥锁保护起来的资源时,如果该资源被某个线程所占用,那么他只能去等待一个线程有没有可能出现死锁问题?可能,当这个线程申请的锁还没有被他解锁时,紧接着又再次选择申请该锁,那么他此时只能去等待,这就会造成死锁
在这里插入图片描述

我们先来整理一下出现死锁时需要的四个必要条件,也就是当发生出现死锁现象时,这四个条件一定满足,但满足这四个条件不一定会出现死锁。
1.互斥条件:一个资源每次只能被一个执行流使用(一个线程就对应一个执行流)
2.请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源不释放(就是即使请求资源失败,它也不会释放已有的资源,仍保持原来已有的资源)
3.不剥夺条件:一个执行流已获得的资源,在未使用完之前,不能被强行剥夺(就是此时这个线程拥有的资源,不能被其它的线程强行占用)
4.循环等待条件:若干个执行流之间形成的一种头尾相接的循环等待资源的关系(我们上述举的例子就是其中一种情况,就是A等B,而B等A)
怎么去解决死锁问题呢?
1.破坏死锁必要条件的其中一个
2.加锁顺序一致
3.避免锁未释放的场景
4.资源一次性分配
避免死锁的算法:
1.死锁检测算法
2.银行家算法

加锁后的饥饿问题

每一个线程在访问临界区的时候,都要去申请同一把锁,但是当一个线程在申请锁之后,在它刚刚释放这个互斥锁后,它对互斥锁的竞争能力相对于其他线程可能会比较强,这就导致这个互斥锁还是会被他拿到,而其它的线程仍然拿不到,这种现象我们称为其它线程的饥饿问题。如何解决线程饥饿问题呢?线程同步就是解决饥饿问题的一种方式,当如果一个线程在退出后,不能立即抢占互斥锁,需要到等待队列的末尾去等待,也就是让线程按照某种顺序去抢占资源,这就是线程同步,具体实现我们后面讲解。

同步

什么线程同步?
线程同步是指,在多线程编程中,为了保证多个线程按照某种特定的方式正确、有序的执行,需要进行线程间的协作与同步。
为什么需要同步?
在多线程编程中,当多个线程共享同一份资源时,由于线程的执行顺序是不确定的,因此会存在一些并发问题,如死锁、竞态条件、资源争用等问题。为了避免这些问题,需要让线程进行同步
竞态条件:因为时序问题,导致的程序异常,我们称为竞态条件。
如何让多线程同步?通过条件变量来实现线程同步,具体的函数如下

pthread_cond_init()初始化和销毁

条件变量的初始化也分为静态初始化和动态初始化
在这里插入图片描述
初始化参数:
cond:要初始化的条件变量
attr:设为nullptr即可
销毁参数:
cond:要销毁的条件变量

pthread_cond_wait()

当条件不满足时,让线程等待,从而不让它去访问临界资源
在这里插入图片描述
参数:
cond:让线程在这个条件变量下进行等待
mutex:保护这个临界资源所加的锁
让线程在这个条件变量下去等待时为什么还要给他传递访问该临界资源所加的锁?
因为当这个线程在访问临界资源时,会占有这个互斥量(锁),而其余的线程也想访问这个临界资源时,发现锁被别人占有,就不能够去访问这个临界资源,因此也就不会到条件变量下去等待,要想让别的线程也能够访问该临界资源,就必须先把之前线程所占有的锁释放掉,因此我们通过给pthread_cond_wait()传递锁,从而可以让它把之前的锁释放掉,因此pthread_cond_wait()的作用不仅是让线程去在条件变量下去等待,还会释放锁,当条件满足时会再自动的给该临界资源加锁

当线程访问临界资源时,当发现条件不满足时,我们就通过pthread_cond_wait()函数让这些线程在该条件变量下进行等待,当条件满足时,我们在通过函数去唤醒条件变量下的线程即可,线程在该条件变量下是有序的,因此我们就可以做到线程同步

pthread_cond_signal(or broadcast)

当不满足某个条件时,我们让线程在某个条件变量下进行等待,那么当条件满足时,如何去唤醒该条件变量下的线程呢?pthread_cond_signal函数是唤醒在该条件变量下的第一个线程。
pthread_cond_signal函数用于唤醒在该条件变量下的所有线程。
在这里插入图片描述
参数:
cond:唤醒该条件变量下的线程
返回值:
在这里插入图片描述
成功返回0,失败返回错误码

生产者消费者模型(CP问题)

什么是生产者消费者模型?为什么要有生产者消费者模型?
我们以一个例子来说明这个模型到底是什么,例如我们今天要去买一包方便面,我们是会去工厂里面买还是商店买呢?那肯定是去商店,那么为什么要有商店这个中间商呢?如果没有商店,这就要我们直接去工厂里购买,假定工厂一次只能生产一包,我们不仅要去等,而且工厂只能生产一包,这就导致效率大大的降低,当有了商店之后,工厂可以一次性将生产好的一万包放到商场里,我们直接去商场里里面买就可以了,当商场需要进货时,直接让工厂送就可以了,这个就类似于进程池,就是提前将一堆产品生产好,放到某个地方,当我们还需要时,工厂再去生产就可以了。
生产者消费者模型优点:
有效地解决了,生产者和消费者忙闲不均的问题
解耦,使生产者和消费者关联程度减小
支持并发
生产者消费者关系:
321原则,3种关系2中角色1个交易场所
生产者和生产者:互斥
消费者和消费者:互斥
生产者和消费者:互斥,同步

#include <pthread.h>
#include <queue>
#include <iostream>
#include <unistd.h>

using namespace std;

template <class T>
class BlockQueue
{
    static const int defalutnum = 30;

public:
    BlockQueue(int maxcapacity = defalutnum)
    {
        _maxcapacity = maxcapacity;
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_ccond, nullptr);
        pthread_cond_init(&_pcond, nullptr);
        _lowerwater = _maxcapacity / 3;
        _highwater = _maxcapacity / 3 * 2;
        
    }

    void push(T data)//生产者p
    {
        pthread_mutex_lock(&_mutex); // 为什么要先加锁再判断?因为判断也是在访问临界资源,所以要先加锁,再访问
        if(_blockq.size() == _maxcapacity)//满了就不要再去生产了
        {
            pthread_cond_wait(&_pcond, &_mutex);
        }
        _blockq.push(data);
        if(_blockq.size() >= _highwater)//提醒消费者去消费
        {
            pthread_cond_signal(&_ccond);
        }
        pthread_mutex_unlock(&_mutex);
        
    }

    T pop()//消费者c
    {
        pthread_mutex_lock(&_mutex); // 为什么要先加锁再判断?因为判断也是在访问临界资源,所以要先加锁,再访问
        if(_blockq.size() == 0)
        {
            pthread_cond_wait(&_ccond, &_mutex);
        }
        T front = _blockq.front();
        _blockq.pop();
        if(_blockq.size() <= _lowerwater)//提醒生产者去生产
        {
            pthread_cond_signal(&_pcond);
        }
        pthread_mutex_unlock(&_mutex);
        return front;
    }

    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_pcond);
        pthread_cond_destroy(&_ccond);
    }

private:
    queue<T> _blockq; // 阻塞队列
    int _maxcapacity; // 阻塞队列的最大容量
    pthread_mutex_t _mutex;
    pthread_cond_t _pcond; // 生产者环境变量
    pthread_cond_t _ccond; // 消费者环境变量
    int _lowerwater;
    int _highwater;
};
// 生产者消费者模型
#include "BlockQueue.hpp"

//pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void *Consumer(void *args)
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);
    while (true)
    {
        int data = bq->pop();
        cout << "消费了一个数据:" << data << endl;
        sleep(1);//单位是微妙,1000000微妙 = 1000毫秒 = 1秒 
    }
}

void *Productor(void *args)
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);
    int data = 0;
    while (true)
    { 
        data++;
        bq->push(data);
        cout << "生产了一个数据:" << data << endl;
    }
}

int main()
{
    BlockQueue<int> *bq = new BlockQueue<int>(10);
    pthread_t ctid, ptid;
    pthread_create(&ctid, nullptr, Consumer, bq);
    pthread_create(&ptid, nullptr, Productor, bq);

    pthread_join(ctid, nullptr);
    pthread_join(ptid, nullptr);
    delete bq;
    return 0;
}
  • 36
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

梦想很美

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值