c++核心:小心条件变量的陷阱

c++ 11多线程总结(二):条件变量 多线程程序中当你不仅想要保护数据,还想对单独的线程进行同步。例如,在第一个线程完成前,可能需要等待另一个线程执行完成。 c++11标准库提供了一些工具可用于同步操作:条件变量(condition variables)和期望(futures)。 条件变量(condition variables)基本使用 条件变量使用包含在头文件 condition_variable 中,条件变量需要和unique_lock共同工作。 使用condition_variable: 基本成员函数:wait() , notify_o 阅读详情

原文:http://www.modernescpp.com/index.php/c-core-guidelines-be-aware-of-the-traps-of-condition-variables

条件变量支持一个很简单的功能,一个线程准备好数据,然后向另外一个线程发送通知,通知另一个线程处理这些数据.为什么有可能会很危险呢?

不要无条件等待

有这样的一个规则:一个没有条件(condition)的等待有可能错失线程唤醒,或者线程唤醒后没有工作可做.这句话是什么意思呢?条件变量有可能存在两个问题:丢失线程唤醒和虚假唤醒(spurious wakeup),条件变量的一个问题就是他们不占内存.

首先我们先来看这样一段程序:

// conditionVariables.cpp

#include <condition_variable>
#include <iostream>
#include <thread>

std::mutex mutex_;
std::condition_variable condVar; 

bool dataReady{false};

void waitingForWork(){
    std::cout << "Waiting " << std::endl;
    std::unique_lock<std::mutex> lck(mutex_);
    condVar.wait(lck, []{ return dataReady; });   // (4)
    std::cout << "Running " << std::endl;
}

void setDataReady(){
    {
        std::lock_guard<std::mutex> lck(mutex_);
        dataReady = true;
    }
    std::cout << "Data prepared" << std::endl;
    condVar.notify_one();                        // (3)
}

int main(){
    
  std::cout << std::endl;

  std::thread t1(waitingForWork);               // (1)
  std::thread t2(setDataReady);                 // (2)

  t1.join();
  t2.join();
  
  std::cout << std::endl;
  
}

这段程序是如何实现线程间同步的呢?程序有两个线程t1,t2,分别执行waitingForWork和setDataRead.

setDataReady进行通知:使用条件变量condVar,通过condVar.notify()(line 3)进行通知.线程t1拿到锁之后,等待通知condVar.wait(lck, []{ return dataReady; })( line 4).发送者和接受者都需要一个锁,对于发送者来说,使用lock_guard就足够了,因为发送方只需要lock和unlock一次.对于接收者来说,需要使用unique_lock,因为接受者需要经常进行lock和unlock操作.

也许你会奇怪,wati方法为什么需要一个predicate?因为从理论上分析wait方法不需要predicate也可以实现这个功能.下面我们再看一下线程的唤醒丢失和虚假唤醒

线程唤醒丢失和虚假唤醒

唤醒丢失:这个现象是指发送方在接受方进入wait之前就发送了通知,结果就是发送方发送的这个通知丢失,c++标准中把条件变量描述成一个同时的同步机制(simultaneous synchronisation mechanism):条件变量是一种原始的同步机制,可以同时阻塞一个或多个线程.如果不使用predicate,当发送方的通知丢失的时候,接收方会永远等待下去.

虚假唤醒:在接收方未收到通知的时候,接收方线程也有可能会唤醒.

由于存在这两个问题,你必须使用额外的predicate.如果你不相信,我们再看一下条件变量wait方法的工作流程:

wait工作流程

在调用wait之前,线程需要拿到锁并加锁.

执行wait,进程首先判断predicate,如果predicate为true,线程继续运行.如果predicate为false,线程unlock锁,进入等待(阻塞)状态.

如果条件变量处于等待状态的时候收到了通知或虚假唤醒,线程会执行一下步骤:

线程解除阻塞,重新lock住锁,判断predicate,如果predicate为true,线程继续工作,如果为false,则unlock锁,线程重新进入等待阻塞态.

没有predicate

如果没有predicate,会发生什么呢?

// conditionVariableWithoutPredicate.cpp

#include <condition_variable>
#include <iostream>
#include <thread>

std::mutex mutex_;
std::condition_variable condVar;

void waitingForWork(){
    std::cout << "Waiting " << std::endl;
    std::unique_lock<std::mutex> lck(mutex_);
    condVar.wait(lck);                       // (1)
    std::cout << "Running " << std::endl;
}

void setDataReady(){
    std::cout << "Data prepared" << std::endl;
    condVar.notify_one();                   // (2)
}

int main(){
    
  std::cout << std::endl;

  std::thread t1(waitingForWork);
  std::thread t2(setDataReady);

  t1.join();
  t2.join();
  
  std::cout << std::endl;
  
}

现在,对于wait的调用不再使用predicate,这种同步机制看起来很简单.但是不幸的是,这个程序会存在一个 race condition,会引发死锁.

发送方在line(1)发送通知,发送方在接受方wait之前就发送了通知,接收方就永久阻塞了.

原子操作的predicate

也许你已经注意到了,dataReady变量只是一个布尔变量,我们可不可以把它变成一个原子的布尔变量,这样就可以不加锁了呢?

// conditionVariableAtomic.cpp

#include <atomic>
#include <condition_variable>
#include <iostream>
#include <thread>

std::mutex mutex_;
std::condition_variable condVar;

std::atomic<bool> dataReady{false};

void waitingForWork(){
    std::cout << "Waiting " << std::endl;
    std::unique_lock<std::mutex> lck(mutex_);
    condVar.wait(lck, []{ return dataReady.load(); });   // (1)
    std::cout << "Running " << std::endl;
}

void setDataReady(){
    dataReady = true;
    std::cout << "Data prepared" << std::endl;
    condVar.notify_one();
}

int main(){
    
  std::cout << std::endl;

  std::thread t1(waitingForWork);
  std::thread t2(setDataReady);

  t1.join();
  t2.join();
  
  std::cout << std::endl;
  
}

这个程序很直观,但是这个程序,线程间依然有race condition并且会导致死锁.因为wait操作比看起来的还要复杂一些.wait操作等价于:

std::unique_lock<std::mutex> lck(mutex_);
while ( ![]{ return dataReady.load(); }() {
    // time window (1)
    condVar.wait(lck);
}

即使你把dataReady设置成原子变量,但是也必须加锁操作,如果不加锁的话可能无法实现线程同步.我们假设所有变量的操作是原子的并且没有锁保护.

我们假设接收方在执行wait但是线程没有阻塞,即接收方执行line 1的代码段,这样的话发送方发送的通知就丢失了,接下来接受方会永久进入阻塞状态.但是如果加锁进行保护的话,这种情况就不会再发生,当加锁保护的时候,只有接收方处于等待态的时候,发送方才能发送通知.

这是不是一个鬼故事?那么有没有其他的可能来简化我们的程序呢?有的,但不是使用条件变量,你可以使用一对promise和future(a promise and future pair the make the job done).

C++多线程编程核心陷阱:数据竞争、死锁与条件变量实战解析 并发编程是现代软件开发中提升性能的核心技术,它允许多个任务同时执行以充分利用多核CPU资源。其基本原理在于操作系统对线程的调度与管理,通过时间片轮转或优先级调度实现宏观上的并行。掌握并发编程的技术价值在于能够构建高性能、高响应的应用程序,尤其在服务器、游戏引擎、数据处理等场景中至关重要。然而,并发在带来性能提升的同时,也引入了数据竞争和死锁等经典问题。数据竞争发生在多个线程未同步地访问共享内存且至少有一个写操作时,常导致程序结果不确定或崩溃,可通过互斥锁或原子操作解决。死锁则是线程间因循环等待资源而陷入的永 阅读详情

相关推荐

C++多线程顺序执行:从条件变量到信号量的四种同步方案详解

并发编程是现代软件开发的核心技术,它通过允许多个任务同时执行来提升系统性能。其基本原理在于线程间的同步与通信,确保数据一致性和执行顺序。在C++中,标准库提供了多种同步原语,每种都有其特定的技术价值和应用场景。互斥锁和条件变量组合提供了精细的等待/通知控制,适用于复杂的条件同步;信号量则通过计数机制直观表达资源或信号传递,特别适合阶段化任务同步;异步操作与Future/Promise抽象了任务依赖,简化了结果获取和链式等待;原子操作则通过无锁编程实现极致性能,适用于底层高性能组件。本文以经典的线程顺序执行问

weixin_33462927的博客 264

生产者/消费者代码(使用notify和wait实现)

package cn.wzy.Lock; import java.util.Random; public class ConsumerAndProducer { static String[] food = new String[5]; static String take() throws InterruptedException { synchronized (food) {...

qq_38089964的博客 624

C++数组长度计算的真相:从sizeof陷阱到std::size安全实践

C++中,‘数组长度’并非单一概念,而是取决于类型本质与生命周期:C风格数组长度是编译期常量,std::array继承此特性,而std::vector的size()则是运行时动态值。其底层原理源于数组衰变(Array Decay)——当C风格数组作为函数参数传递时,会退化为指针,导致sizeof失效。这一机制直接关联begin/end迭代器协议和C++17引入的统一接口std::size,它通过模板特化安全提取编译期长度,并兼容标准容器。技术价值在于消除越界风险、支持泛型算法与RAII内存管理;典型应用场

weixin_34128237的博客 364

c++ 条件变量使用详解 wait_for wait_unitl 虚假唤醒

使当前线程阻塞直至条件变量被通知、抵达指定时间或虚假唤醒发生,可选的循环直至满足 pred。使当前线程阻塞直至条件变量被通知、抵达指定时间或虚假唤醒发生,可选的循环直至满足 pred。使当前线程阻塞直至条件变量被通知,或虚假唤醒发生,可选地循环直至满足 pred。由于操作系统调度或资源争议,此函数可能阻塞长于 rel_time。使用注意:时钟最好使用稳定时钟,即计时速率恒定且无法调整的时钟。线程1修改变量,线程2等待变量被修改。

luohaha66的博客 7455

C++并发编程(7):条件变量(conditional variable)、wait( )与notify_one( )、spurious wakeups(虚假唤醒)

主要学习介绍C++并行编程的基础知识,做一些学习记录并分享

Solititude的博客 1222

C++条件变量Wait及虚假唤醒

C++生产者消费者的条件变量及虚假唤醒

5500

C++条件变量std::condition_variable的5大陷阱与最佳实践

在多线程编程中,线程同步是确保数据一致性和程序正确性的核心机制。其基本原理是通过互斥锁和条件变量等原语,协调多个线程对共享资源的访问顺序。条件变量提供了一种高效的等待-通知机制,允许线程在特定条件不满足时挂起,并在条件可能满足时被唤醒,从而避免了忙等待带来的性能损耗。这一技术的核心价值在于能够构建高效、响应式的并发结构,如生产者-消费者模型、任务队列和线程池。在实际应用场景中,如高性能服务器、游戏引擎或数据处理流水线,正确使用条件变量是实现高吞吐量和低延迟的关键。然而,其使用也伴随着诸多挑战,例如虚假唤醒和

weixin_30564901的博客 411

C++并发编程:基于互斥锁与条件变量实现线程安全的信号量

在并发编程中,信号量是一种用于控制多线程对共享资源访问的同步机制,其核心原理是通过一个计数器来管理资源的可用数量。与互斥锁不同,信号量允许多个线程同时访问资源,从而实现限量共享。在C++标准库中,虽然未直接提供信号量实现,但开发者可以通过组合互斥锁和条件变量来构建更安全的信号量,这不仅能加深对底层同步原语的理解,还能提升代码的健壮性。信号量在工程实践中具有重要价值,常用于线程池的并发度控制、生产者-消费者模型中的有界缓冲区以及限制并行任务数量等场景。本文通过实现一个工业级的C++信号量,详细解析了其设计哲学

weixin_34224941的博客 320

C++条件变量与互斥锁:为什么必须配合使用及正确实践

在多线程编程中,线程同步是确保数据一致性和程序正确性的核心机制。其基本原理是通过协调多个线程对共享资源的访问顺序,避免数据竞争和竞态条件。从技术价值看,有效的同步能提升程序可靠性和性能,是构建高并发系统的基石。常见的应用场景包括生产者-消费者模型、线程池任务调度和资源池管理等。本文聚焦于C++标准库中的条件变量(std::condition_variable)与互斥锁(std::mutex)的协同工作,深入解析其设计原理,并探讨如何避免虚假唤醒和丢失通知等并发陷阱,为开发者提供一套稳健的同步方案。

weixin_34306593的博客 325

C++条件变量详解:从原理到生产者-消费者模型实战

在多线程编程中,线程同步是确保数据一致性和程序正确性的核心机制。其基本原理是通过协调不同线程对共享资源的访问顺序,避免数据竞争和竞态条件。条件变量作为一种高级同步原语,其技术价值在于允许线程在特定条件不满足时主动释放CPU并进入等待状态,从而彻底消除忙等待带来的CPU资源浪费。这一机制在并发编程中至关重要,广泛应用于生产者-消费者、线程池、任务队列等需要高效线程协作的场景。本文以C++标准库中的std::condition_variable为例,深入剖析其与互斥锁、谓词的协同工作原理,并通过一个完整的线程安

weixin_30861459的博客 352

C++11生产者消费者模型:从互斥锁到条件变量的完整实现指南

生产者消费者模型是并发编程中的核心范式,用于解耦数据生产与消费过程,提升系统吞吐量。其原理基于共享缓冲区与线程同步机制,通过互斥锁保证数据访问的原子性,利用条件变量实现线程间的高效等待与通知。在C++11标准中,std::thread、std::mutex和std::condition_variable等组件为这一模型提供了标准化实现基础,显著提升了代码的可移植性与安全性。该模型的技术价值在于平衡生产与消费速度差异,避免忙等待造成的CPU资源浪费,广泛应用于网络服务器请求队列、GUI消息泵及数据处理流水线等

weixin_29973493的博客 263

C++多线程编程:互斥锁、条件变量、信号量与原子操作实战解析

在并发编程中,线程同步是确保多线程程序正确性与数据一致性的核心技术。其核心原理在于协调多个执行流对共享资源的访问顺序,防止数据竞争和竞态条件。从技术价值看,有效的同步机制能显著提升程序在多核处理器上的执行效率与可靠性。常见的应用场景包括高性能计数器、任务队列、连接池以及生产者-消费者模型等。本文聚焦于C++标准库中的四种核心同步原语:互斥锁通过独占访问保护临界区,条件变量实现线程间的精准通知与等待,信号量用于控制并发访问的线程数量,而原子操作则提供了无需锁的轻量级变量安全保障。掌握这些工具的组合与选型,是构

weixin_33836874的博客 305

C++条件变量:从原理到实战,彻底解决多线程同步难题

在多线程编程中,线程同步是确保数据一致性和程序正确性的核心机制。其基本原理是通过协调多个线程对共享资源的访问顺序,避免数据竞争和竞态条件。条件变量作为一种高效的线程间通信机制,通过“等待-通知”模型解决了传统忙等待导致的CPU资源浪费问题,显著提升了并发程序的性能和响应性。在工程实践中,条件变量常与互斥锁、条件谓词构成“铁三角”,广泛应用于生产者-消费者模型、线程池任务调度、事件等待等场景。本文深入剖析了条件变量的工作原理,并结合伪唤醒处理、通知机制选择等实战技巧,通过一个完整的有界队列实现案例,展示了如何

weixin_33912453的博客 441

C++线程中断机制:原子标志、条件变量与future的协作式实现

在多线程编程中,线程安全终止是并发控制的核心挑战。强制终止线程可能导致数据损坏和资源泄漏,因此协作式中断机制成为关键技术方案。其原理是通过共享状态变量向线程发送中断请求,线程在预设检查点主动响应并执行清理操作后自行退出。这一机制的技术价值在于保障程序状态的一致性和资源的正确释放,广泛应用于服务器后台任务管理、GUI应用耗时操作取消、线程池优雅关闭等场景。本文以C++为例,深入解析如何结合原子布尔标志、条件变量和future/promise等同步原语,构建健壮的可中断线程模型,并探讨其在避免死锁、处理虚假唤醒

weixin_34038293的博客 406

C++多线程编程7大核心错误与避坑指南:从数据竞争到性能陷阱

并发编程是现代软件开发的核心技术,旨在通过同时执行多个任务来提升程序性能。其基本原理在于利用多核处理器并行处理能力,但同时也引入了数据竞争、死锁等复杂问题。在C++等系统级语言中,多线程编程的技术价值尤为突出,它允许开发者精细控制硬件资源,实现高性能服务器、游戏引擎、实时系统等关键应用。然而,若缺乏对内存模型、同步原语和常见陷阱的深刻理解,极易引发难以调试的线上故障。本文聚焦C++多线程实践中的高频痛点,深入剖析数据竞争、死锁、条件变量误用等典型问题,并结合原子操作、锁粒度优化等热词,提供从原理到工程落地的

weixin_30892889的博客 402

C++多线程实战:从核心概念到线程池与无锁编程

多线程编程是现代软件开发中提升性能的核心技术,它允许程序同时执行多个任务,充分利用多核CPU的计算能力。其基本原理是通过创建多个执行流(线程)来并发处理任务,关键技术包括线程同步、互斥锁、条件变量等。在C++中,多线程编程的价值在于能够直接控制硬件资源,实现零成本抽象,这对于游戏引擎、高频交易等高性能场景至关重要。实际应用中,开发者常面临数据竞争、死锁等挑战,需要掌握线程池、原子操作等高级模式。本文聚焦C++多线程实战,深入解析std::thread、互斥锁等核心工具,并探讨线程池实现与无锁编程等进阶话题,

weixin_29587015的博客 204

C++11多线程编程:从核心库到实战应用全面解析

并发编程是现代软件开发中提升性能的关键技术,它允许多个任务同时执行以充分利用多核处理器资源。其核心原理在于通过线程管理、同步机制和内存模型来协调共享资源的访问,避免数据竞争和死锁。在C++11标准中,多线程支持被正式纳入语言核心,提供了统一、可移植的抽象,极大地简化了并发编程的复杂性。通过std::thread、std::mutex、std::condition_variable等组件,开发者可以构建高性能的并发应用,如服务器后端、游戏引擎和数据处理系统。本文聚焦于C++11多线程编程,深入探讨线程池和生产

weixin_30443731的博客 275

C++线程安全队列实现:基于互斥锁与条件变量的生产者-消费者模型

在多线程编程中,数据竞争是常见挑战,而线程安全队列作为核心的并发数据结构,能有效解决多线程环境下的数据共享问题。其原理是通过互斥锁保护共享资源,结合条件变量实现线程间的同步与通信,从而确保数据的一致性和操作的原子性。这一技术在高性能服务器、实时数据处理等场景中具有重要价值,是实现生产者-消费者模式的基础。本文以C++为例,深入解析基于std::mutex和std::condition_variable的线程安全队列实现,涵盖锁的粒度控制、条件变量的正确使用及异常安全等关键点,并探讨性能优化与无锁队列的权衡。

diaomeijiao3430的博客 519
上一篇: linux平台下使用unix socket连接mysql/MariaDB unix socket路径问题
下一篇: leetcode152 Maximum Product Subarray
LOVETEDA
博客等级 码龄12年 8粉丝 56原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值