几个概念
同步和异步:同步和异步是两种常见的编程模式,特别是在处理任务执行时,它们定义了如何处理任务的顺序和等待时间。
同步 (Synchronous)
定义:同步意味着任务按顺序执行,一个任务必须等前一个任务完成之后才能开始。也就是说,当一个任务运行时,程序会等待该任务完成后再继续执行下一个任务。
特点:执行顺序严格,任务完成前不能开始下一个任务。
例子:比如你去银行排队办理业务,只有前一个客户办理完了,才能轮到你。
异步 (Asynchronous)
- 定义:异步意味着任务可以并行执行。任务的执行不需要等待其他任务完成,可以在等待任务完成的同时继续执行其他任务。
- 特点:任务可以在后台执行,程序在等待一个任务时可以继续做其他事情。
- 例子:就像在银行办理业务时,你可以同时去旁边的自动取款机取钱,不需要等待前面客户的业务完成。
并行和并发:并行(Parallelism)和并发(Concurrency)是计算机科学中描述多任务处理的两个术语,虽然它们常常一起出现,但有着不同的含义。
并发(Concurrency)
- 定义:并发是指在同一时间段内,系统可以管理多个任务(或线程)的执行,但这些任务不一定是同时执行的。并发的目标是有效地切换和管理多个任务的执行,使它们看起来像是同时进行的。
- 特点:并发不一定要求任务同时执行,它强调的是任务的合理安排和调度。它涉及任务的交替执行,通常依赖于操作系统的调度策略。并发是一个管理多个任务的概念,可以通过单核或多核CPU实现。
- 例子:在一个单核CPU上,操作系统通过快速切换任务,使得看起来像多个任务在“同时”进行,这就是并发。比如,在一个程序中,一边处理用户输入,一边处理网络请求,虽然 CPU 可能只有一个核心,但通过任务切换,给用户提供了“并发执行”的感觉。
并行(Parallelism)
- 定义:并行是指同时执行多个任务,通常是在多核处理器或多处理器系统上实现的。并行计算的目标是加速任务的执行,通过同时处理多个任务来提高计算效率。
- 特点:并行要求任务真正同时执行。它是硬件支持的,通常依赖多核或多处理器系统。并行能够显著提升处理速度,尤其在计算密集型任务中。
- 例子:在一个多核CPU上,可以同时运行多个线程,每个线程在不同的核心上执行,这就是并行。例如,科学计算中的矩阵运算,可以将不同的计算任务分配到多个处理器上并行执行,从而提高计算效率。
并发与并行的主要区别:
执行方式:
并发:多个任务交替执行,看起来像是同时进行,但实际上它们是顺序执行的。
并行:多个任务真正同时执行。资源需求:
并发:可以在单核或多核的系统中实现,依赖操作系统的调度来切换任务。
并行:需要多核或多处理器的硬件支持,任务需要被分割成可以并行执行的部分。适用场景:
并发:适用于I/O密集型任务(例如网络请求、文件操作等),在这些任务中,程序常常需要等待某些操作完成。
并行:适用于计算密集型任务(例如科学计算、图像处理等),通过同时处理多个数据块来加速执行。举例
并发:假设你有三个任务,分别是任务A、任务B、任务C。它们在同一个CPU上执行,任务A、B和C会交替执行,操作系统会在不同的时间片切换它们,给你一种它们在并行执行的感觉。
并行:假设你有三个任务,分别是任务A、任务B、任务C。它们在三个不同的CPU核心上执行,任务A、B、C确实是同时运行的。
C++多线程
在 C++ 中,多线程编程可以用来提高程序的性能,尤其是当程序涉及到计算密集型或 I/O 密集型任务时。C++11 引入了对多线程的直接支持,使得创建和管理线程变得更为简便。以下是 C++ 中多线程的基本概念和示例代码。
一、std::thread
std::thread 是 C++11 引入的一个标准库类,用于在多线程环境下创建和管理线程。它提供了一个简单的接口来启动并控制多个线程的执行。下面是一些关于 std::thread 的基本介绍:
1. 创建和启动线程
要创建一个线程,你可以直接传递一个可调用对象(如函数指针、Lambda 表达式或者函数对象)给 std::thread 的构造函数,线程会在后台启动执行。
#include <iostream>
#include <thread>
void printHello() {
std::cout << "Hello from thread!" << std::endl;
}
int main() {
std::thread t(printHello); // 创建并启动线程
t.join(); // 等待线程完成
return 0;
}
2. join() 和 detach() 方法
join(): 用于等待线程执行完毕,主线程会阻塞直到该线程结束。如果在主线程中调用 join(),会阻塞并等待该线程完成。
detach(): 将线程与主线程分离,使线程在后台继续执行而不阻塞主线程。需要小心使用,防止线程资源泄漏。
std::thread t(printHello);
t.detach(); // 线程继续后台执行,不会阻塞主线程
3. 传递参数到线程
可以通过传递参数给线程来传递数据。如果传递的是值,则会复制一份副本;如果传递的是引用,则需要使用 std::ref 来标识引用。
void printNumber(int n) {
std::cout << "Number: " << n << std::endl;
}
int main() {
int num = 10;
std::thread t(printNumber, num); // 传递值
t.join();
std::thread t2(printNumber, std::ref(num)); // 传递引用
t2.join();
return 0;
}
4. 线程的生命周期
线程在创建后,直到调用 join() 或 detach() 才会结束。如果线程没有 join() 或 detach(),会导致程序抛出异常。
5. 线程ID 和管理
每个线程都有一个唯一的线程 ID,可以通过 std::this_thread::get_id() 获取当前线程的 ID。还可以使用 std::thread::get_id() 获取其他线程的 ID。
std::thread t(printHello);
std::cout << "Thread ID: " << t.get_id() << std::endl;
t.join();
二、std::mutex
在 C++ 中,std::mutex 是一个互斥量(mutex),用于保护共享数据,避免多个线程同时访问导致的竞态条件。std::mutex 是 C++11 引入的,用于实现线程同步,确保同一时刻只有一个线程可以访问某些资源。
1. std::mutex 的基本用法
std::mutex 提供了两个基本的方法:lock() 和 unlock()。lock() 用于请求互斥锁,如果锁被其他线程占用,当前线程将阻塞,直到锁可用。unlock() 用于释放锁。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // 创建一个互斥量
void printHello(int id) {
mtx.lock(); // 请求锁
std::cout << "Hello from thread " << id << std::endl;
mtx.unlock(); // 释放锁
}
int main() {
std::thread t1(printHello, 1);
std::thread t2(printHello, 2);
t1.join();
t2.join();
return 0;
}
2.lock_guard
std::lock_guard 是 C++ 中用于管理互斥锁(mutex)的一种工具类,它提供了一种基于作用域的自动加锁和解锁方式。它属于 头文件。主要特点:
- 目的:std::lock_guard 主要用于在作用域内自动锁定和解锁互斥锁,确保在作用域结束时(即 std::lock_guard 对象超出作用域时),互斥锁被自动解锁,即使发生异常也能保证锁的释放。
- 加锁:当创建一个 std::lock_guard 对象时,它会自动锁定传入的互斥锁。
- 解锁:当 std::lock_guard 对象超出作用域时,析构函数会自动解锁互斥锁。
#include <iostream>
#include <mutex>
std::mutex mtx;
void printNumbers() {
std::lock_guard<std::mutex> lock(mtx); // 自动加锁
std::cout << "Hello from thread!" << std::endl;
// 自动解锁,在作用域结束时
}
int main() {
printNumbers();
return 0;
}
3.unique_lock
std::unique_lock 是 C++11 引入的另一种互斥锁管理类,提供比 std::lock_guard 更灵活的锁管理功能。它也是基于作用域的,但相比于 std::lock_guard,std::unique_lock 允许更多的控制,如手动解锁、延迟加锁、尝试加锁等功能。主要特点:
- 灵活性:std::unique_lock 可以在构造时不立即加锁,也可以在任何时候手动加锁和解锁。
- 可解锁:与 std::lock_guard 只能在析构时自动解锁不同,std::unique_lock 允许手动解锁(通过调用 unlock() 方法)。
- 可重新加锁:std::unique_lock 支持“重新加锁”功能,允许在解锁之后再次加锁同一互斥锁。
- 可以与条件变量配合使用:std::unique_lock 支持与 std::condition_variable 配合使用,这是 std::lock_guard 不支持的功能。
主要成员函数:
- lock():显式加锁。
- unlock():显式解锁。
- try_lock():尝试加锁,如果加锁失败则返回 false。
- release():释放当前的锁,但不会解锁,它返回指向原锁的指针。
#include <iostream>
#include <mutex>
#include <thread>
std::mutex mtx;
void printNumbers() {
std::unique_lock<std::mutex> lock(mtx); // 自动加锁
std::cout << "Hello from thread!" << std::endl;
// 可以手动解锁并在之后重新加锁
lock.unlock();
std::cout << "Mutex unlocked!" << std::endl;
lock.lock(); // 重新加锁
std::cout << "Mutex re-locked!" << std::endl;
}
int main() {
std::thread t1(printNumbers);
t1.join();
return 0;
}
4.其他锁
std::recursive_mutex:
- 用途:std::recursive_mutex 是一种递归锁,允许同一个线程多次加锁同一把锁,而不会导致死锁。
- 特点:一个线程可以多次锁定 recursive_mutex,每次加锁时锁的计数器会增加,只有解锁相同次数后,锁才会被真正释放。
- 使用场景:当一个线程需要在递归函数中多次加锁时,recursive_mutex 是一个很好的选择。
std::timed_mutex:
- 用途:std::timed_mutex 是一种可以设置超时时间的互斥锁,尝试加锁时,如果超时则返回失败。
- 特点:它提供了 try_lock_for() 和 try_lock_until() 方法,用于在特定时间内尝试加锁。
- 使用场景:适用于需要等待锁一段时间但不希望无限期阻塞的场景。
std::recursive_timed_mutex:
主要特点:递归特性:与 std::recursive_mutex 相似,std::recursive_timed_mutex 允许同一个线程多次加锁同一把锁,而不会导致死锁。每次加锁时,锁的计数器会增加,直到计数器为零时,锁才会被释放。
定时功能:与 std::timed_mutex 相似,它允许在指定的时间内尝试加锁。如果在超时前没有获取到锁,它会返回失败。这避免了线程在无法获取锁时被无限期阻塞。
使用场景:当你需要递归加锁的能力,并且还希望在加锁时能够控制超时,避免长时间阻塞,可以使用 std::recursive_timed_mutex。
三、条件变量condition_variable
std::condition_variable 是 C++11 引入的同步机制,它用于线程间的通信,允许一个线程在某个条件满足之前等待,而另一个线程可以通知它条件已经满足。它通常和 std::mutex 一起使用,用来在多线程程序中实现线程的等待和通知机制。
1. std::condition_variable 的基本概念
std::condition_variable 允许一个线程在某个条件成立时等待,而另一个线程可以通过通知来唤醒等待的线程。它有两个主要操作:
- 等待:等待一个条件发生,通常与互斥量一起使用。
- 通知:唤醒等待线程,通常是通过条件变量的 notify_one() 或 notify_all() 来完成。
2. 常用成员函数
- wait(std::unique_lockstd::mutex& lock): 使调用线程进入等待状态,直到条件满足(通常是某个共享数据的状态)。
- wait_for(std::unique_lockstd::mutex& lock, std::chrono::duration<Rep, Period> rel_time): 使调用线程等待指定时间,或者直到条件满足。
- wait_until(std::unique_lockstd::mutex& lock, std::chrono::time_point<Clock, Duration> abs_time): 使调用线程等待直到指定的时间点,或者直到条件满足。
- notify_one(): 唤醒一个等待该条件的线程。
- notify_all(): 唤醒所有等待该条件的线程。
这是一个经典的使用条件变量的例子,生产者线程将数据放入共享缓冲区,消费者线程从缓冲区取出数据。当缓冲区为空时,消费者线程需要等待生产者生产数据;当缓冲区满时,生产者线程需要等待消费者消费数据。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::mutex mtx; // 互斥量
std::condition_variable cv; // 条件变量
std::queue<int> buffer; // 缓冲区
const unsigned int maxBufferSize = 5;
void producer() {
for (int i = 0; i < 10; ++i) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [](){ return buffer.size() < maxBufferSize; }); // 等待直到缓冲区有空位
buffer.push(i);
std::cout << "Produced: " << i << std::endl;
cv.notify_all(); // 通知消费者有新数据
}
}
void consumer() {
for (int i = 0; i < 10; ++i) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [](){ return !buffer.empty(); }); // 等待直到缓冲区有数据
int value = buffer.front();
buffer.pop();
std::cout << "Consumed: " << value << std::endl;
cv.notify_all(); // 通知生产者缓冲区有空位
}
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
实例:用C++实现两个线程交替打印一个1-100的奇偶数字。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx; // 用于保护共享资源
std::condition_variable cv; // 条件变量
int current = 1; // 当前要打印的数字
void printOdd() {
for (int i = 1; i <= 100; i += 2) {
std::unique_lock<std::mutex> lock(mtx);
// 等待,直到 `current` 是奇数
cv.wait(lock, []{ return current % 2 != 0; });
std::cout << current << " ";
++current; // 增加当前数字
cv.notify_all(); // 通知另一个线程可以打印
}
}
void printEven() {
for (int i = 2; i <= 100; i += 2) {
std::unique_lock<std::mutex> lock(mtx);
// 等待,直到 `current` 是偶数
cv.wait(lock, []{ return current % 2 == 0; });
std::cout << current << " ";
++current; // 增加当前数字
cv.notify_all(); // 通知另一个线程可以打印
}
}
int main() {
std::thread t1(printOdd); // 打印奇数
std::thread t2(printEven); // 打印偶数
t1.join();
t2.join();
return 0;
}