目录
本文不分析具体实现原理,《Linux线程学习》对原理层进行学习,该处重点学习C++11下的接口使用
C++条件变量
std::condition_variable线程间同步机制,允许线程在某个条件不满足的时候等待,直到其他线程通知该条件满足,线程才会继续执行。
使用条件变量的步骤
创建条件变量和互斥锁
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
等待线程
std::unique_lock
用于管理互斥锁的生命周期。cv.wait(lock, []{ return ready; });
会在ready
为true
时解除阻塞,否则当前线程会一直处于等待状态
void wait_for_ready() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; });
// 当ready为true时继续执行
std::cout << "Thread is ready to proceed." << std::endl;
}
通知线程
std::lock_guard
确保在设置ready
变量时互斥锁被持有cv.notify_one()
通知一个正在等待的线程(如果有的话)条件已经满足。cv.notify_all()
会通知所有等待的线程
void set_ready() {
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
cv.notify_one(); // 或 cv.notify_all();
}
创建10个线程,等待read条件变为true,然后通知所有线程运行实现设置好的函数
- 条件变量要和互斥锁配合使用,保证访问共享资源的时候线程安全
notify_one()
和notify_all()
不会自动释放锁,它们只会通知等待线程去重新竞争锁- 如果通知时没有线程在等待,通知会被“丢弃
#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);
cv.wait(lock, [] { return ready; }); // 等待 ready 变为 true
std::cout << "Thread " << id << std::endl;
}
void go() {
std::lock_guard<std::mutex> lock(mtx);
ready = true;
cv.notify_all(); // 通知所有线程
}
int main() {
std::thread threads[10];
// 创建 10 个线程
for (int i = 0; i < 10; ++i)
threads[i] = std::thread(print_id, i);
std::cout << "10 threads ready to race..." << std::endl;
go(); // 设置 ready 为 true 并通知所有线程
for (auto& th : threads) th.join();
return 0;
}
重点问题
条件变量是什么?C++中的作用是什么?
- 条件变量是 C++ 中用于线程间同步的机制,通常与互斥锁一起使用。它允许一个或多个线程在某个条件不满足时进入等待状态,直到其他线程通知条件满足。这样可以避免忙等待,提高程序的效率和可扩展性
条件变量与互斥锁的关系是什么?为什么需要一起使用
- 条件变量必须与互斥锁一起使用,因为条件变量在检查和修改共享数据时,需要确保线程安全。互斥锁用于保护共享数据的访问,防止多个线程同时修改数据而导致数据竞争。在等待条件满足时,线程会先锁定互斥锁,然后进入等待状态。当条件满足时,线程会重新竞争互斥锁以继续执行
std::condition_variable
的wait
函数是如何工作的
wait
函数接收一个std::unique_lock<std::mutex>
对象,并在该对象锁定的情况下调用。wait
会使当前线程进入等待状态,直到其他线程调用notify_one
或notify_all
并且指定的条件满足。wait
函数会自动释放互斥锁以允许其他线程修改条件,然后在被唤醒后重新获得互斥锁
什么是“虚假唤醒”?如何应对这种情况
- “虚假唤醒”是指线程在没有明显原因的情况下被唤醒。为了应对虚假唤醒,通常在调用
wait
函数时使用一个条件判断的循环。例如,cv.wait(lock, []{ return ready; });
,这样可以确保线程在条件未满足的情况下继续等待,而不是直接继续执行
notify_one
和notify_all
有什么区别?在什么情况下使用?
notify_one
唤醒一个等待的线程,notify_all
唤醒所有等待的线程。如果只有一个线程需要被唤醒,使用notify_one
更加高效;而如果多个线程需要同时继续执行,则使用notify_all
。选择使用哪个取决于具体的应用场景和条件逻辑
线程同步和线程异步
- 线程同步:协调多个线程对共享资源的访问,确保同一时间只有一个线程可以访问到共享资源,避免数据竞争。常用的有互斥量、条件变量、信号量等
- 线程异步:允许多个线程独立执行,不需要同步,也就是线程的执行顺序不确定,而且其他可能不会等待其他线程完成
互斥量
互斥量用于保护临界资源,确保一次只有一个线程可以访问资源
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
void print_thread_id(int id) {
mtx.lock();
std::cout << "Thread " << id << std::endl;
mtx.unlock();
}
int main() {
std::thread threads[10];
for (int i = 0; i < 10; ++i) {
threads[i] = std::thread(print_thread_id, i);
}
for (auto& th : threads) th.join();
return 0;
}
为什么需要使用互斥量?
- 互斥量用于防止多个线程同时访问共享资源时发生数据竞争,从而确保数据的一致性
解释递归互斥量的应用场景
- 递归互斥量允许同一线程多次加锁,而不会导致死锁。这在递归函数或在同一线程中多次需要锁的情况下非常有用
读写锁
允许多个线程同时读取数据,但是写操作只可以被一个线程独占(shared_mtx)
#include <iostream>
#include <thread>
#include <shared_mutex>
std::shared_mutex shared_mtx;
void read_data(int id) {
std::shared_lock<std::shared_mutex> lock(shared_mtx);
std::cout << "Thread " << id << " is reading" << std::endl;
}
void write_data(int id) {
std::unique_lock<std::shared_mutex> lock(shared_mtx);
std::cout << "Thread " << id << " is writing" << std::endl;
}
int main() {
std::thread reader1(read_data, 1);
std::thread reader2(read_data, 2);
std::thread writer(write_data, 3);
reader1.join();
reader2.join();
writer.join();
return 0;
}
RAII
一种资源管理方式,常用于包装互斥量,确保资源在对象生命周期结束的时候被释放
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
void print_thread_id(int id) {
std::lock_guard<std::mutex> lock(mtx);
std::cout << "Thread " << id << std::endl;
}
int main() {
std::thread threads[10];
for (int i = 0; i < 10; ++i) {
threads[i] = std::thread(print_thread_id, i);
}
for (auto& th : threads) th.join();
return 0;
}
RAII 在 C++ 中的重要性
- RAII 确保资源在对象生命周期结束时自动释放,防止资源泄漏。在多线程编程中,RAII 可以自动管理锁的释放,避免因异常或提前返回导致的死锁
信号量
信号量用于控制对资源访问的及计数器,C++20引入 std::conuting_semaphore 和 std::binary_semaphore
#include <iostream>
#include <thread>
#include <semaphore.h>
std::counting_semaphore<3> sem(3);
void task(int id) {
sem.acquire();
std::cout << "Thread " << id << " is in critical section" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Thread " << id << " is leaving critical section" << std::endl;
sem.release();
}
int main() {
std::thread threads[5];
for (int i = 0; i < 5; ++i) {
threads[i] = std::thread(task, i);
}
for (auto& th : threads) th.join();
return 0;
}
信号量和互斥量的主要区别
- 互斥量用于独占访问资源,而信号量可以控制多个线程同时访问有限资源。信号量是计数器,允许指定数量的线程同时访问共享资源,而互斥量通常只允许一个线程访问
屏障
用于同步多个线程,使得所有线程在某个点上等待,直到所有线程都到达改点后再继续执行
#include <iostream>
#include <thread>
#include <barrier>
std::barrier sync_point(3);
void task(int id) {
std::cout << "Thread " << id << " is working" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
sync_point.arrive_and_wait();
std::cout << "Thread " << id << " is continuing after barrier" << std::endl;
}
int main() {
std::thread t1(task, 1);
std::thread t2(task, 2);
std::thread t3(task, 3);
t1.join();
t2.join();
t3.join();
return 0;
}
什么是屏障?它在多线程编程中的作用是什么?
- 屏障用于同步多个线程,使所有线程在某个点上等待,直到所有线程都到达该点。它在并行算法中很有用,当需要确保多个线程同步执行到某个步骤时,可以使用屏障
std::call_once
保证某个初始化操作只会执行一次,无论多少个线程调用它
#include <iostream>
#include <thread>
#include <mutex>
std::once_flag flag;
void init() {
std::cout << "Initialized" << std::endl;
}
void task() {
std::call_once(flag, init);
std::cout << "Thread is running" << std::endl;
}
int main() {
std::thread t1(task);
std::thread t2(task);
t1.join();
t2.join();
return 0;
}
std::call_once
的典型应用场景是什么
std::call_once
主要用于确保某个初始化操作(如单例模式的初始化)只执行一次。这在多线程环境中避免了多次初始化导致的竞态条件和资源浪费
互斥信号量
C++20 引入了 std::binary_semaphore
,它是一种特殊的信号量,用于控制对共享资源的独占访问。互斥信号量类似于互斥量,但它更通用,可以用于更广泛的同步场景。互斥信号量要么是 0(锁定状态),要么是 1(解锁状态),这使得它能够保证一次只有一个线程访问共享资源
#include <iostream>
#include <thread>
#include <semaphore>
std::binary_semaphore sem(1); // 初始化为 1,表示解锁状态
void critical_section(int id) {
sem.acquire(); // 获取信号量,进入临界区
std::cout << "Thread " << id << " is in critical section" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟工作
std::cout << "Thread " << id << " is leaving critical section" << std::endl;
sem.release(); // 释放信号量,离开临界区
}
int main() {
std::thread t1(critical_section, 1);
std::thread t2(critical_section, 2);
t1.join();
t2.join();
return 0;
}
互斥信号量和互斥量有什么区别
- 互斥信号量和互斥量都用于控制对共享资源的独占访问。区别在于互斥信号量更通用,它不仅可以用于二元的锁定/解锁,还可以扩展为计数信号量,用于控制多个线程对资源的并发访问。而互斥量通常只用于控制单一线程访问资源。此外,信号量的 acquire 和 release 操作是独立的,而互斥量的 lock 和 unlock 操作必须成对出现
在什么场景下你会选择使用互斥信号量而不是互斥量
- 如果仅需要控制对共享资源的独占访问,那么互斥量通常是更简洁的选择。然而,如果需要更灵活的同步控制,例如将信号量作为事件通知机制或需要在多个不同函数中管理临界区的进入和退出(acquire 和 release 不必在同一作用域中),那么互斥信号量会更合适
什么是二元信号量
- 二元信号量是一种信号量,它的取值只能是 0 或 1。它用于控制一个资源的独占访问,类似于互斥量。二元信号量可以看作是计数信号量的特例,其中最大计数为 1
如何确保使用互斥信号量不会引起死锁
- 确保所有线程以相同的顺序获取多个信号量。
- 尽量减少临界区的代码,尽快释放信号量。
- 避免嵌套获取多个信号量,除非非常必要。
- 使用超时机制来获取信号量,如果无法获取,则可以选择放弃操作或重新尝试。
std::thread
C++11中引入用于创建和管理线程的类,前面的事例已经经常使用,此处不赘述。
如何启动一个线程
- 创建一个thread对象,然后传递给一个可调用对象(函数指针、函数对象、lambda表达式都可以)来启动线程
std::thread::join
的作用是什么
join()
用于阻塞调用它的线程,直到被join
的线程完成执行。它确保当前线程等待子线程执行完毕后再继续。如果不调用join()
或detach()
,std::thread
对象在析构时会终止程序
什么是
std::thread::detach
detach()
将线程与std::thread
对象分离,使得线程在后台继续运行,而不需要同步或等待它完成。分离后的线程自行运行,不能再被join
,如果主线程先于分离的线程结束,程序将继续运行,直到分离的线程完成
std::async
启动异步任务的函数模版
#include <iostream>
#include <future>
int add(int a, int b) {
return a + b;
}
int main() {
std::future<int> result = std::async(std::launch::async, add, 5, 3);
// 在这里可以执行其他操作
int sum = result.get(); // 获取异步任务的返回值
std::cout << "Sum: " << sum << std::endl;
return 0;
}
std::async
和std::thread
有什么区别
std::async
和std::thread
都可以用于创建并发任务,但std::async
更加灵活,它返回一个std::future
对象,用于获取异步任务的结果。而std::thread
不提供这种功能,线程结束后不会有返回值。std::async
还可以根据策略选择是否延迟执行或在调用get()
时同步执行
std::future
是什么
std::future
是 C++11 引入的一种同步机制,用于从异步操作中获取结果。通过std::async
返回的std::future
对象,你可以在稍后的时间点调用get()
方法来获取异步操作的结果,get()
会阻塞直到结果可用
thread和async对比
std::thread
用于启动一个新的线程,适合需要手动管理线程生命周期和同步的场景。它简单直接,但不提供返回值和异常管理功能。
std::async
提供了一种更高级的异步任务管理方式,可以轻松地获取返回值并处理异常,适合需要返回结果或自动管理线程生命周期的场景
std::async
的std::launch
参数有什么作用?
std::async
的第二个参数是std::launch
,它用于控制异步任务的启动策略:
std::launch::async
:强制任务在新线程中异步启动。std::launch::deferred
:任务在调用get()
或wait()
时才会同步执行,不会创建新线程。- 默认情况下,可以同时使用这两个选项,如
std::launch::async | std::launch::deferred
,这样由系统决定以哪种方式启动任务
I/O操作
标准输入和输出
常用操作总结
std::cin
:标准输入流,通常从键盘接收数据。std::cout
:标准输出流,通常将数据输出到控制台。std::cerr
:标准错误输出流,通常用于输出错误信息
#include <iostream>
int main() {
int number;
std::cout << "Enter a number: "; // 输出提示信息
std::cin >> number; // 从标准输入读取一个整数
std::cout << "You entered: " << number << std::endl; // 输出读取的整数
return 0;
}
重定向
输入、输出信息指定输出到指定的设备或者文件中(下文事例中指定输出到文件中)
#include <iostream>
#include <fstream>
int main() {
std::ofstream outFile("output.txt");
if (!outFile) {
std::cerr << "Error opening file!" << std::endl;
return 1;
}
std::streambuf* coutBuf = std::cout.rdbuf(); // 保存原始的缓冲区
std::cout.rdbuf(outFile.rdbuf()); // 将 `std::cout` 的缓冲区重定向到文件
std::cout << "This will be written to the file instead of the console." << std::endl;
std::cout.rdbuf(coutBuf); // 恢复原始缓冲区
outFile.close();
return 0;
}
如何将
std::cout
的输出重定向到文件?
- 可以通过
std::cout.rdbuf()
函数将std::cout
的缓冲区重定向到文件流的缓冲区。例如:std::cout.rdbuf(outFile.rdbuf());
会将std::cout
的输出重定向到文件outFile
如何将输入从文件重定向到
std::cin
- 可以使用
std::cin.rdbuf()
将std::cin
的缓冲区重定向到文件流的缓冲区。例如:std::ifstream inFile("input.txt"); std::cin.rdbuf(inFile.rdbuf());
将使std::cin
从input.txt
文件读取输入
清除输入缓冲区
std::cin::ignore对缓冲区进行处理,处理输入缓冲区的所有内容,直到遇到换行符为止
#include <iostream>
#include <limits>
int main() {
int number;
std::cout << "Enter a number: ";
std::cin >> number;
// 清除输入缓冲区
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::cout << "Enter another number: ";
std::cin >> number;
std::cout << "You entered: " << number << std::endl;
return 0;
}