C++ 并发编程
1. 如何在C++中创建和管理线程?
C++11 引入了 <thread> 头文件,提供了标准化的线程操作接口,使得多线程编程更加便捷和可移植。
创建线程:
std::thread创建线程,可以传递函数、函数指针、lambda表达式等作为线程的执行内容。线程的生命周期管理:
join()主线程调用join()来等待新的线程结束,join()确保主线程等待子线程完成,避免主线程提前结束,导致子线程被销毁。
detach(): 如果不需要等待线程结束,而是希望让线程独立运行,可以调用detach()。被detach的线程会在后台继续执行,主线程结束后,std::thread会自动清理线程资源。线程的返回值:
std::thread()本身不支持线程返回值。如果需要线程返回结果,可以使用std::future和std::promise来获取异步结果。示例:使用
std::async获取线程返回值std::async可以方便地创建一个线程并返回一个std::future,通过future获取线程的结果。#include <iostream> #include <future> int add(int a, int b) { return a + b; } int main() { // 使用 std::async 启动线程并返回 std::future std::future<int> result = std::async(std::launch::async, add, 5, 3); // 获取线程结果 std::cout << "Result: " << result.get() << std::endl; return 0; }在这个例子中,
std::async创建了一个新的线程并执行add函数,返回一个std::future<int>对象。调用result.get()会阻塞主线程直到获取到线程的返回值。
- 为什么
detach()后无法控制线程? 分离的线程由C++运行时托管,无法再获取其状态或结果,也不保证生命周期。- 何时该用
join()vsdetach()?join()用于需要等待结果的场景;detach()适用于后台监控、日志写入等无需交互的任务。- 如何避免死锁? 按固定顺序上锁,或用
std::lock(mtx1,mtx2)同时锁定多个互斥量,并配合lock_guard的adopt_lock标签。
2. 请解释C++11中的thread、mutex和lock_guard。
概括:
在C++11中:
std::thread:用于创建和管理线程。std::mutex:互斥量,用于保护共享数据,防止多个线程同时访问。std::lock_guard:是一个作用域锁,用于简化mutex的锁定和解锁操作。
详细解释:
std::thread:
std::thread类表示单个执行线程。它可以被用来包装函数或者可调用对象,从而在单独的线程中执行。- 创建线程时,可以传递函数和参数,线程启动后会执行该函数。
- 线程可以通过
join()方法等待其完成,或者通过detach()方法使其独立运行。 示例代码: cpp #include #include void func() { std::cout << “Hello from thread!” << std::endl; } int main() { std::thread t(func); t.join(); return 0; }std::mutex:
std::mutex是互斥量,提供基本的线程同步机制,用于保护共享数据。- 当一个线程锁定了一个互斥量,其他尝试锁定同一个互斥量的线程将会阻塞,直到互斥量被解锁。
std::mutex提供了lock()、unlock()和try_lock()方法来控制互斥量的状态。 示例代码: cpp #include #include #include std::mutex mtx; // 创建互斥量 void print_block(int n, char c) { mtx.lock(); // 锁定互斥量 for (int i = 0; i < n; ++i) { std::cout << c; } std::cout << ‘\n’; mtx.unlock(); // 解锁互斥量 } int main() { std::thread t1(print_block, 50, ‘*’); std::thread t2(print_block, 50, ‘$’); t1.join(); t2.join(); return 0; }std::lock_guard:
std::lock_guard是一个作用域锁,它在构造时自动锁定互斥量,并在析构时自动解锁互斥量。它简化了互斥量的管理,避免了忘记解锁互斥量导致的死锁问题。 示例代码:
#include #include #include std::mutex mtx; // 创建互斥量 void print_block(int n, char c) { std::lock_guard[std::mutex](https://study.kamacoder.com/questions/500942) guard(mtx); // 自动锁定和解锁 for (int i = 0; i < n; ++i) { std::cout << c; } std::cout << '\n'; } int main() { std::thread t1(print_block, 50, '*'); std::thread t2(print_block, 50, '$'); t1.join(); t2.join(); return 0; }知识拓展
- 死锁:当两个或多个线程永久地等待对方持有的资源时,就会发生死锁。使用
std::lock_guard而不是手动锁定和解锁互斥量可以减少死锁的风险。- RAII:
std::lock_guard遵循RAII(Resource Acquisition Is Initialization)原则,即在对象构造时获取资源,在对象析构时释放资源,这是C++管理资源的一种常见方式。- 其他同步机制:除了
std::mutex和std::lock_guard,C++11还提供了其他同步机制,如std::unique_lock(提供更多灵活性),std::condition_variable(用于线程间的条件同步)等。- 原子操作:在多线程编程中,有时可以使用原子操作来避免使用互斥量,从而提高性能。
- 锁策略:
std::lock_guard是独占锁,std::shared_mutex提供了共享锁,允许多个线程读取共享数据,但只允许一个线程写入。- 线程局部存储:
thread_local关键字可以声明线程局部存储的变量,这些变量在每个线程中都有独立的实例,互不干扰。
3. C++中lock_guard和unique_lock的区别?
- std::lock_guard:是一个作用域,构造时自动锁定互斥量,构造时自动解锁,不支持手动解锁和重新解锁。
- std::unique_lock:提供了更灵活的互斥量锁定管理,可以手动解锁和重新锁定支持条件变量,但性能略低于lock_gurad
详细回答
- std::lock_guard
- 是一个RAII风格的互斥量锁管理类它在构造时自动调用互斥量的
lock()方法,并在析构时自动调用unlock()方法。- 不支持手动解锁和重新锁定互斥量,因此它适用于简单的锁定需求,可以防止忘记解锁导致的死锁问题。
- 其构造函数可以接受一个
std::adopt_lock参数,表示互斥量已经被当前线程锁定,无需再次锁定。 std::unique_lock:
- std::unique_lock
- 也是一个RAII风格的互斥量锁管理类,但它提供了比
std::lock_guard更灵活的锁定策略。
- 可以手动调用
lock()、unlock()和try_lock()方法来控制互斥量的锁定状态。- 支持移动语义,可以通过
std::move将锁的所有权从一个unique_lock对象转移到另一个。- 可以与
std::condition_variable一起使用,支持等待特定条件,这是std::lock_guard无法做到的。- 构造函数接受多种参数,如
std::defer_lock(不立即锁定互斥量),std::try_to_lock(尝试锁定互斥量但不阻塞),std::adopt_lock(假定互斥量已被当前线程锁定)。
4.C++中thread的join和detach的区别?
join:等待线程执行结束,线程结束后资源被回收。detach:将线程从当前线程分离,线程结束后资源自动回收,主线程不再等待该线程。
详细回答:
join:
- 当在一个线程对象上调用
join()方法时,调用线程(通常是主线程)会阻塞,直到被调用的线程完成其执行。join()方法确保线程函数的执行完成,并且线程的栈和资源被正确地回收。- 一旦线程被
join,它就不能被再次join或detach。- 如果线程在
join()之前已经结束,join()会立即返回,而不会阻塞。detch:
detach()方法将线程从其线程对象中分离出来,使得线程独立于其线程对象运行。- 一旦线程被分离,它将在后台运行,其生命周期不受创建它的线程对象的影响。
- 分离的线程在结束时,其资源会自动被回收,不需要显式地
join。- 分离后的线程对象不再代表任何线程,因此不能对其调用
join()或detach()。- 使用
detach()时,需要确保线程函数不会访问任何局部变量或资源,因为线程可能在它们不再有效时仍在运行。
5. C++ 中jthread和thread的区别
| 特性 | std::thread |
std::jthread(C++20引入) |
|---|---|---|
| 析构行为 | 危险:如果析构时线程仍在运行(joinable() == true),会调用 std::terminate导致程序崩溃。 |
安全:析构时会自动调用 join(),等待线程结束,不会导致程序崩溃。 |
| 线程管理 | 必须手动管理:在线程对象析构前,必须手动调用 join()(等待结束)或 detach()(分离后台运行)。 |
自动管理:无需手动操作,生命周期管理自动化,极大避免了资源泄露和崩溃。 |
| 停止功能 | 没有内置的线程停止机制。需要自己设计信号量、标志位等复杂的通信机制来请求线程停止。 | 内置协作式停止机制。通过 get_stop_token()获取一个 std::stop_token,线程可以定期检查是否被请求停止,从而实现优雅退出。 |
| 发起停止 | 无此功能。 | 可以调用 request_stop()向关联的线程发起停止请求。 |
6. 什么场景下使用锁,什么场景下使用原子变量
使用锁
当需要保护多个相关操作或复杂操作的数据结构,以避免多个线程同时访问导致数据不一致时。
使用原子变量
当只需要执行简单的原子操作,如计数、标志设置或简单的数据交换时。
详细回答:
使用锁
场景:锁是适用于保护一段代码或多个相关变量,确保在同一时刻只有一个线程可以执行这段代码或访问这些变量。
优势:锁可以保护复杂的操作和数据结构,允许执行一系列的操作而不用担心中间状态被其他线程打断。
劣势:可能导致死锁、饥饿、性能瓶颈、尤其高并发或锁粒度较粗的情况下,
使用原子变量
原子变量使用于单一的操作,如增加计数器、设置布尔标志或者进行简单的数据交换
优势:原子操作通常比锁更加轻量级,不需要操作系统进行上下文切换,因此性能开销较小。
**劣势:**原子变量仅限于比较简单的操作,无法用于保护比较复杂的数据结构或者执行一系列的操作。
7. 如何让理解C++中的atomic
- 原子变量,可以保证变量的赋值,修改原子性,要不全部完成,要不完全不操作。
- 时C++11引入的新特性,和锁比较相似,但是原子操作比较快,适合比较简单的操作,复杂的数据结构锁还是无可替代的。
- 原子操作在底层时硬件用自旋锁实现的
8. 锁的底层原理是什么?
mutex的本质就是一个内存标志,这个标志可以是一个flag,也可以是一个指针,指向一个持有者的线程ID,也可以是两个都有。以及一个等待(阻塞)队列,以及其他的一些若干信息。当
flag被标记成被占用的时候,或者持有者指针不为空的时候,那么它就不能被其他的线程进行访问。只有当mutex变的空闲的时候,操作系统会把等待队列的第一任务取出来,然后调度执行,如果CPU很忙,那么改任务就会就绪状态,后续如果CPU空闲了,就会被调度。
9.C++的六种内存序列
memory_order_relaxed(宽松顺序)memory_order_consume(消费顺序)memory_order_require(获取顺序)memory_order_release(释放顺序)memory_order_acq_rel(获取释放顺序)memory_order_seq_cst(顺序一致性)
- 宽松顺序
- 特性:仅保持原子操作的原子性,不保证顺序或者同步
- 使用场景:适用于不需要严格顺序的场景
- 消费顺序
- 特性:限制数据的复杂依赖顺序(以来该数据的后续操作必须在本操作之后进行操作)。C++ 17建议避免使用,因为规则复杂且实现差异大。
- 适用场景: 理论上用于依赖数据加载的场景。实践中较少使用。
- 获取顺序
- 特性:保证该操作之前的写操作不回被重拍到当前操作之后。
- 适用场景:用于写入共享数据后需要同步的场景(如锁的释放)。
- 释放顺序
- 特性:保证当前操作之前的写操作不会被重排到当前操作之后(防止写指令重排)。
- 适用场景:用于写入共享数据后需要同步的场景(如锁的释放)。
- 获取-释放顺序
- 特性:结合
acquire和release,既防止读操作重排到前面,也防止写操作重排到后面。- 适用场景:适用于读-修改-写操作(如
fetch_add)。
- 顺序一致性
- 特性:最严格的顺序,保证所有线程看到的操作顺序一致(全局顺序)。性能开销最大,但行为最直观。
- 适用场景:需要强一致性的场景(如默认的原子操作)。
10. C++的条件变量为什么要配合着锁使用?
考虑不搭配锁的情况
bool is_ready = false;
std::condition_variable cv;
void thread_1() {
while (not is_ready)
cv.wait();
// ....
}
void thread_2() {
is_ready = true;
cv.notify_one();
}
首先就有一个非常明显的错误,thread_2 里对共享状态 is_ready 读写和 thread_1的读 显然产生了竞争,这里显然得加锁。然而这里其实还有另外一个更为严重的问题,两个线程的执行顺序可能会被调度为如下形式
time_point_1: while (not is_ready) // thread_1
time_point_2: is_ready = true; // thread_2
time_point_3: cv.notify_one(); // thread_2
time_point_4: cv.wait(); // thread_1
这样就好导致线程 1「漏掉通知」,一直都被阻塞,无法恢复运行。 仔细考虑如上问题,根本的原因是 while (not is_ready) 和 cv.wait 这两个操作并不是「原子」的,这里我们就需要一个锁来保证上述两个操作的1「原子性」,也即如下实现
bool is_ready = false;
std::condition_variable cv;
std::mutex m;
void thread_1() {
std::unique_lock<std::mutex> lock(m);
while (not is_ready)
cv.wait(lock);
// ....
}
void thread_2() {
{
std::lock_guard<std::mutex> lock(m);
is_ready = true;
}
cv.notify_one();
}
这里,首先我们在 while (not is_ready) 前就加锁,接着把锁传给 wait 操作,wait 内部实现在将 thread_1挂到 cv的等待线程列表后就解锁,让其他线程可以操作共享状态 is_ready,然后在 thread_1 被唤醒时再加上锁。这样一来就保证了「 while (not is_ready)到挂载当前线程到等待列表」 这一整个过程的原子性,保证了 wait 执行的时候,notify_one必然没有执行, 也就不会出现「漏掉通知」这一现象。 所以本质上可以说这个 mutex 主要目的仍然是保护共享状态 is_ready,并不是条件变量本身,故而条件变量被设计为和锁搭配使用。
作者:江东某人 链接:https://www.zhihu.com/question/587575043/answer/2921492508 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)