c++中互斥锁和条件变量详解

1.互斥锁mutex

行为:当多个线程竞争同一把锁时,首先会执行加锁操作,如果加锁失败,也就是没有获取锁,线程会进入阻塞态,阻塞在加锁操作上,如果有其他线程释放锁,也就是锁可用了,在linux中,由内核决定唤醒一个线程而让他们直接获得锁。

注意:如果阻塞在加锁操作上,是不用用户线程显式执行唤醒操作的

#include <iostream>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <functional>
#include <chrono>
#include <vector>
#include <memory>
class Thread{
public:
    std::mutex mtx_;
    std::condition_variable cv_;
    void func(){
        std::cout<<"threadid="<<std::this_thread::get_id()<<"try get lock "<<std::endl;
        //使用智能锁unique_lock 默认创建智能锁自动执行加锁操作
        //加锁范围是 对象创建这一行到出作用域析构
        std::unique_lock<std::mutex> lck(mtx_);
        std::cout<<"threadid="<<std::this_thread::get_id()<<"get lock "<<std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(2));
        std::cout<<"thread:id="<<std::this_thread::get_id()<<"release lock "<<std::endl;
    }
};
int main(){
    Thread myThread;//绑定器成员函数的调用依赖一个对象 保证子线程访问的都是同一个对象的锁和条件变量
    std::vector<std::unique_ptr<std::thread>> threads;
    //创建三个线程并设置分离线程
    for (int i = 0; i < 3; ++i) {
        threads.emplace_back(std::make_unique<std::thread>(std::bind(&Thread::func,&myThread)));
    }
    for (int i = 0; i < threads.size(); ++i) {
        threads[i]->detach();
    }
    //等待子线程执行完毕
    std::this_thread::sleep_for(std::chrono::seconds(100));
    return 0;
}

 执行结果分析:

threadid=threadid=3try get lock threadid=4try get lock
threadid=4get lock

2try get lock
thread:id=4release lock
threadid=3get lock
thread:id=3release lock
threadid=2get lock
thread:id=2release lock

可以看出在获取锁之前的操作是并发乱序执行的,之后线程4获得了锁,线程2 3被阻塞,两秒后释放了锁,然后线程3被操作系统唤醒获取了锁,最后是线程2被操作系统唤醒获取了锁,整个过程用户没有执行显示的notify操作,因此可以得知,阻塞在加锁操作的线程,是由操作系统唤醒的,不需要用户干预

2.条件变量condition_variable

 条件变量最主要的就是两大操作:1.notify2.wait。其中notify操作比较简单,无论是cv.notify_all()还是cv.notify_one(),两个操作仅仅是负责唤醒阻塞在条件变量cv上的线程;对于wait根据是否指定超时时间,可以分为wait和wait_for(先不考虑wait_until),根据是否向wait传递等待条件,wait一共有四种方式:wait(lock),wait(lock,condition),wait_for(lock,time),wait_for(lock,time,condition)其中这四种方式行为各不相同下面将会做出详细说明。

2.1cv.notify_all()/cv.notify_one()

 cv.notify_all()是唤醒等待条件变量cv上的所有线程,cv.notify_one()是唤醒等待在条件变量cv上的一个线程,这个不是讨论的重点。仅以cv.notify_all()为例。

注意:cv.notify操作会唤醒等待在条件变量cv上的线程,不涉及任何和锁相关的操作,notify不获取锁,也不释放锁一个线程即使没有获得锁也能执行notify操作

注意:一个调用cv.wait(lock)被阻塞的线程(先不考虑调用cv.wait_for设置了超时时间的情况),只能等待用户其他线程显示调用这个条件变量的notify操作来唤醒这个线程,不会自己苏醒;线程阻塞在条件变量的wait操作和加锁操作是不同的,阻塞在加锁操作行为已经说过,不再赘述。

演示代码1

#include <iostream>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <functional>
#include <chrono>
#include <vector>
#include <memory>
class Thread{
public:
    std::mutex mtx_;
    std::condition_variable cv_;
    void funcWait(){
        std::unique_lock<std::mutex> lck(mtx_);
        cv_.wait(lck);
        std::cout<<"threadid="<<std::this_thread::get_id()<<"wait end"<<std::endl;

    }
    void funcLockAndRelease(){
        std::unique_lock<std::mutex> lck(mtx_);
        std::this_thread::sleep_for(std::chrono::seconds(2));
        std::cout<<"threadid="<<std::this_thread::get_id()<<"sleep end"<<std::endl;
        std::cout<<"threadid="<<std::this_thread::get_id()<<"release lock"<<std::endl;
    }
};
int main(){
    Thread myThread;
    std::thread t1(std::bind(&Thread::funcWait,&myThread));
    t1.detach();
    //保证t1先执行
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::thread t2(std::bind(&Thread::funcLockAndRelease,&myThread));
    t2.detach();
    //等待子线程执行完毕
    std::this_thread::sleep_for(std::chrono::seconds(100));
    return 0;
}

执行结果分析:

threadid=3sleep end
threadid=3release lock

我们让线程1先执行然后cv_.wait(lck)让线程1释放锁,然后线程2获得了锁,执行了自己的任务,然后释放了锁,可以看到线程1仍然在阻塞,并没有看到wait end字样,但是此时锁是可用的,说明,即使锁可用,没有显式调用cv_.notify的话,阻塞在条件变量上的线程不会被唤醒,印证了上述结论。 

演示代码2

#include <iostream>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <functional>
#include <chrono>
#include <vector>
#include <memory>
class Thread{
public:
    std::mutex mtx_;
    std::condition_variable cv_;
    void funcNotify(){
        std::unique_lock<std::mutex> lck(mtx_);
        cv_.notify_all();
        std::cout<<"threadid="<<std::this_thread::get_id()<<" notify "<<std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(3));
        std::cout<<"thread:id="<<std::this_thread::get_id()<<" release lock "<<std::endl;
    }
    void funcWait(){
        std::unique_lock<std::mutex> lck(mtx_);
        cv_.wait(lck);
        std::cout<<"threadid="<<std::this_thread::get_id()<<" wait end"<<std::endl;
    }
};
int main(){
    Thread myThread;

    std::thread t1(&Thread::funcWait,&myThread);
    //保证t1先执行
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::thread t2(&Thread::funcNotify,&myThread);
    t1.detach();
    t2.detach();

    std::this_thread::sleep_for(std::chrono::seconds(100));
    return 0;
}

执行结果分析:

 threadid=3 notify
thread:id=3 release lock
threadid=2 wait end 

执行结果是先打印第一行,休眠三秒后相继打印第二行第三行。执行过程首先是线程1执行funcWait线程函数,获得了锁,调用wait释放了锁并且将自己阻塞;线程2执行线程函数funcNotify,首先是获得了锁,然后进行cv.notify_all()操作,唤醒线程1,此时线程1已经苏醒,但是线程1并未向下执行,这是由于虽然线程1苏醒了,还没有获得锁不能向下执行,锁仍然被线程2持有,因此notify不获取锁,也不释放锁,锁的释放仍然要等线程2执行完毕出作用域。因此线程2在notify之后仍然能执行线程2后续代码,直至执行完毕释放锁,然后线程1获得锁,线程1继续执行。

演示代码3 

#include <iostream>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <functional>
#include <chrono>
#include <vector>
#include <memory>
class Thread{
public:
    std::mutex mtx_;
    std::condition_variable cv_;
    void funcNotify(){
        cv_.notify_all();
        std::this_thread::sleep_for(std::chrono::seconds(3));
        std::cout<<"thread:id="<<std::this_thread::get_id()<<" notify end"<<std::endl;
    }
    void funcWait(){
        std::unique_lock<std::mutex> lck(mtx_);
        cv_.wait(lck);
        std::cout<<"threadid="<<std::this_thread::get_id()<<" wait end"<<std::endl;
    }
};
int main(){
    Thread myThread;

    std::thread t1(&Thread::funcWait,&myThread);
    //保证t1先执行
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::thread t2(&Thread::funcNotify,&myThread);
    t1.detach();
    t2.detach();

    std::this_thread::sleep_for(std::chrono::seconds(100));
    return 0;
}

相较于演示代码2,只是线程函数funcNotify稍作修改,不进行加锁操作,仅仅是notify。

执行结果分析:

threadid=2 wait end
thread:id=3 notify end

 执行流程:线程1先执行,获得锁,然后调用cv.wait()释放锁,将自己阻塞,然后线程2执行cv.notify_all(),唤醒等待在条件变量cv上的线程。注意:此时锁是空闲的,所以当执行完cv.notify_all被唤醒后,线程1直接被唤醒拿到锁,直接开始执行,打印threadid=2 wait end线程1结束,此时线程2还未结束。再次印证了notify不获取锁,也不释放锁一个线程即使没有获得锁也能执行notify操作

2.2cv.wait(lock)

首先讨论wait的第一个重载版本,只传入一个unique_lock。如果是第一次执行到cv.wait(lock)那么线程会无条件释放互斥锁,并且阻塞当前线程;如果线程阻塞在cv.wait(lock)的话,唯一打破阻塞的方法就是等待其他线程调用cv.notify进行唤醒,否则会一直阻塞;这个线程一旦被唤醒,就不会再次阻塞(只调用一次cv.wait的情况下,无循环),他会一直竞争,直到获得锁,然后执行后续代码。

演示代码

#include <iostream>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <functional>
#include <chrono>
#include <vector>
#include <memory>
class Thread{
public:
    std::mutex mtx_;
    std::condition_variable cv_;
    void funcNotify(){
        cv_.notify_all();
        std::cout<<"thread:id="<<std::this_thread::get_id()<<" notify "<<std::endl;
    }
    void funcWait(){
        std::cout<<"threadid="<<std::this_thread::get_id()<<" try get lock "<<std::endl;
        std::unique_lock<std::mutex> lck(mtx_);
        cv_.wait(lck);
        std::cout<<"threadid="<<std::this_thread::get_id()<<" start!!! "<<std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(3));
        std::cout<<"threadid="<<std::this_thread::get_id()<<" end!!! "<<std::endl;
    }
};
int main(){
    Thread myThread;
    std::vector<std::unique_ptr<std::thread>> threads;
    for (int i = 0; i < 3; ++i) {
        threads.emplace_back(std::make_unique<std::thread>(std::bind(&Thread::funcWait,&myThread)));
    }
    for (int i = 0; i < threads.size(); ++i) {
        threads[i]->detach();
    }
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::thread t(std::bind(&Thread::funcNotify,&myThread));
    t.detach();
    std::this_thread::sleep_for(std::chrono::seconds(100));
    return 0;
}

执行结果分析:

threadid=threadid=4 try get lock threadid=3 try get lock

2 try get lock
thread:id=5 notify
threadid=2 start!!!
threadid=2 end!!!
threadid=4 start!!!
threadid=4 end!!!
threadid=3 start!!!
threadid=3 end!!!

 首先让线程id为2,3,4的线程阻塞,然后线程id为5的线程负责唤醒他们,之后线程2,3,4苏醒;线程竞争获得互斥锁,即使第一次没有获取锁,也不会再次阻塞,而是继续竞争,直到获取锁

2.3cv.wait(lock,condition)

cv.wait(lock)只阻塞一次,也只需要唤醒一次;但是加了条件就不一样了,可能需要阻塞多次,并且唤醒多次。第一次调用cv.wait(lock,condition)如果条件不满足的话,释放锁,并阻塞当前线程;直至被其他线程调用cv.notify显式唤醒,唤醒后仍然是先尝试获取锁,获取锁了以后,会再次判断条件(没有处于阻塞态,获取锁以后才会判断条件),如果条件满足会向下执行,反之会再次释放锁并阻塞线程。

演示代码

#include <iostream>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <functional>
#include <chrono>
#include <vector>
#include <memory>
class Thread{
public:
    std::mutex mtx_;
    std::condition_variable cv_;
    void funcNotify(){
        int count=0;
        while (count++<10){
            std::this_thread::sleep_for(std::chrono::seconds(1));
            cv_.notify_all();
            std::cout<<" notify "<<std::endl;
        }
    }
    void funcWait(){
        std::unique_lock<std::mutex> lck(mtx_);
        int count=0;
        cv_.wait(lck,[&]()->bool{
            count++;
            std::cout<<" check condition count: "<<count<<std::endl;
            return count>5;
        });
        std::cout<<" start!!! "<<std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(3));
        std::cout<<" end!!! "<<std::endl;
    }
};
int main(){
    Thread myThread;
    std::thread t1(std::bind(&Thread::funcWait,&myThread));
    t1.detach();
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::thread t2(std::bind(&Thread::funcNotify,&myThread));
    t2.detach();
    std::this_thread::sleep_for(std::chrono::seconds(100));
    return 0;
}

执行结果分析:

 check condition count: 1
 notify
 lock release
 check condition count: 2
 notify
 lock release
 check condition count: 3
 notify
 lock release
 check condition count: 4
 notify
 lock release
 check condition count: 5
 notify
 lock release
 check condition count: 6
 start!!!
 end!!!
 notify
 lock release
 notify
 lock release
 notify
 lock release
 notify
 lock release
 notify
 lock release

 线程2执行notify之后,未立刻释放锁;线程1虽然会被唤醒但没有立刻获得锁,线程一也是在线程2释放锁之后才去检查条件。印证了cv.wait(lock,condition)可能会阻塞多次直到条件满足,而且只在没有被阻塞,且获得锁以后,才会进行条件判断

使用cv.wait(lock,condition)和cv.wait(lock)+循环的行为是一样的 如下

cv_.wait(lck,[&]()->bool{
            count++;
            std::cout<<" check condition count: "<<count<<std::endl;
            return count>5;
        });

while(count<=5){
    cv_.wait(lck);
    count++;
    std::cout<<" check condition count: "<<count<<std::endl;
}

 2.4cv.wait_for(lock,time)

相较于cv.wait(lock),cv.wait_for(lock,time)能够设置一个超时时间。如果超时时间到期的话,线程不需要借助其他线程显式调用cv.notify,能自动唤醒,并尝试获取锁,获取锁之后,继续执行后续代码。可根据返回值,查看是否是超时返回

演示代码

#include <iostream>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <functional>
#include <chrono>
#include <vector>
#include <memory>
class Thread{
public:
    std::mutex mtx_;
    std::condition_variable cv_;
    void funcWait(){
        std::unique_lock<std::mutex> lck(mtx_);
        std::cout<<"begin!!! "<<std::endl;
        if(std::cv_status::timeout==cv_.wait_for(lck,std::chrono::seconds(5))){
            std::cout<<"timeout "<<std::endl;
        }else{
            std::cout<<"notimeout "<<std::endl;
        }
        std::cout<<"end!!! "<<std::endl;

    }
};
int main(){
    Thread myThread;
    std::thread t(std::bind(&Thread::funcWait,&myThread));
    t.detach();

    std::this_thread::sleep_for(std::chrono::seconds(100));
    return 0;
}

执行结果分析:

begin!!!
timeout
end!!!

 先打印begin!!!,5s后打印后两行。在没有其他线程唤醒的情况下,超时时间到期了确实是摆脱了阻塞状态;实际上5s到期后线程会自己苏醒,但是不一定会立即执行后续代码,而是获取了锁才能继续执行,因此在有锁竞争的场景下,不一定5s到了就能向下执行,还需要花费额外的时间获取锁。

实际应用中如果你需要让某个正在阻塞的线程完成某个任务,可以选择外部显式notify唤醒线程,或者设置超时时间让它自己苏醒。

 2.5cv.wait_for(lock,time,condition)

相较于cv.wait(lock,condition)如果在不超时的情况下,二者行为一样,如果发生超时和cv.wait_for(lock,time)的行为一样,也是将自己唤醒,然后尝试获取锁,但是会执行condition里的代码,但是无论返回值是true还是false,都会执行后续代码。实际上,如果想用wait_for,并且需要循环判断的话更倾向使用while+cv.wait_for(lock,time)。

演示代码

#include <iostream>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <functional>
#include <chrono>
#include <vector>
#include <memory>
class Thread{
public:
    std::mutex mtx_;
    std::condition_variable cv_;
    void funcWait(){
        std::unique_lock<std::mutex> lck(mtx_);
        int count=0;
        cv_.wait_for(lck,std::chrono::seconds(6),[&]()->bool{
            count++;
            std::cout<<count<<" times "<<std::endl;
            return false;
        });
        std::cout<<count<<" end!!! "<<std::endl;
    }
};
int main(){
    Thread myThread;
    std::thread t(std::bind(&Thread::funcWait,&myThread));
    t.detach();

    std::this_thread::sleep_for(std::chrono::seconds(100));
    return 0;
}

执行结果分析:

1 times
2 times
2 end!!! 

第一次进入时条件不满足,释放锁,并阻塞;6s后将自己唤醒,获得互斥锁之后,继续执行判断条件里的代码,由于是超时返回,故即使条件不成立也不会再次阻塞。

  • 16
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值