文章目录
1.1.1 计算机的并发
定义:计算机的并发指在单个系统中同时执行多个独立任务。
历史背景:
- 多任务操作系统:多年前,计算机通过任务切换实现多任务运行。
- 多处理器服务器:早期已实现并行计算。
并发流行的原因:真正的并行。
单核处理器:
- 任务切换:通过快速切换任务,制造并发的假象。
- 局限性:每次只能执行一个任务,切换有开销。
多核处理器:
- 硬件并发:真正并行多个任务。
- 普及:多核处理器在台式机中越来越常见。
任务切换与硬件并发:
- 任务切换:单核处理器上,任务交替执行,有切换开销。
- 硬件并发:多核处理器上,任务真正并行执行。
硬件线程:
- 多线程:某些处理器在一个核心上可执行多个线程。
- 数量重要性:硬件线程数量决定并发任务数。
任务切换的持续重要性:
- 任务数量:即使有硬件并发,任务切换仍适用于任务数超过硬件并行能力的情况。
- 后台任务:任务切换使后台任务得以运行。
实际应用:
- 图1.1:双核机器上任务并行 vs. 单核机器上任务切换。
- 图1.2:双核处理器上四个任务的任务切换。
总结:
- 技术适用性:无论单核还是多核,任务切换和硬件并发技术都适用。
- 硬件影响:并发使用方式取决于可用硬件并发能力。
图一
图二
1.1.2 并发的方式
类比:
- 两个程序员在两个办公室:代表多进程并发。
- 优点:独立工作,互不干扰。
- 缺点:沟通困难,管理成本高。
- 两个程序员在一个办公室:代表多线程并发。
- 优点:沟通方便,资源共享。
- 缺点:注意力分散,资源竞争。
并发的基本途径:
- 开发人员:代表线程。
- 办公室:代表进程。
多进程并发
定义:将应用程序分为多个独立的进程同时运行。
特点:
- 进程间通信:通过信号、套接字、文件、管道等,但通常复杂且速度慢。
- 进程保护:操作系统保护进程,防止数据被修改。
- 固定开销:启动进程和管理进程需要额外资源。
优点:
- 安全性:操作系统提供保护和高级通信机制,易于编写安全并发代码。
- 分布式支持:可在不同机器上运行进程,提高并行可用性和性能。
缺点:
- 通信成本高:进程间通信复杂且开销大。
- 资源消耗:运行多个进程需要更多资源。
应用场景:适用于需要高隔离性和安全性的场景,如分布式系统。
多线程并发
定义:在单进程中运行多个线程。
特点:
- 线程共享地址空间:线程间可访问大部分数据(全局变量、指针等)。
- 轻量级:线程开销远小于进程。
- 数据一致性:需确保线程访问的数据一致,增加了线程通信的复杂性。
优点:
- 高效通信:通过共享内存通信,开销小。
- 资源利用率高:线程共享资源,减少管理成本。
缺点:
- 数据竞争:共享内存可能导致数据不一致。
- 复杂性:需要额外机制(如锁)确保线程安全。
应用场景:适用于需要高效通信和资源共享的场景,如高性能计算。
总结:
- 多进程并发:适合高隔离性和分布式场景,但通信成本高。
- 多线程并发:适合高效通信和资源共享,但需处理数据竞争。
- C++的并发实现:本书专注于多线程并发,因其高效且主流。
图例:
- 图1.3:多进程间通信。
- 图1.4:多线程间通过共享内存通信。
后续内容:本书将重点讨论多线程并发及其实现。
图 1.3
图1.4
1.2 为什么使用并发
原因:
- 分离关注点(SOC)。
- 性能提升。
1.2.1 分离关注点
定义:将相关代码与无关代码分离,使程序更易理解、测试和维护。
应用场景:
- 示例:DVD播放程序。
- 功能1:播放DVD(解码数据并输出到硬件)。
- 功能2:处理用户输入(如“暂停”、“退出”)。
- 单线程问题:需要定期检查用户输入,导致代码耦合。
- 多线程解决方案:
- 线程1:处理用户界面。
- 线程2:播放DVD。
- 交互:用户点击“暂停”时,线程间通信。
优点:
- 响应性:用户界面线程可立即响应用户请求。
- 清晰性:任务分离使代码更简单、更易维护。
- 独立性:线程划分基于设计,不依赖CPU核心数。
1.2.2 性能
背景:
- 多核处理器普及:芯片制造商通过多核设计提升性能。
- 挑战:开发者需设计并发软件以利用多核性能。
性能提升方式:
-
任务并行(Task Parallelism):
- 定义:将单个任务拆分为多个部分并行执行。
- 类型:
- 过程并行:不同线程执行算法的不同部分。
- 数据并行:不同线程对不同数据块执行相同操作。
- 易并行算法:具有良好的可扩展性,随硬件线程增加而提升性能。
-
处理更大问题:
- 定义:通过并行处理更多数据,提升吞吐量。
- 示例:并行处理图片的不同区域,支持更高分辨率视频。
限制:
- 依赖关系:任务间可能存在依赖,增加复杂性。
- 资源限制:线程数量受系统资源限制。
1.2.3 什么时候不使用并发
原因:收益小于成本。
成本:
- 复杂性:
- 多线程代码难以理解和维护。
- 增加的复杂性可能导致更多错误。
- 性能开销:
- 线程启动和上下文切换的开销可能超过性能增益。
- 任务完成时间短时,单线程可能更高效。
- 资源限制:
- 线程数量受系统资源(如内存、地址空间)限制。
- 过多线程可能导致系统性能下降。
适用场景:
- 性能关键部分:只有性能关键部分值得并发化。
- 设计清晰性:如果分离关注点带来的清晰性优于性能收益,也可使用并发。
总结:
- 权衡:使用并发需权衡性能增益与代码复杂性。
- 优化策略:并发是优化策略之一,需谨慎使用。
结论:
- C++开发者的多线程意义:多线程是提升性能和分离关注点的重要工具,但需根据具体场景权衡使用。
后续内容:本书将深入探讨如何在C++中实现多线程并发。
并发和多线程
1.3.1 C++多线程历史
- C++98标准:不支持线程,缺乏内存模型。因此,在没有编译器扩展的情况下无法编写多线程应用。
- 平台相关扩展:供应商通过POSIX或Windows API等提供多线程支持,但这些依赖于特定平台且需要相应的运行库支持。
- 第三方库:如MFC、Boost和ACE提供了面向对象的多线程工具,简化了任务管理。
1.3.2 支持并发
- C++11标准:引入了新的内存模型和扩展的标准库,包括线程管理、数据保护、同步操作及原子操作。
- 继承自Boost:许多C++11线程库类的设计与Boost线程库相似,方便已有用户过渡。
- 语言改进:C++11不仅限于并发支持,还包括对语言本身的多项改进,有利于简化并行编程。
1.3.3 C++14和C++17对并发和并行的支持
- C++14:增加了新的互斥量类型以更好地保护共享数据。
- C++17:引入了并行算法,进一步增强了标准库的功能,使编写多线程代码更加便捷。
- 原子操作:直接在语言层面支持,减少了对平台特定汇编指令的依赖,提高了代码的可移植性和效率。
1.3.4 C++线程库的效率
- 抽象代价:C++标准委员会在设计线程库时注重性能,确保高级API与底层API之间的性能差距最小化。
- 灵活性:提供了从低级到高级的各种工具,允许开发者根据需求选择合适的工具进行开发。
- 优化考虑:尽管高级工具可能带来额外开销,但在大多数情况下,这种开销是可控的,并且可以通过良好的程序设计来减少性能瓶颈。
1.3.5 平台相关的工具
- native_handle():C++线程库中提供的函数,允许访问平台特定的API,以便在标准库不足以满足需求时使用更底层的工具。
- 慎重选择:虽然可以利用平台特定的工具实现更高的性能或特殊功能,但这通常意味着增加复杂度和维护成本,因此应在充分评估后谨慎采用。
总结来说,随着C++版本的演进,特别是从C++11开始,对并发和多线程的支持有了显著增强,使得开发者能够编写出高效、可移植且易于维护的多线程应用程序。同时,对于追求极致性能的应用场景,C++也提供了足够的灵活性和底层访问能力。
开始入门
1.4.1 欢迎来到并发世界
为了编写C++多线程程序,首先需要一个支持C++11/C++14/C++17标准的编译器。下面通过对比单线程和多线程版本的“Hello World”程序来介绍如何使用C++进行并发编程。
单线程 “Hello World” 程序
#include <iostream>
int main() {
std::cout << "Hello World\n";
}
这个简单的程序将字符串“Hello World”输出到标准输出流。
多线程 “Hello, Concurrent World” 程序
#include <iostream>
#include <thread> // 1
void hello() { // 2
std::cout << "Hello Concurrent World\n";
}
int main() {
std::thread t(hello); // 3
t.join(); // 4
}
关键点解析:
-
包含头文件
<thread>
:这是使用C++标准库中多线程功能的第一步,提供了管理线程的类和函数。 -
定义独立函数
hello()
:每个线程都需要一个执行起点,这里定义了一个简单的函数hello()
用于新线程的启动。 -
创建并启动新线程:通过
std::thread t(hello);
创建了一个新的线程t
,该线程开始执行hello()
函数中的代码。 -
等待线程完成:调用
t.join();
让主线程等待t
线程执行完毕后再继续执行后续代码。如果不调用join()
,主线程可能会在t
线程之前结束,导致程序提前退出。
总结:
- 区别与联系:相较于单线程程序,多线程版本主要增加了线程管理的逻辑,包括引入了
<thread>
库、定义了独立的执行单元(函数),并通过std::thread
对象启动新线程,最后使用join()
确保所有线程正确完成。 - 适用场景:尽管这个例子展示了如何创建和管理线程,但对于如此简单的任务来说,并不推荐使用多线程,因为这样不仅没有性能上的优势,还增加了复杂性。更多复杂的实例将在后续章节中展示,说明何时及如何有效地利用多线程技术。
通过这些基础示例,你可以开始探索更高级的并发编程概念和技术,逐步构建出高效且可维护的多线程应用程序。