系列文章目录
- C++ 多线程和多进程 介绍
Overview
1.多线程和多进程 介绍
在 C++ 中,多线程和多进程是两种常见的并行计算方式,它们各自有不同的用途和实现方式。
1.1.多线程(Multithreading)
多线程是指在单个进程中运行多个线程,共享进程的内存空间。C++11 引入了 <thread>
头文件,提供了原生的线程支持。
优点:
- 线程间共享内存,减少了数据传输的开销。
- 线程切换的开销比进程切换小。
缺点:
- 线程同步问题,需要使用锁等同步机制来避免竞态条件。
- 一个线程崩溃可能导致整个进程崩溃。
实现:
#include <iostream>
#include <thread>
void hello() {
std::cout << "Hello from thread" << std::endl;
}
int main() {
std::thread t(hello);
t.join();
return 0;
}
1.2.多进程(Multiprocessing)
多进程是指创建多个进程,每个进程有独立的内存空间。在 C++ 中,可以使用 fork()
系统调用来创建新进程(仅限于 POSIX 兼容系统,如 Linux 和 macOS),或者使用 CreateProcess
函数在 Windows 上创建新进程。
优点:
- 进程间相互独立,一个进程崩溃不会影响其他进程。
- 可以利用多台机器的资源。
缺点:
- 进程间通信(IPC)复杂,需要使用管道、套接字等机制。
- 创建进程和进程间通信的开销较大。
实现:
- Linux/macOS 使用
fork()
:
#include <iostream>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == -1) {
std::cerr << "Fork failed" << std::endl;
return 1;
} else if (pid == 0) {
// Child process
std::cout << "Hello from child process" << std::endl;
} else {
// Parent process
std::cout << "Hello from parent process" << std::endl;
}
return 0;
}
- Windows 使用
CreateProcess()
:
#include <windows.h>
#include <iostream>
int main() {
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
// Start the child process.
if (!CreateProcess(
TEXT("C:\\Path\\To\\ChildProcess.exe"), // No module name (use command line)
NULL, // Command line
NULL, // Process handle not inheritable
NULL, // Thread handle not inheritable
FALSE, // Set handle inheritance to FALSE
0, // No creation flags
NULL, // Use parent's environment block
NULL, // Use parent's starting directory
&si, // Pointer to STARTUPINFO structure
&pi // Pointer to PROCESS_INFORMATION structure
)) {
std::cerr << "CreateProcess failed" << std::endl;
return 1;
}
// Wait until child process exits.
WaitForSingleObject(pi.hProcess, INFINITE);
// Close process and thread handles.
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return 0;
}
1.3.选择多线程还是多进程?
- 多线程:适合于需要大量计算且计算之间有依赖关系的任务,如图形渲染、图像处理等。
- 多进程:适合于需要隔离任务或在不同机器上运行的任务,如服务器处理不同用户的请求。
在实际应用中,可以根据具体需求和资源情况来选择使用多线程还是多进程。
2.C++ 中的线程同步机制有哪些,它们是如何工作的?
在 C++ 中,线程同步是确保多个线程在访问共享资源时不会产生冲突的重要机制。C++11 引入了几种线程同步工具,主要包括:
-
互斥锁(Mutex):
互斥锁用于保护临界区,确保一次只有一个线程可以访问共享资源。- std::mutex:最基本的互斥锁类型。
- std::recursive_mutex:允许同一个线程多次加锁。
工作方式:线程在进入临界区前尝试加锁,如果锁已经被其他线程持有,则线程会阻塞直到锁被释放。
-
锁(Locks):
C++ 标准库提供了多种锁,它们封装了互斥锁的行为。- std::lock_guard:作用域锁,当构造时自动加锁,当析构时自动解锁。
- std::unique_lock:比
std::lock_guard
更灵活,可以手动加锁和解锁,也可以用于条件变量。
-
条件变量(Condition Variables):
条件变量用于线程间的同步,允许一个线程在特定条件成立之前挂起等待。工作方式:一个线程可以等待条件变量,直到其他线程修改了条件并通知等待的线程。
-
原子操作(Atomic Operations):
原子操作确保无锁的线程安全操作。- std::atomic:提供了基本的原子类型。
- std::atomic_flag:用于实现自旋锁。
工作方式:通过底层硬件支持的原子指令来保证操作的原子性。
-
读写锁(Shared-Exclusive Locks):
读写锁允许多个读线程同时访问共享资源,但写线程会独占访问。- std::shared_mutex:可以被多个线程共享地锁定,或者被一个线程独占地锁定。
-
信号量(Semaphores):
信号量用于控制对共享资源的访问数量。- std::counting_semaphore:C++20 新增,用于限制同时访问资源的线程数量。
-
屏障(Barriers):
屏障用于同步多个线程,直到它们都到达某个点。- std::barrier:C++20 新增,用于让一组线程相互等待,直到所有线程都到达屏障。
2.1.示例代码
以下是一些同步机制的简单示例:
互斥锁:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
void print_thread_id(int id) {
mtx.lock();
std::cout << "Thread ID: " << id << std::endl;
mtx.unlock();
}
int main() {
std::thread t1(print_thread_id, 1);
std::thread t2(print_thread_id, 2);
t1.join();
t2.join();
return 0;
}
条件变量:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void print_id(int id) {
std::unique_lock<std::mutex> lock(mtx);
while (!ready) { cv.wait(lock); }
std::cout << "Thread " << id << std::endl;
}
void go() {
std::unique_lock<std::mutex> lock(mtx);
ready = true;
cv.notify_all();
}
int main() {
std::thread threads[10];
for (int i = 0; i < 10; ++i)
threads[i] = std::thread(print_id, i);
std::cout << "10 threads ready to race...\n";
go();
for (auto& th : threads) th.join();
return 0;
}
原子操作:
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> count(0);
void increment(int n) {
for (int i = 0; i < n; ++i) {
count.fetch_add(1, std::memory_order_relaxed);
}
}
int main() {
std::thread t1(increment, 10000);
std::thread t2(increment, 10000);
t1.join();
t2.join();
std::cout << "Count: " << count << std::endl;
return 0;
}
使用这些同步机制时,需要仔细设计以避免死锁、活锁和竞态条件等问题。
3.C++ 中的线程同步机制在实际项目中如何应用?
在实际项目中,线程同步机制的应用非常广泛,特别是在需要处理并发访问共享资源、执行并行计算、或者需要协调多个线程行为的场景中。以下是一些常见的应用场景和示例:
3.1. 保护共享数据
在多线程环境中,多个线程可能会同时访问和修改共享数据,这可能导致数据不一致的问题。使用互斥锁(Mutex)可以确保一次只有一个线程可以访问共享数据。
示例:计数器
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
int counter = 0;
void increment() {
mtx.lock();
++counter;
mtx.unlock();
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Counter: " << counter << std::endl;
return 0;
}
3.2. 生产者-消费者问题
生产者-消费者问题是多线程编程中的经典问题,生产者线程生成数据,消费者线程消费数据。使用条件变量可以协调生产者和消费者的行为。
示例:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> data_queue;
void producer(int n) {
for (int i = 0; i < n; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
int data = i;
{
std::lock_guard<std::mutex> lock(mtx);
data_queue.push(data);
std::cout << "Produced: " << data << std::endl;
}
cv.notify_one();
}
}
void consumer(int n) {
for (int i = 0; i < n; ++i) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return !data_queue.empty(); });
int data = data_queue.front();
data_queue.pop();
lock.unlock();
std::cout << "Consumed: " << data << std::endl;
}
}
int main() {
std::thread t1(producer, 5);
std::thread t2(consumer, 5);
t1.join();
t2.join();
return 0;
}
3.3. 并行计算
在科学计算、大数据处理等领域,经常需要进行大量的并行计算。使用线程可以显著提高计算效率。
示例:并行计算数组的和
#include <iostream>
#include <vector>
#include <thread>
#include <numeric>
void parallel_sum(std::vector<int>& data, size_t start, size_t end, int& result) {
result = std::accumulate(data.begin() + start, data.begin() + end, 0);
}
int main() {
std::vector<int> data(1000000);
std::iota(data.begin(), data.end(), 0);
int result1 = 0, result2 = 0;
std::thread t1(parallel_sum, std::ref(data), 0, data.size()/2, std::ref(result1));
std::thread t2(parallel_sum, std::ref(data), data.size()/2, data.size(), std::ref(result2));
t1.join();
t2.join();
int total_sum = result1 + result2;
std::cout << "Total sum: " << total_sum << std::endl;
return 0;
}
3.4. 图形界面应用程序
在图形界面应用程序中,通常需要在后台线程中执行耗时的任务,同时更新界面。使用线程同步机制可以确保界面的响应性和数据的一致性。
示例:(伪代码)
void updateUI() {
// 更新界面
}
void performTask() {
// 执行耗时任务
std::lock_guard<std::mutex> lock(mtx);
updateUI();
}
int main() {
std::thread t(performTask);
// 界面事件循环
t.join();
return 0;
}
3.5. 网络服务器
在网络服务器中,通常需要处理多个客户端的并发请求。使用线程可以提高服务器的并发处理能力。
示例:(伪代码)
void handleClient(int clientSocket) {
// 处理客户端请求
}
void serverLoop() {
while (true) {
int clientSocket = acceptConnection();
std::thread(t(handleClient), clientSocket);
}
}
int main() {
startServer(serverLoop);
return 0;
}
在实际应用中,选择合适的同步机制和设计合理的并发策略是非常重要的。这需要根据具体的应用场景和性能要求来决定。
4.在C++中,如何使用多线程来提高程序的性能?
在 C++ 中,使用多线程可以显著提高程序的性能,尤其是在执行并行任务和利用多核处理器的能力时。以下是一些提高程序性能的多线程使用策略:
4.1. 并行计算
对于可以分解为多个独立任务的计算密集型任务,可以使用多线程来并行处理。
示例:并行计算数组的总和
#include <iostream>
#include <vector>
#include <thread>
#include <numeric>
void parallel_sum(const std::vector<int>& data, size_t start, size_t end, int& result) {
result = std::accumulate(data.begin() + start, data.begin() + end, 0);
}
int main() {
std::vector<int> data(10000000);
std::iota(data.begin(), data.end(), 0);
int result1 = 0, result2 = 0;
std::thread t1(parallel_sum, std::ref(data), 0, data.size() / 2, std::ref(result1));
std::thread t2(parallel_sum, std::ref(data), data.size() / 2, data.size(), std::ref(result2));
t1.join();
t2.join();
int total_sum = result1 + result2;
std::cout << "Total sum: " << total_sum << std::endl;
return 0;
}
4.2. 任务分割
将大任务分割成多个小任务,每个任务由一个线程处理。
示例:分割任务并行处理
void process_task(int start, int end) {
// 处理任务
}
int main() {
std::vector<std::thread> threads;
int num_threads = 4; // 假设我们有4个线程
int num_tasks = 20; // 假设有20个任务
for (int i = 0; i < num_tasks; i += num_threads) {
for (int j = 0; j < num_threads && i + j < num_tasks; ++j) {
threads.emplace_back(process_task, i + j, i + j + 1);
}
for (auto& thread : threads) {
thread.join();
}
threads.clear();
}
return 0;
}
4.3. 利用线程池
使用线程池可以避免频繁创建和销毁线程的开销,并有效管理线程资源。
示例:使用线程池
#include <iostream>
#include <vector>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <functional>
class ThreadPool {
public:
ThreadPool(size_t);
~ThreadPool();
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>;
private:
// 需要定义内部类和方法
};
int main() {
ThreadPool pool(4); // 创建一个有4个工作线程的线程池
auto result = pool.enqueue([](int answer) { return answer; }, 42);
std::cout << "Result: " << result.get() << std::endl; // 输出 42
return 0;
}
4.4. I/O 操作
对于涉及大量 I/O 操作的任务,使用多线程可以避免阻塞主线程,提高程序的响应性和吞吐量。
示例:并行读取文件
void read_file(const std::string& filename) {
// 读取文件内容
}
int main() {
std::vector<std::string> filenames = {"file1.txt", "file2.txt", "file3.txt"};
std::vector<std::thread> threads;
for (const auto& filename : filenames) {
threads.emplace_back(read_file, filename);
}
for (auto& thread : threads) {
thread.join();
}
return 0;
}
4.5. 避免竞态条件和死锁
正确使用互斥锁、条件变量等同步机制,避免竞态条件和死锁,确保数据的一致性和线程安全。
示例:使用互斥锁保护共享数据
#include <iostream>
#include <vector>
#include <thread>
#include <mutex>
std::mutex mtx;
int shared_data = 0;
void increment() {
std::lock_guard<std::mutex> lock(mtx);
++shared_data;
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(increment);
}
for (auto& thread : threads) {
thread.join();
}
std::cout << "Shared data: " << shared_data << std::endl;
return 0;
}
4.6. 使用原子操作
对于简单的数据操作,使用原子类型可以避免锁的开销。
示例:使用原子操作
#include <iostream>
#include <vector>
#include <thread>
#include <atomic>
std::atomic<int> counter(0);
void increment() {
counter.fetch_add(1, std::memory_order_relaxed);
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10000; ++i) {
threads.emplace_back(increment);
}
for (auto& thread : threads) {
thread.join();
}
std::cout << "Counter: " << counter << std::endl;
return 0;
}
通过这些策略,可以有效地利用多核处理器的能力,提高程序的性能和响应速度。
5.多进程和多线程在实际应用中有哪些优缺点,它们通常在什么情况下使用?
多进程和多线程都是并发编程的常用手段,它们各自有其优缺点,并适用于不同的应用场景。
5.1.多进程
优点:
- 隔离性:每个进程拥有独立的内存空间,一个进程崩溃不会影响其他进程。
- 稳定性:操作系统提供了成熟的进程间通信机制,如管道、消息队列、共享内存等。
- 利用多核:可以充分利用多核处理器的计算能力,实现真正的并行计算。
缺点:
- 资源消耗:每个进程都需要独立的内存空间,资源消耗较大。
- 上下文切换开销:进程间的上下文切换开销比线程大。
- 通信复杂:进程间通信(IPC)比线程间通信复杂。
适用场景:
- 需要隔离任务,防止一个任务的失败影响其他任务。
- 需要利用多台机器的资源,如分布式计算。
- 需要长时间运行的后台任务。
5.2.多线程
优点:
- 资源共享:线程间共享进程的内存空间,减少了数据传输的开销。
- 上下文切换开销小:线程间的上下文切换开销比进程小。
- 易于实现:线程的创建和管理比进程简单。
缺点:
- 安全性:线程间的同步需要额外处理,否则可能导致数据不一致。
- 一个线程崩溃可能导致整个进程崩溃:线程共享进程的资源,一个线程的错误可能影响整个进程。
- 无法充分利用多核:在某些操作系统或编程环境下,线程的调度是由操作系统控制的,可能无法充分利用多核处理器。
适用场景:
- 需要快速响应用户界面,如图形用户界面(GUI)应用程序。
- 需要进行大量计算,且计算任务可以并行化。
- 需要进行大量的I/O操作,如网络服务器、数据库。
5.3.实际应用中的选择
- Web服务器:通常使用多线程或异步I/O来处理多个客户端的并发请求。
- 科学计算:如果计算任务可以并行化,可以使用多进程来充分利用多核处理器。
- 游戏服务器:可能同时使用多进程和多线程,进程用于隔离不同的游戏逻辑,线程用于处理游戏逻辑中的并发任务。
- 数据库:数据库管理系统通常使用多线程来处理查询请求,同时使用多进程来处理后台任务,如索引构建。
- 操作系统:操作系统内核通常使用多进程来管理不同的任务,如文件系统、设备驱动程序。
在选择多进程还是多线程时,需要根据应用的需求、资源限制、性能要求和开发成本等因素综合考虑。
关于作者
- 微信公众号:WeSiGJ
- GitHub:https://github.com/wesigj/cplusplusboys
- CSDN:https://blog.csdn.net/wesigj
- 微博:
- -版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。