C++并发与并行编程:从多线程到并行算法,构建高性能应用
发布时间: 2024-10-23 21:08:22 阅读量: 29 订阅数: 32
高性能计算之并行编程技术PPT课件.ppt
![C++并发与并行编程:从多线程到并行算法,构建高性能应用](https://media.geeksforgeeks.org/wp-content/uploads/20211007112954/UntitledDiagram1.jpg)
# 1. 并发与并行编程基础
## 并发与并行的概念
并发和并行是多任务处理的两种主要形式,它们在计算机科学中扮演着关键角色。并发指的是在宏观上,多个任务似乎同时进行,但微观上可能是分时片进行;并行则是指物理上同时执行多个任务。理解这两者的区别对于开发高性能应用程序至关重要。
## 并发与并行编程的目标
在多线程编程中,目标通常是提高程序的执行效率和响应能力。例如,Web服务器可以并发处理多个客户端请求,图形用户界面可以并行渲染多个动画,而科学计算软件可以并行执行大量计算任务。这些情况下,合理地应用并发与并行可以显著提升性能。
## 并发与并行编程的挑战
并发编程往往伴随着复杂的同步和通信问题。开发者需要处理资源竞争、死锁、条件竞争等挑战,以保证程序的正确性和效率。随着多核处理器的普及,编程模型也在向着更好地利用并行性转变,这使得并行编程的难度逐渐增加。
# 2. 深入C++多线程编程
## 2.1 线程的创建和管理
### 2.1.1 创建线程的基本方法
在C++11中,标准库引入了`<thread>`头文件,提供了一个跨平台的线程库。创建线程的基本方法是通过`std::thread`类。我们可以使用构造函数来传递一个函数对象,或者一个函数与它的参数,从而启动一个线程。
```cpp
#include <thread>
void printHello() {
std::cout << "Hello from thread" << std::endl;
}
int main() {
std::thread t(printHello);
t.join(); // 等待线程结束
return 0;
}
```
上述代码展示了如何创建一个线程,它会输出一条消息然后结束。`std::thread`的构造函数接收一个可调用对象和一组参数(如果需要传递参数给线程函数,可以使用`std::bind`或者`std::thread`的完美转发构造函数)。主线程通过调用`join`等待新创建的线程`t`完成执行。
如果要在创建线程时传递参数,可以使用如下代码:
```cpp
void printHelloWithArgs(std::string msg, int times) {
for(int i = 0; i < times; ++i) {
std::cout << msg << std::endl;
}
}
int main() {
std::thread t(printHelloWithArgs, "Hello", 5);
t.join();
return 0;
}
```
在上面的例子中,`printHelloWithArgs`函数需要两个参数,我们使用构造函数的参数传递特性来初始化线程。
### 2.1.2 线程的同步和互斥
当多个线程共享数据时,必须确保数据的一致性和防止竞态条件。C++提供了`std::mutex`来实现同步。
```cpp
#include <thread>
#include <mutex>
std::mutex mtx;
void printHello() {
mtx.lock();
std::cout << "Hello from thread" << std::endl;
mtx.unlock();
}
int main() {
std::thread t1(printHello);
std::thread t2(printHello);
t1.join();
t2.join();
return 0;
}
```
在上面的代码中,我们使用`std::mutex`的`lock`和`unlock`方法来确保只有一个线程能够打印信息,从而避免竞态条件。更好的方式是使用`std::lock_guard`,它是一个RAII(Resource Acquisition Is Initialization)风格的互斥锁,当它被创建时自动锁定,当离开作用域时自动释放锁。
```cpp
void printHello() {
std::lock_guard<std::mutex> lock(mtx);
std::cout << "Hello from thread" << std::endl;
}
```
使用`std::lock_guard`可以避免忘记释放锁导致的死锁问题,同时代码更加简洁。
## 2.2 线程间通信
### 2.2.1 使用条件变量
条件变量允许线程等待直到某个条件为真。`std::condition_variable`常与互斥锁一起使用,以避免虚假唤醒。
```cpp
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void printNumber() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; });
std::cout << "Thread 1: The value is " << x << std::endl;
}
int main() {
std::thread t(printNumber);
{
std::lock_guard<std::mutex> lock(mtx);
x = 5;
ready = true;
}
cv.notify_one();
t.join();
return 0;
}
```
上面的代码中,线程`printNumber`会等待条件变量`cv`直到`ready`为真。当主函数设置`ready`为`true`并通知条件变量`cv`,`printNumber`线程会继续执行并打印变量`x`的值。
### 2.2.2 使用信号量和互斥量
信号量是一个更通用的同步原语,可以用来控制多个线程对共享资源的访问。在C++中,并没有直接的标准信号量实现,但可以使用操作系统提供的信号量或者第三方库。
信号量的典型用法是限制对共享资源的访问数量。以下是一个使用伪信号量实现资源池的示例:
```cpp
// 假设有一个第三方库提供信号量的实现
#include <threadpool.hpp>
class ResourcePool {
public:
void useResource() {
semaphore.wait(); // 等待信号量
// 使用资源的代码
semaphore.signal(); // 释放信号量
}
private:
Semaphore semaphore{1}; // 信号量的实例,允许的访问数为1
};
int main() {
ResourcePool pool;
std::thread t1([&pool]{ pool.useResource(); });
std::thread t2([&pool]{ pool.useResource(); });
t1.join();
t2.join();
return 0;
}
```
在这个例子中,`semaphore.wait()`会使线程等待直到资源可用,而`semaphore.signal()`会释放资源。通过这种方式,我们可以确保在任何时间点只有一个线程可以使用资源。
## 2.3 线程的高级特性
### 2.3.1 线程局部存储
线程局部存储(Thread Local Storage,TLS)是一种存储机制,允许每个线程都拥有该变量的唯一实例。在C++中,使用`thread_local`关键字可以声明一个线程局部变量。
```cpp
#include <thread>
#include <iostream>
thread_local int local_value = 0;
void printLocalValue() {
local_value++;
std::cout << "Thread ID: " << std::this_thread::get_id()
<< " Local value: " << local_value << std::endl;
}
int main() {
std::thread t(printLocalValue);
std::thread t2(printLocalValue);
t.join();
t2.join();
return 0;
}
```
在上面的代码中,每个线程都有自己的`local_value`变量副本,且互不干扰。主线程中`local_value`的初始值为0,然后每个线程都会将`local_value`增加1,并打印自己的线程ID和局部变量的值。
### 2.3.2 线程的优先级和调度
线程优先级和调度策略允许操作系统在多个可运行线程之间选择线程进行执行。在C++中,可以使用`std::thread`的`native_handle`方法,或者直接使用操作系统的API来设置线程优先级和调度策略。
由于这些操作通常是平台
0
0