漫话linux:死锁与同步

#哪个编程工具让你的工作效率翻倍?#

本文名词解释:

条件变量:条件变量是线程同步的一种机制,它允许线程等待某个特定条件的发生。线程可以在条件变量上等待,直到其他线程通知条件已经满足。条件变量的使用通常与一个互斥锁结合在一起,以防止多个线程同时修改条件或共享资源,从而导致竞争条件

互斥锁:互斥锁(Mutex)是一种用于多线程同步的机制,确保同一时刻只有一个线程可以访问共享资源或执行特定代码块。互斥锁的主要目的是防止多个线程同时访问共享资源,从而导致数据竞争和不一致性

临界区:临界区指的是一个访问共用资源(如共用设备或共用存储器)的程序片段,这些共用资源无法同时被多个线程访问。当有线程进入临界区时,其他线程必须等待,以确保共享资源是被互斥地获得和使用

1.死锁

        1.死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源,而处于的一种永久等待状态

        2.死锁的四个必要条件

                1.互斥条件:一个资源每次只能被一个执行流使用--前提

                2.请求与保持:条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放--原则

                3.不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺--原则

                4.循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系--重要条件

        3.解决死锁(破坏四个必要条件的任意一个)

                1.加锁顺序一致:我们在申请锁的时候,A线程先申请A锁,在申请B锁,而B线程先申请B锁,在申请A锁,所以两个线程天然申请锁的顺序就是环状的。我们可以尽量不让线程出现这个环路情况,我们让两个线程申请锁的顺序保持一致,就可以破坏循环等待问题。两个线程都是先申请A锁在申请B锁

                2.避免锁未释放的场景:接口:pthread_mutex_trylock,失败了就会返回退出,释放锁

                3.资源一次性分配:资源一次性分配,比如说你有一万行代码,有五处要申请锁,你可以最开始一次就给线程分配好,而不要把五处申请打散到代码各个区域里所导致加锁场景非常复杂

                4.避免死锁算法(基本用不上):死锁检测算法和银行家算法

2.同步

        1.同步!同步问题是保证数据安全的情况下,让我们的线程访问资源具有一定的顺序性

        2.保证线程安全同步了,为什么还要设置锁?要注意前言和后果。排队是结果。例如突然新来了一个线程,被锁挡在了门外,才开始到后面排队的。分配均衡的可以使用纯互斥,同步是解决分配不均衡问题

        3.快速提出解决方案--条件变量

                1.锁和铃铛(条件变量--布尔类型)都是一个结构体,OS 先描述再组织

                2.条件变量必须依赖于锁的使用(条件就是被锁了,所以才加入等待队列)

        4.条件变量

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

                2.例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个 节点添加到队列中。这种情况就需要用到条件变量

        5.条件变量函数cond

条件变量就相当于是铃铛,和锁的设置非常的相似

初始化 – pthread_cond_init()

        静态分配

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

        动态分配

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);

         参数分析:

                1.cond:需要初始化的条件变量

                2.attr:初始化条件变量的属性,一般设置为 nullptr

        返回值:成功返回 0,失败返回错误码

销毁– pthread_cond_destroy()

int pthread_cond_destroy(pthread_cond_t *cond);

        参数分析:

                1.cond: 需要销毁的条件变量

        返回值:成功返回 0,失败返回错误码

        注意:使用 PTHREAD_COND_INITIALIZER 初始化的条件变量不需要销毁

等待条件变量 – pthread_cond_wait()

                                

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

        参数分析:

                1.cond:需要等待的条件变量

                2.mutex:当前线程所处临界区对应的互斥锁       

        为啥要传一个锁变量,pthread_cond_wait让线程等待的时候,会自动释放锁,将其加入等待队列中,不用管临界资源的状态情况

        返回值:成功返回0,失败返回错误码

        wait必须在加锁和解锁之间进行

唤醒所有进程 – pthread_cond_broadcast():唤醒等待队列中的全部线程

int pthread_cond_broadcast(pthread_cond_t *cond);

        参数分析:cond需要等待的条件变量

        返回值:成功返回0,失败返回错误码

唤醒首个线程 – pthread_cond_signal:唤醒等待队列中的首个进程

int pthread_cond_signal(pthread_cond_t *cond);

        参数分析:cond需要等待的条件变量

        返回值:成功返回0,失败返回错误码

        6.条件变量的使用

对于线程的管理: 先所有都锁上,再依次唤醒,就实现了每个线程进去执行一次,退出,下一个执行

 while(true)
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond, &mutex);              
        std::cout << "pthread: " << number << " , cnt: " << cnt++ << std::endl;
        pthread_mutex_unlock(&mutex);
    }
}
 
int main()
{
    for(uint64_t i = 0; i < 5; i++)
    {
        pthread_t tid;
        pthread_create(&tid, nullptr, Count, (void*)i);
    }
    while(true) 
    {
        sleep(1);
        pthread_cond_broadcast(&cond);
        std::cout << "signal one thread..." << std::endl;
    }
 
    return 0;
}

如何知道要让一个线程去等待了?临界区有资源,此时的操作,直接离开说明两线程是互斥状态,去排队说明两线程是同步状态

如何知道临界区有资源?在加锁后自行判断,常见的方法是加一个全局bool变量来追踪

所以等待的过程,一定要在加锁和解锁之间, pthread_cond_wait让线程等待的时候,会自动释放锁,将其加入到等待队列中

总之,等待条件满足的时候往往是在临界区内等待的,当该线程进入等待的时候,互斥锁会自动释放,而当该线程被唤醒时,又会自动获得对应的互斥锁,条件变量需要配合互斥锁使用,其中条件变量是用来完成同步的,而互斥锁是用来完成互斥的

3.cp模型:生产者消费者模型

存在超市的原因:效率高,中转站,大号缓存解决了忙闲不均,使时生产者知道有多少存储空间,使消费者知道有多少商品,让生产和消费有一定程度的解耦

在计算机中,生产者指线程,消费者也指线程,超市指特定结构的内存空间->共享资源->存在并发问题,将商品理解为数据,执行流在做通信

如何高效通信

        1.互斥是一个保证安全的关系

        2.研究超市的并发:生产者与生产者(竞争互斥,只允许一个),消费者与消费者(互斥),生产者与消费者(互斥--安全,同步--一定的顺序性)

321原则:三种关系,两种角色(生产和消费),一个内存结构(特定结构的内存空间)

例如解耦 add 和 main ,实现高并发

4.基于等待队列的生产者消费者模型

在多线程编程中,阻塞队列是一种常用的数据结构,用于实现生产者和消费者模型。与普通队列相比,阻塞队列具有以下特点:当队列为空时:从队列获取元素的操作将会被阻塞,直到队列中有新元素被放入,当队列满时:往队列里存放元素的操作也会被阻塞,直到队列中有元素被取出,其他情况:其余时间就是边生产边消费,同时进行

Makefile

注意还要包含pthread库并使用c++11

 head1.hpp

#include <iostream>  
#include <pthread.h>  
#include <queue>  
  
template <class T>  
class BlockQueue {  
    static const int defaultNum = 20; 
public:  
    BlockQueue(int maxcap = defaultNum) : maxcap_(maxcap) {  
        pthread_mutex_init(&mutex_, nullptr);  
        pthread_cond_init(&c_cond_, nullptr);  
        pthread_cond_init(&p_cond_, nullptr);  
    }  
    ~BlockQueue() {  
        pthread_mutex_destroy(&mutex_);  
        pthread_cond_destroy(&c_cond_);  
        pthread_cond_destroy(&p_cond_);  
    }  
  
      
    void push(const T &in) {  
        pthread_mutex_lock(&mutex_);  
        while (q_.size() == maxcap_) {  
            pthread_cond_wait(&p_cond_, &mutex_);  
        }  
        q_.push(in);  
        pthread_cond_signal(&c_cond_);  
        pthread_mutex_unlock(&mutex_);  
    }  
  
    T pop() {  
        pthread_mutex_lock(&mutex_);  
        while (q_.size() == 0) {  
            pthread_cond_wait(&c_cond_, &mutex_);  
        }  
        T out = q_.front();  
        q_.pop();  
        pthread_cond_signal(&p_cond_);  
        pthread_mutex_unlock(&mutex_);  
        return out;  
    }  
  
private:  
    std::queue<T> q_;  
    int maxcap_; // 极限容量  
    pthread_mutex_t mutex_;  
    pthread_cond_t c_cond_; // 消费者条件变量  
    pthread_cond_t p_cond_; // 生产者条件变量  
};  
  

代码解析:

阻塞队列:首先是队列宽度20,私有成员,一个队列,容量,mutex_互斥锁,保护队列的访问,c_cond_消费者条件变量,p_cond_生产者条件变量,构造函数:初始化最大容量,互斥是条件变量

push逻辑:首先锁定互斥锁,确保没有其他线程正在访问队列,然后检查队列是否已经满了,使用while循环而不是if可以防止线程被虚假唤醒,如果队列已满,则等待生产者条件变量(解锁互斥锁,将调用线程置于等待队列中,直到一个线程对生产者条件变量使用signal或broad_cast,当线程被唤醒时,该函数会在重新获取互斥锁之前返回),如果不满,则将in加入队列中,再触发消费者条件变量(队列现在不为空,可以买了),表示有东西已经生产完成,再解锁互斥锁,表示可以调用其他部分

pop逻辑:首先锁定互斥锁,检查队列是否为空,为空就等待消费者条件变量,然后删除队头元素,触发生产者条件变量(队列现在是未满状态,可以生产了),解锁互斥锁

zuse1.cc

#include <iostream>  
#include <pthread.h>  
#include <unistd.h>  
#include <ctime>  
#include <queue>  
#include <mutex>  
#include <condition_variable>  
  
// Task 类定义  
class Task {  
public:  
    Task(int a, int b, char op) : data1(a), data2(b), oper(op), result(0), exitcode(0) {}  
  
    std::string GetTask() const {  
        return std::to_string(data1) + " " + std::string(1, oper) + " " + std::to_string(data2) + "=?";  
    }  
  
    std::string GetResult() const {  
        std::string r = std::to_string(data1) + " " + std::string(1, oper) + " " + std::to_string(data2) + "=" + std::to_string(result);  
        if (exitcode != 0) {  
            r += "[Error: " + std::to_string(exitcode) + "]";  
        }  
        return r;  
    }  
  
    void run() {  
        switch (oper) {  
        case '+':  
            result = data1 + data2;  
            break;  
        case '-':  
            result = data1 - data2;  
            break;  
        case '*':  
            result = data1 * data2;  
            break;  
        case '/':  
            if (data2 == 0) {  
                exitcode = 1; // Division by zero  
            } else {  
                result = data1 / data2;  
            }  
            break;  
        case '%':  
            if (data2 == 0) {  
                exitcode = 2; // Modulo by zero  
            } else {  
                result = data1 % data2;  
            }  
            break;  
        default:  
            exitcode = 3; // Unknown operator  
            break;  
        }  
    }  
  
private:  
    int data1, data2;  
    char oper;  
    int result;  
    int exitcode; // Error code, e.g., division by zero  
};  
  
// 阻塞队列实现  
template<typename T>  
class BlockQueue {  
private:  
    std::queue<T> queue_;  
    std::mutex mtx_;  
    std::condition_variable cv_;  
  
public:  
    void push(const T& value) {  
        std::lock_guard<std::mutex> lock(mtx_);  
        queue_.push(value);  
        cv_.notify_one();  
    }  
  
    T pop() {  
        std::unique_lock<std::mutex> lock(mtx_);  
        cv_.wait(lock, [this] { return !queue_.empty(); });  
        T value = queue_.front();  
        queue_.pop();  
        return value;  
    }  
};  
  
// 生产者线程函数  
void* Producer(void* args) {  
    BlockQueue<Task>* bq = static_cast<BlockQueue<Task>*>(args);  
  
    while (true) {  
        int data1 = rand() % 100 + 1;  
        int data2 = rand() % 100 + 1;  
        char op = "+-*/%"[rand() % 5];  
        Task t(data1, data2, op);  
  
        bq->push(t);  
        std::cout << "Produced task: " << t.GetTask() << " by thread id: " << pthread_self() << std::endl;  
        sleep(rand() % 3 + 1);  
    }  
    return nullptr;  
}  
  
// 消费者线程函数  
void* Consumer(void* args) {  
    BlockQueue<Task>* bq = static_cast<BlockQueue<Task>*>(args);  
  
    while (true) {  
        Task t = bq->pop();  
        t.run();  
        std::cout << "Processed task: " << t.GetTask() << " Result: " << t.GetResult() << " by thread id: " << pthread_self() << std::endl;  
        sleep(1);  
    }  
    return nullptr;  
}  
  
int main() {  
    srand(time(nullptr));  
  
    BlockQueue<Task> bq;  
    pthread_t consumers[3], producers[5];  
  
    for (int i = 0; i < 3; i++) {  
        pthread_create(&consumers[i], nullptr, Consumer, &bq);  
    }  
  
    for (int i = 0; i < 5; i++) {  
        pthread_create(&producers[i], nullptr, Producer, &bq);  
    }  
  
    // 注意:在这个示例中,我们没有等待线程结束,因为它们是无限循环的。  
    // 在实际应用中,您可能需要一种机制来优雅地停止这些线程(例如,使用全局变量作为标志)。  
  
    // 为了示例的完整性,这里我们简单地让主线程睡眠一段时间,然后退出(不推荐在实际应用中使用)。  
    sleep(60); // 让主线程睡眠60秒,以便观察生产者和消费者的输出。  
  
    // 注意:由于线程是无限循环的,下面的代码在实际情况下是无效的。  
    // 在实际应用中,您需要在停止线程后再进行这些操作。  
  
    // pthread_exit(NULL); // 这行代码不会停止其他线程,仅退出主线程。  
    // delete &bq; // 这是错误的,不能删除一个局部对象的地址。  
    // 而且,由于 bq 是局部对象,它会在 main 函数结束时自动销毁,  
    // 但由于线程还在运行并可能访问 bq,这会导致未定义行为。  
  
    // 由于示例的特殊性(无限循环线程),我们在这里不调用 pthread_join。  
    // 在实际应用中,您应该确保所有线程都已正确停止并清理资源。  
  
    // 由于线程是无限循环的,并且我们没有提供停止它们的机制,  
    // 因此这个程序将永远运行下去。在实际应用中,您需要设计一种停止线程的方法。  
  
    return 0; // 注意:由于线程未正确停止和清理,这个返回值在实际情况下可能不是安全的。  
}

代码解析:

task类即任务类,执行四则运算的代码

生产者线程函数producer,不断生成task对象到阻塞队列,打印消息并休眠

消费者线程函数和生产者的大差不差,只不过添加变为删除,从创建task对象变为运行task类的run函数

main函数,首先初始化随机数生成器,创建阻塞队列,启动3个生产者和5个消费者进程,再无限循环进行观察

生产者消费者模型的高效点:生产和消费之后线程并行执行,同时生产消费 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值