C++线程管理简述

我在学C++线程管理的时候学了发现C++有很多可以帮助管理线程的类
做个整理

std::mutex

互斥量类, 最基本的一个类, 实例化std::mutext就会产生一个互斥量, 调用成员函数lockunlock 就可以上锁, 去锁, 很方便, 但是还不够, 我们需要手动的去设置mutex的锁和不锁, 这个很简单就像PV原语一样, 代码如下

void critical_zone(string message){
    std::mutex mutex;
    mutext.lock()//锁住
    //do something
    mutex.unlock() //解锁
}

之后C++为了方便大家管理和防止忘记解锁的情况, 给大家准备了一系列的锁类, 关于Mutex RAII(Resource Acquisition Is Initialization), 百度百科说是C++常用的一种方式, 也是大家常用的方式, 会不会有种官方钦定的感觉, 简单的来说就是

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。

其实可以看出来这种方式有点类似于智能指针的实现, 有两个很厉害的模板类

第一个: lock_guard

直接上代码

void some_operation(const std::string &message) {
    static std::mutex mutex;
    std::lock_guard<std::mutex> lock(mutex);

    // ...操作

    // 当离开这个作用域的时候,互斥锁会被析构,同时unlock互斥锁
    // 因此这个函数内部的可以认为是临界区
}

他会在互斥区将要被占用的时候锁住, 所以它叫lock_guard锁的守卫, 这段代码的巧妙也在于栈对象会在气生命周期结束的时候调用他的析构函数, 如果抛出异常, 那么栈就会回退, 调用内部的unlock()

第二个就是unique_lock

这个就像他的名字一样独占mutex对象, 一直维持一个mutex对象属于自己, 同时还提供了几个方法
1. lock() 锁住他所关联的mutex对象
2. try_lock()尝试锁住关联的mutex, 但是不阻塞, 返回是否成功的结果,如果已经被关联互斥了就会抛出异常std::system_error
3. try_lock_until() 可以在一个timeout_time(阻塞)时间点之前尝试锁住mutex
4. try_lock_for() 阻塞一段时间尝试锁住mutex
5. unlock 解锁啦
上代码

std::mutex mtx;

void block_area() {
    std::unique_lock<std::mutex> lock(mtx);
    //...临界区
}
int main() {
    std::thread thd1(block_area);

    thd1.join();

    return 0;
}

开辟新的线程(1)-std::future与std::packaged_task

我在查这个时候看到一篇博文, 他说他查到了std::future的翻译嗯 性病
什么翻译我就不说了, 其实也没错, std 其实是sexually transmitted disease, hhhhh
这几个东西就厉害了, 他们是为了你可以访问异步进行的的结果的, 斯…斯国一, 按照以前的想法, 我们开辟了一个线程那么我们需要定义一个打入那个线程身体里的变量, 然后通过他来知道这个线程执行的结果, 事情就So easy,来看代码

void future_example(void){
    std::packaged_task<std::string()> long_task([](){ return "这是个很久很久很久的一个任务"; });
    std::future<std::string> result=long_task.get_future();
    std::thread(std::move(long_task)).detach(); //detach()是允许线程分离出去独立执行,但是主进程结束后,这个进程还是会被强制杀死
    //这里有个很厉害的优化就是long_task被移动到了一个临时的右值引用然后变成了"窃取"或者将亡状态执行完
    std::cout<<"Please wait...";
    std::cout << '.' << std::flush;
    result.wait(); //程序会阻塞到这里 会一直去查询future的status是否完成
    std::cout<<"The result is "<<result.get()<<std::endl;
}

所以packaged_task会封装一个异步操作并提供给你访问线程返回的方法

开辟新的线程(2)-std::promise

这个类呢也是和std::future一起使用的, 可以和另一个线程共享某个对象, 首先和一个future关联起来, 同时提供了该共享值的同步或者互斥的方法, 它既可以异步设置共享之也可以儿异步返回, 上代码

void promise_example1(void){
    std::promise<std::string> promise;
//    std::future<std::string> future=promise.get_future(); //注册future
    auto future=promise.get_future(); //注册future
    std::thread thread([](std::future<std::string>& future1)
                       {std::cout<<"value: "<<future1.get()<<std::endl;},std::ref(future));
                       //传入future的引用
    promise.set_value("Hello World"); //设置传入的值
    thread.join();//执行thread
    return;
}
void promise_example2(void){
    std::promise<std::string> promise; //定义
    std::thread t([](std::promise<std::string> &promise1){
        promise1.set_value_at_thread_exit("Hello World");
    },std::ref(promise));
    t.join();
    auto future=promise.get_future();
    std::cout<<future.get()<<std::endl;
}

所以promise相当于是包装了一个值, 提供这个值的修改和获取的方法
但是这样好麻烦啊, 如果我需要同时使用promise和packaged_task就会很麻烦, C++还有一个方法

开辟新的线程(3)-std::async

它的内部就是future和promise以及packaged_task, 有两种方式一种叫std::launch::async一种叫 std::launch::deferred延迟创建线程, 直到调用future.get的时候才创建线程, 或者wait阻塞到有结果的时候,async可以比较方便的管理线程还是很好用的, 而且屏蔽了很多细节, 如果不是精通线程操作可以选择std:async, 上代码

void async_example(void){
    auto future1=
            std::async(std::launch::async,[]() mutable->std::string { return "This is a long task,task 1!"; }); //这个的返回类型是std::string
    std::cout<<"value is:"<<future1.get()<<std::endl;


    auto future2=
            std::async(std::launch::async,[](){ std::cout<<"This is a long task,task2!"; });
    future2.wait();
    //以上就是在wait和get的时候启动线程了

    auto future3=
            std::async(std::launch::async,[](){
                std::this_thread::sleep_for(std::chrono::seconds(3));//线程先休眠3秒
                //std::chrono C++11的定时器
                return "This is a long task,task3"; //这个的返回类型是const char*
            });
    std::cout<<"waiting for this..."<<std::endl;
    std::future_status  status1;
    do{
        status1=future3.wait_for(std::chrono::seconds(1));//只要线程还没执行完就等待一秒
        if(status1==std::future_status::deferred){
            std::cout<<"deffered" <<std::endl; //延迟
        }
        if(status1==std::future_status::timeout){
            std::cout<<"timeouted"<<std::endl;//休眠的时候会超时
        }
        if(status1==std::future_status::ready){
            std::cout<<"ready!"<<std::endl; //完成
        }
    }
    while(status1!=std::future_status::ready);

    std::cout<<"value is "<<future3.get()<<std::endl;

}

死锁

首先讲资源

资源

计算机中有不同的资源 按分类

消耗性资源和可复用资源

消耗性资源就是存在生产者消费者关系的资源
可复用的资源就是可以多次重复分配的资源

可抢占资源和不可抢占

CPU或者处理机就是典型的可抢占资源, 不可抢占资源比如磁带机和打印机

死锁的形成

假设有P1和P2两个进程,都需要A和B两个资源,现在P1持有A等待B资源,而P2持有B等待A资源,两个都等待另一个资源而不肯释放资源,就这样无限等待中,这就形成死锁,这也是死锁的一种情况。给死锁下个定义,如果一组进程中每一个进程都在等待仅由该组进程中的其他进程才能引发的事件,那么该组进程是死锁的。

分类
  • 竞争不可抢占资源引起死锁: P1和P2分别要从磁带机中输出到打印机进程P1拿了打印机要磁带机, 进程P2拿了磁带机要打印
  • 竞争可消耗资源引起死锁:哲学家进餐问题就是一个很典型的问题,如果说所有哲学家全部拿起来了他们左边的筷子那么就一定会死锁
  • 进程推进顺序不当引起死锁:进程存在安全序列,如果说存在多个资源的管理和多个进程对多个资源进行请求,那么就需要寻找安全的进程序列,银行家算法就是其中之一判断是否存在安全序列
    线程也存在这样的问题
    C++提供了一些方式来解决死锁问题

std::condition_variable

当mutex无法满足要求的时候,依然会发生死锁的情况, 这个时候就可以选择std::condition_variable, 它可以使得线程必须要满足某些条件才可以执行, 所以它叫条件变量, 我学的时候真的觉得看的一知半解, 觉得很难懂这个东西, 先来一个比较简单的例子,

std::mutex mutex; //全局互斥量
std::condition_variable cv; //全局条件变量
bool ready; //线程准备抢夺互斥量

void begin_threads(){
    std::unique_lock<std::mutex> ulk(mutex);//先拿到互斥量
    std::this_thread::sleep_for(std::chrono::seconds(3));//倒计时三秒钟
    ready=true;//线程可以开始准备抢夺
    cv.notify_all();//通知所有在等待的线程,并且在作用域结束的时候解锁
}

void do_procedure(){//执行逻辑
    std::vector<std::thread> threads;//线程数组
    for(int i=0;i<10;i++){
        threads.push_back(std::thread([](int id){ //装载一系列线程
            std::unique_lock<std::mutex> ulk(mutex); //尝试锁住互斥量
            while(!ready){//当ready变为true的
                cv.wait(ulk); //等待这个锁属于自己,阻塞
            }
            std::cout<<"I am the "<<id<<" thread"<<std::endl;//终于拿到了锁
            ulk.unlock();//解锁,事实上不需要这句话,离开这个lambda的时候ulk已经被解锁析构
        },i));
    }
    std::cout<<"All the thread has been loaded"<<std::endl;
    begin_threads();//线程开始
    for(auto &t_iter:threads){
        t_iter.join();//线程分别运行
    }
}
int main(int argc,char *argv[]){
    do_procedure();//执行上述逻辑
    return 0;
}

事实上, condition_variable解决的问题就是那个ready变量, 如果我们写成这样

void begin_threads2(){
    std::unique_lock<std::mutex> ulk(mutex);//锁住全局锁
    std::this_thread::sleep_for(std::chrono::seconds(5));//这个线程先休息5s
    ready=!ready;//然后将ready置反
}

void do_procedure2(){
    std::vector<std::thread> threads;
    for(int i=0;i<10;i++){
        threads.push_back(std::thread([](int id){
            std::unique_lock<std::mutex> ulk(mutex);//尝试锁住全局锁
            while(!ready);//等待ready被置反
            std::cout<<"May I do somthing in critical zone?"<<std::endl;//互斥区
        },i));
    }
    std::cout<<"All the thread has been loaded!"<<std::endl;
    begin_threads2();//置反
    for(auto &t_iter:threads){
        t_iter.join();//执行
    }
}

你会发现这玩意儿没法执行了, 程序在五秒之后没有出现其他任何输出, 而是停在了那里, 原因是大家都在等待那个ready值, while(ready);,这个时候没有人真正进入了临界区, 而是一直在循环等待, 死锁诞生了, 因为有线程拿了全局锁, 但是并没有拿到ready的值, 但是当执行cv.wait(ulk)这句的时候, cv.wait(ulk)会把线程挂起, 然后让出全局锁, 方便其他线程也就是begin_threads2的执行
这个东西还可以用在生产者消费者模型上, 看下面这个例子

void do_product_consume(){
    std::queue<int> products; //产品队列
    std::mutex mutex; //互斥量
    std::condition_variable con_var; //条件变量
    bool notifid=false;
    bool done= false;

    std::thread productor([&](){
        for(int i=0;i<5;i++){
            std::this_thread::sleep_for(std::chrono::seconds(2));//生产者先准备2s,等消费者消费完毕
            std::unique_lock<std::mutex> ulk(mutex);//锁住互斥区
            std::cout<<"I am producting!"<<std::endl;//生产
            products.push(i);//生产一个产品
            notifid= true;//置反
            con_var.notify_one();//通知被阻塞的线程,可以取一个线程了
        }
        done=true;//完成生产
        con_var.notify_one();//再次通知线程
    });

    std::thread consumer( [&](){
        while(!done){//只有生产者还在生产的时候,才允许一个消费者进入互斥区
            std::unique_lock<std::mutex> ulk(mutex);//锁住
            while(!notifid){//等待生产出一个产品
                con_var.wait(ulk);//等待获得锁,线程被挂起,不再执行循环
            }
            while(!products.empty()){//当有产品的时候消费产品
                std::cout<<"consumer "<<products.front()<<std::endl;
                products.pop();
            }
            notifid= false;
        }
    });
    productor.join();
    consumer.join();
}

这个模型是生产一个消费一个, 每次消费者消费之后都会对notify进行置反, 下一个进程就不可以进入消费, 只能等生产者重新置位, 就是这样进行同步.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值