C++ condition_variable

互斥锁与条件变量的区别:

  • 功能不同:互斥锁用于保护共享资源的互斥访问,即同一时间只允许一个线程访问共享资源;而条件变量用于线程之间的通信,允许一个线程等待另一个线程满足特定的条件后再继续执行。
  • 用途不同:互斥锁主要用于解决共享资源的并发访问问题,可以防止多个线程同时访问共享资源导致的数据竞争问题;而条件变量主要用于实现线程之间的等待和通知机制,使得线程可以根据特定的条件进行等待或唤醒。
  • 使用方式不同:互斥锁通过对共享资源加锁和解锁来实现互斥访问;而条件变量通常与互斥锁一起使用,等待线程会在条件变量上等待,而唤醒线程会通过互斥锁来保护共享资源的访问。
  • 线程调度不同:互斥锁只负责保护共享资源的互斥访问,而不涉及线程的调度;而条件变量可以通过等待和唤醒操作实现线程的调度,等待的线程会让出CPU,进入等待状态,而唤醒的线程会被调度到就绪状态。

cv条件变量

  • mutex体现的是一种竞争,我离开了,通知你进来。

  • cond体现的是一种协作,我准备好了,通知你开始吧。
    条件变量使多个线程以一种无竞争的方式等待条件的改变

    互斥锁一个明显的缺点是它只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起配合使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其他的某个线程改变了条件变量,他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条件变量被用来进行线程间的同步。

  • 两个线程操作同一临界区时,通过互斥锁保护,若A线程已经加锁,B线程再加锁时候会被阻塞,直到A释放锁,B再获得锁运行,线程B必须不停的主动获得锁、检查条件、释放锁、再获得锁、再检查、再释放,一直到满足运行的条件的时候才可以(而此过程中其他线程一直在等待该线程的结束),这种方式是比较消耗系统的资源的。

  • 而条件变量同样是阻塞,还需要通知才能唤醒,线程被唤醒后,它将重新检查判断条件是否满足,如果还不满足,该线程就休眠了,应该仍阻塞在这里,等待条件满足后被唤醒,节省了线程不断运行浪费的资源。这个过程一般用while语句实现。当线程B发现被锁定的变量不满足条件时会自动的释放锁并把自身置于等待状态,让出CPU的控制权给其它线程。其它线程此时就有机会去进行操作,当修改完成后再通知那些由于条件不满足而陷入等待状态的线程。这是一种通知模型的同步方式,大大的节省了CPU的计算资源,减少了线程之间的竞争,而且提高了线程之间的系统工作的效率。这种同步方式就是条件变量。

cv.wait(lock, [] { return ready; });
//cv.wait(lock, pred); pred为可执行对象
  1. 条件的在wait之前,需要进行条件的判断,而此条件也属于竞争(临界)资源(lambda表达式中的内容),所以在wait函数调用之前,需要先调用std::unique_lock<std::mutex>lock(mu);对此临界资源进行加锁,以保证只有一个线程在进行 pred中的表达式判断;
  2. 如果 pred 的返回值为 false,则当前线程将被阻,进入等待状态,直到接收到条件变量 cv 的通知。在等待期间,互斥锁 lock 会被自动释放,以允许其他线程去持有这个互斥锁,修改共享数据。
  3. 如果 pred 的返回值为 true,则 wait 函数立即返回,当前线程不会被阻塞。此时,互斥锁 lock 仍然被当前线程锁定(持有)。
  4. wait函数中需要传入这把锁,是因为:如果pred中的表达式不满足条件,那么需要cv帮忙释放这把锁,否则其他线程无法拿到这把锁;
思考:

std::lock_guardstd::mutex也可以实现加锁和解锁,为什么std::condition_variable要和std::unique_lock一起使用,而不是和std::lock_guard或者std::mutex一起使用呢?

  1. 更好的控制:std::unique_lock允许显式地加锁和解锁,同时还提供了锁的所有权转移。这使得编写需要在多个条件下释放和重新获取锁的复杂的线程间同步代码成为可能。相比之下,std::lock_guard的锁定和解锁是在其构造和析构时自动进行的,这在复杂的线程间同步场景中可能不足以满足需求。
  2. 条件变量的等待机制: std::condition_variable::wait()函数需要std::unique_lock对象。这个函数将解锁互斥,将当前线程阻塞,并对condition variable进行等待。一旦condition variable被通知,或者发生虚假觉醒,wait()函数会自动再次获取互斥锁。 如果使用std::lock_guard,我们就无法实现这样的操作。

举个例子:

举一个常用的生产者,消费者的示例:生产者线程往队列中放元素,消费者线程从队列中取出元素,不考虑无锁的数据结构实现

使用互斥锁: 消费者线程伪代码如下:

while(1)
{ 
if (队列为空)
	休眠10s,等待生产者线程放入元素
else
{
    加锁
    取元素
    解锁
 }
}

这就有一个问题,可能在刚进入休眠时,B线程就放入元素了,但仍然需要休眠完整个10s的时间。造成不必要的延迟;当然如果不sleep,也可以,但会造成不必要的CPU开销。

使用条件变量: 使用基于条件变量的事件通知唤醒机制,就可以避免互斥锁带来的以上缺点。
生产者线程:

void producer()
{
    std::unique_lock<std::mutex> lock(mtx);
    push元素到队列
    cv.notify_all(); // 通知消费者线程有数据可取
}

void consumer(int id)
{
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, [](){ return 队列不为空; }); // 等待队列中有数据可取
    // 从队列取元素进行操作
  }
}

这种方式就避免了使用互斥锁的缺点,消费者线程不会一直去访问队列,或是休眠一段时间再去访问队列,而是进入等待状态,等待生产者线程往队列中放入元素之后,将消费者线程唤醒,进入等待状态的消费者线程,不会占用CPU资源

思考:
如果这个例子中只有一个生产者线程,有多个消费者线程(A、B、C);在某一时刻,消费者线程中有一个线程(A)获取到锁,从队列中取出元素,并执行操作;如果这个操作非常耗时,在执行操作期间,会导致其他的消费者线程(B、C)无法获取到这个锁,并且会对这个锁产生竞争,而不是进入等待状态

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值