C++ 并发

本文探讨了计算机中的并发执行,从单核任务切换到多核硬件并发的概念。任务切换在单核系统中制造并发假象,而多处理器或多核系统则能实现真正的硬件并发。多进程和多线程是并发的两种主要方式,前者通过进程间通信实现,后者利用共享内存。多线程虽有资源限制和同步问题,但在适当管理下能提高性能。选择并发策略时,需平衡性能提升与代码复杂性之间的关系。

计算机中的并发

以前,大多数计算机只有一个处理器,具有单个处理单元(processing unit)或核心(core),如今还有很多这样的台式机。这种机器只能在某一时刻执行一个任务,不过它可以每秒进行多次任务切换。通过“这个任务做一会,再切换到别的任务,再做一会儿”的方式,让任务看起来是并行执行的。这种方式称为任务切换。如今,我们仍然将这样的系统称为并发:因为任务切换得太快,以至于无法感觉到任务在何时会被暂时挂起,而切换到另一个任务。任务切换会给用户和应用程序造成一种“并发的假象”。因为这种假象,当应用在任务切换的环境下和真正并发环境下执行相比,行为还是有着微妙的不同。特别是对内存模型不正确的假设,在多线程环境中可能不会出现。

多处理器计算机用于服务器和高性能计算已有多年。基于单芯多核处理器(多核处理器)的台式机,也越来越大众化。无论拥有几个处理器,这些机器都能够真正的并行多个任务。我们称其为硬件并发(hardware concurrency)”。

下图显示了一个计算机处理恰好两个任务时的理想情景,每个任务被分为10个相等大小的块。在一个双核机器(具有两个处理核心)上,每个任务可以在各自的处理核心上执行。在单核机器上做任务切换时,每个任务的块交织进行。但它们中间有一小段分隔(图中所示灰色分隔条的厚度大于双核机器的分隔条);为了实现交织进行,系统每次从一个任务切换到另一个时都需要切换一次上下文(context switch),任务切换也有时间开销。进行上下文的切换时,操作系统必须为当前运行的任务保存CPU的状态和指令指针,并计算出要切换到哪个任务,并为即将切换到的任务重新加载处理器状态。然后,CPU可能要将新任务的指令和数据的内存载入到缓存中,这会阻止CPU执行任何指令,从而造成的更多的延迟。
在这里插入图片描述
有些处理器可以在一个核心上执行多个线程,但硬件并发在多处理器或多核系统上效果更加显著。硬件线程最重要的因素是数量,也就是硬件上可以并发运行多少独立的任务。即便是具有真正硬件并发的系统,也很容易拥有比硬件“可并行最大任务数”还要多的任务需要执行,所以任务切换在这些情况下仍然适用。

在这里插入图片描述

并发的途径

多进程并发

使用并发的第一种方法,是将应用程序分为多个独立的进程,它们在同一时刻运行,就像同时进行网页浏览和文字处理一样。如图1.3所示,独立的进程可以通过进程间常规的通信渠道传递讯息(信号、套接字、文件、管道等等)。不过,这种进程之间的通信通常不是设置复杂,就是速度慢,这是因为操作系统会在进程间提供了一定的保护措施,以避免一个进程去修改另一个进程的数据。还有一个缺点是,运行多个进程所需的固定开销:需要时间启动进程,操作系统需要内部资源来管理进程,等等。

当然,以上的机制也不是一无是处:操作系统在进程间提供附加的保护操作和更高级别的通信机制,意味着可以更容易编写安全的并发代码。实际上,在类似于Erlang的编程环境中,将进程作为并发的基本构造块。

使用多进程实现并发还有一个额外的优势——可以使用远程连接(可能需要联网)的方式,在不同的机器上运行独立的进程。虽然,这增加了通信成本,但在设计精良的系统上,这可能是一个提高并行可用行和性能的低成本方式。
在这里插入图片描述

多线程并发

并发的另一个途径,在单个进程中运行多个线程。线程很像轻量级的进程:每个线程相互独立运行,且线程可以在不同的指令序列中运行。但是,进程中的所有线程都共享地址空间,并且所有线程访问到大部分数据———全局变量仍然是全局的,指针、对象的引用或数据可以在线程之间传递。虽然,进程之间通常共享内存,但是这种共享通常是难以建立和管理的。因为,同一数据的内存地址在不同的进程中是不相同。图1.4展示了一个进程中的两个线程通过共享内存进行通信。
在这里插入图片描述
地址空间共享,以及缺少线程间数据的保护,使得操作系统的记录工作量减小,所以使用多线程相关的开销远远小于使用多个进程。不过,共享内存的灵活性是有代价的:如果数据要被多个线程访问,那么程序员必须确保每个线程所访问到的数据是一致的。问题并非无解,只要在编写代码时适当地注意即可,这同样也意味着需要对线程通信做大量的工作。多个单线程/进程间的通信(包含启动)要比单一进程中的多线程间的通信(包括启动)的开销大,若不考虑共享内存可能会带来的问题,多线程将会成为主流语言(包括C++)更青睐的并发途径。此外,C++标准并未对进程间通信提供任何
原生支持,所以使用多进程的方式实现,这会依赖与平台相关的API。

线程资源

线程的资源有限,如果太多的线程同时运行,则会消耗很多操作系统资源,从而使得操作系统整体上运行得更加缓慢。不仅如此,因为每个线程都需要一个独立的堆栈空间,所以运行太多的线程也会耗尽进程的可用内存或地址空间。对于一个可用地址空间为4GB(32bit)的架构来说,这的确是个问题:如果每个线程都有一个1MB的堆栈(很多系统都会这样分配),那么4096个线程将会用尽所有地址空间,不会给代码、静态数据或者堆数据留有任何空间。即便64位(或者更大)的系统不存在这种直接的地址空间限制,但其他资源有限:如果你运行了太多的线程,最终也是出会问题的。尽管线程池可以用来限制线程的数量,但这也并不是什么灵丹妙药,它也有自己的问题。

最后,运行越多的线程,操作系统就需要越多的上下文切换,每一次切换都需要耗费本可以花在有价值工作上的时间。所以在某些时候,增加一个额外的线程实际上会降低,而非提高应用程序的整体性能。为此,如果你试图得到系统的最佳性能,可以考虑使用硬件并发(或不用),并调整运行线程的数量。为性能而使用并发就像所有其他优化策略一样:它拥有大幅度提高应用性能的潜力,但它也可能让代码更加复杂,更难以理解,并且更容易出错。因此,应用中只有具有显著增益潜力的性能关键部分,才值得进行并发化。当然,如果性能收益的潜力仅次于设计清晰或关注点分离,可能也值得使用多线程。

### C++ 并发编程与多线程标准库 #### 一、C++并发处理的基础概念 在现代软件开发中,多线程并发编程成为不可或缺的一部分。C++11引入了一套全新的线程库和并发工具,这些特性极大地简化了开发者编写多线程应用程序的工作[^2]。 #### 二、C++11标准线程库的核心组件 C++11及其后续版本的标准库提供了一系列核心类和函数来支持多线程编程: - **std::thread**: 表示操作系统中的一个独立线程。通过创建 `std::thread` 对象可以启动一个新的线程。 - **std::mutex, std::lock_guard, std::unique_lock**: 提供互斥锁机制,防止多个线程同时访问共享资源而导致的数据竞争[^4]。 - **std::condition_variable**: 实现线程间的同步通信,允许某个线程等待特定条件满足后再继续运行。 - **std::atomic**: 支持原子操作,确保某些变量的操作不会被其他线程中断,从而避免竞态条件。 以下是使用 `std::thread` 的简单示例: ```cpp #include <iostream> #include <thread> void threadFunction(int id) { std::cout << "Thread ID: " << id << "\n"; } int main() { std::thread t(threadFunction, 1); t.join(); // 主线程等待子线程完成 return 0; } ``` #### 三、高级并发模式与工具 除了标准库外,第三方库如 Boost 提供了更丰富的功能以支持复杂的并发场景。例如,Boost.Thread 扩展了 C++ 标准线程的功能,并提供了更多实用的工具和抽象层[^3]。它不仅增强了线程管理能力,还改进了对信号量、屏障和其他同步原语的支持。 #### 四、并发编程中的常见问题及解决方案 在实际开发过程中需要注意一些常见的陷阱,比如数据竞争和死锁等问题。为了减少这些问题的发生概率,建议遵循以下原则: - 使用 RAII (Resource Acquisition Is Initialization) 技术自动管理资源生命周期,例如利用 `std::lock_guard` 或 `std::scoped_lock` 自动释放锁。 - 避免长时间持有锁,尽可能缩短临界区范围。 - 调试阶段可借助专业的分析工具检测潜在的竞争条件或内存泄漏。 #### 五、总结 综上所述,无论是内置还是外部扩展,C++都为程序员们准备好了强大的武器去应对日益增长的需求——即构建高性能且可靠的并行计算环境。掌握好这些基础知识和技术手段将会使你在面对复杂项目挑战时更加从容不迫[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吃米饭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值