C++并发编程速成课
发布时间: 2024-10-24 00:39:50 阅读量: 26 订阅数: 27
![C++并发编程速成课](https://img-blog.csdnimg.cn/5d98a1937f6f4287ac1d8ab5bb8382ec.png)
# 1. C++并发编程概述
并发编程是现代软件开发中的一个重要主题,尤其是在多核处理器日益普及的今天,它能显著提高程序的性能和响应速度。C++作为一种性能卓越的语言,一直致力于支持高效的并发编程实践。本章节旨在为读者提供一个对C++并发编程领域全面的概述,包括其基本概念、使用场景及为何它对开发者至关重要。
C++并发编程涉及到从简单的线程创建和管理,到复杂的内存模型和原子操作等层面,涵盖范围广泛。我们将探讨C++11标准引入的并发库,它为开发者提供了比以往任何时候都更强大、更易于使用的并发工具。这些工具使得在C++中实现复杂的并发模式成为可能,同时也要求开发者对并发编程有更深的理解。
在接下来的章节中,我们将深入研究C++并发编程的各个方面,从基础的线程管理开始,逐步探讨更高级的主题,如原子操作和内存模型,并通过实践案例分析来巩固理论知识。通过本章的介绍,读者将获得一个关于C++并发编程的坚实基础,为进一步的学习和应用打下基础。
# 2. C++中的线程管理
### 2.1 创建和管理线程
#### 2.1.1 std::thread类的使用
在C++中,`std::thread`类是管理线程的主要接口。使用这个类,我们可以创建线程,传递函数和参数给线程,并启动它。下面是一个简单的例子,展示如何使用`std::thread`创建和启动一个线程:
```cpp
#include <iostream>
#include <thread>
void printHello() {
std::cout << "Hello from the thread!" << std::endl;
}
int main() {
// 创建线程对象并关联到函数
std::thread t(printHello);
// 等待线程完成
t.join();
std::cout << "Main thread exiting..." << std::endl;
return 0;
}
```
上面的代码创建了一个新线程`t`,它执行`printHello`函数。使用`join()`等待该线程完成其执行。`std::thread`的构造函数可以接受参数,这些参数将会被转发到新线程的函数。
**注意**:当`std::thread`对象被销毁时,如果没有调用`join()`或`detach()`,程序会调用`std::terminate()`终止。所以,确保在对象生命周期结束前,对线程进行适当的管理。
#### 2.1.2 线程的启动与同步
创建线程后,我们可能需要在主线程和子线程之间进行同步。C++提供了多种同步机制,例如`std::this_thread::sleep_for`,`std::condition_variable`等。
```cpp
#include <iostream>
#include <thread>
#include <chrono>
int main() {
std::thread t([] {
std::this_thread::sleep_for(std::chrono::seconds(1)); // 睡眠1秒
std::cout << "Thread finished after 1 second" << std::endl;
});
std::cout << "Main thread is waiting for the child thread..." << std::endl;
t.join();
std::cout << "Child thread has finished execution." << std::endl;
return 0;
}
```
在这个例子中,子线程会睡眠一秒钟之后才输出信息。`main`函数中的`join()`调用使得主线程等待子线程执行完毕。
### 2.2 线程间的共享数据
#### 2.2.1 数据竞争与竞态条件
当多个线程同时访问同一资源时,可能会产生数据竞争(race condition)和竞态条件(race condition)。这些情况通常发生在没有适当同步机制的情况下读写共享数据。
```cpp
#include <iostream>
#include <thread>
int shared_data = 0;
void increment_data() {
for (int i = 0; i < 1000; ++i) {
++shared_data; // 竞态条件出现的地方
}
}
int main() {
std::thread t1(increment_data);
std::thread t2(increment_data);
t1.join();
t2.join();
std::cout << "Expected value: 2000, Actual value: " << shared_data << std::endl;
return 0;
}
```
上面的代码中,`shared_data`被两个线程同时修改,没有采取任何同步措施,因此最终输出的`shared_data`值很可能不是预期的2000。
#### 2.2.2 互斥锁的使用和注意事项
为了解决数据竞争问题,我们可以使用互斥锁(mutex)。互斥锁能够保证在任何时刻只有一个线程可以访问数据。
```cpp
#include <iostream>
#include <thread>
#include <mutex>
int shared_data = 0;
std::mutex mtx;
void safe_increment_data() {
for (int i = 0; i < 1000; ++i) {
mtx.lock();
++shared_data;
mtx.unlock();
}
}
int main() {
std::thread t1(safe_increment_data);
std::thread t2(safe_increment_data);
t1.join();
t2.join();
std::cout << "Safe value: " << shared_data << std::endl;
return 0;
}
```
通过使用`std::mutex`的`lock()`和`unlock()`方法来保护数据的访问,我们可以避免竞态条件。不过,一定要注意不要在持有锁的时候调用其他可能阻塞或者长时间运行的函数,这可能会导致死锁。
### 2.3 线程的异常处理
#### 2.3.1 线程异常与资源管理
当线程内的代码抛出异常时,如果没有进行适当的处理,程序将会调用`std::terminate()`终止执行。为了优雅地处理异常,我们可以使用`try/catch`块。
```cpp
#include <iostream>
#include <thread>
#include <stdexcept>
void risky_function() {
throw std::runtime_error("Exception occurred!");
}
void thread_function() {
try {
risky_function();
} catch (...) {
std::cout << "Exception caught in thread!" << std::endl;
}
}
int main() {
std::thread t(thread_function);
t.join();
std::cout << "Main thread exiting..." << std::endl;
return 0;
}
```
在这个例子中,`risky_function`函数抛出了一个异常。`thread_function`中的`try/catch`块捕获并处理了这个异常,防止程序终止。
#### 2.3.2 异常安全的线程代码编写
编写异常安全的线程代码时,重要的是要确保所有资源在异常发生时能够被正确释放。这通常意味着我们需要使用RAII(Resource Acquisition Is Initialization)模式,使用局部作用域的栈变量来管理资源。
```cpp
#include <iostream>
#include <thread>
#include <memory>
class Resource {
public:
Resource() { std::cout << "Resource created." << std::endl; }
~Resource() { std::cout << "Resource destroyed." << std::endl; }
void performAction() { std::cout << "Action performed." << std::endl; }
};
void thread_function(std::shared_ptr<Resource> res) {
if (!res) throw std::runtime_error("Resource is null!");
res->performAction();
}
int main() {
std::shared_ptr<Resource> res = std::make_shared<Resource>();
std::thread t(thread_function, res);
t.join();
std::cout << "Main thread exiting..." << std::endl;
return 0;
}
```
在这个例子中,`std::shared_ptr`的使用确保了即使`thread_function`抛出异常,`Resource`对象也会被正确销毁。这保证了资源的安全释放,避免了内存泄漏。
这一章节提供了创建和管理线程的基本方法,并深入探讨了线程间共享数据时可能遇到的问题以及相应的解决方案。通过正确使用互斥锁和异常处理,可以有效避免并发程序中的许多常见问题。在第三章中,我们将进一步探讨C++并发编程的高级主题。
# 3. C++并发编程高级主题
## 3.1 原子操作与无锁编程
### 3.1.1 std::atomic的使用
在现代计算机体系结构中,多线程的并发执行带来了对共享资源访问同步的需求。C++标准库提供了一个广泛使用的模板类`std::atomic`,该类用于对单个变量执行原子操作。原子操作是指在多个线程中同时进行时,能够保证操作的不可分割性,即在任何时刻,要么一个操作完成了,要么没有进行过。这确保了即使在多线程环境中,也不会有数据竞争或状态不一致的问题。
```cpp
#include <atomic>
std::atomic<int> counter(0);
void increment_counter() {
++counter; // 原子递增操作
}
```
在上述代码中,`std::atomic<int>`声明了一个整型的原子变量。在函数`increment_counter()`中,`++counter`是原子操作,确保了即使多个线程同时调用该函数,计数器`counter`也能正确地被递增。
### 3.1.2 无锁编程的优缺点及适用场景
无锁编程是一种利用原子操作来设计并发算法的技术,目的是为了提高并发性能而尽可能避免使用传统锁机制。无锁编程之所以有吸引力,是因为它能够减少线程的上下文切换开销,提高吞吐量,尤其是在大规模并发系统中。
但是,无锁编程也有其缺点。它通常只能应用于一些特定的场景,如无锁队列、无锁哈希表等数据结构的实现,而且在这些场景下实现起来相对复杂。此外,由于错误的无锁算法可能会导致难以发现的逻辑错误,因此正确性验证和测试变得更加困难。
无锁编程适合于读操作远多于写操作的场景,且当写操作之间的冲突率很低时,能够发挥出最佳的性能。当写操作频繁或者冲突率高时,无锁编程可能会导致性能下降,不如使用传统锁机制。
```mermaid
graph TD
A[开始无锁编程] --> B[分析应用场景]
B --> C{是否有大量读操作}
C -->|是| D[设计无锁数据结构]
C -->|否| E[选择传统锁机制]
D --> F[编写并优化无锁代码]
E --> F[编写传统并发代码]
F --> G[测试和验证正确性]
```
在设计无锁数据结构时,需要对原子操作有深入的理解,例如使用`std::atomic`的`load`和`store`方法来实现无锁的读写操作。
## 3.2 同步原语详解
### 3.2.1 条件变量的使用
条件变量是同步原语中的一个重要概念,它允许线程等待某个条件成立。在C++中,条件变量通常与互斥锁一起使用,以避免虚假唤醒(spurious wake-ups)和确保正确的同步。
```cpp
#include <condition_variable>
#include <mutex>
#include <iostream>
std::mutex m;
std::condition_variable cv;
bool ready = false;
void do_work() {
std::this_thread::sleep_for(std::chrono::seconds(1));
{
std::lock_guard<std::mutex> lock(m);
ready = true;
}
cv.notify_one();
}
int main() {
```
0
0