1. 如何在C++中创建和管理线程?

C++11 引入了 <thread> 头文件,提供了标准化的线程操作接口,使得多线程编程更加便捷和可移植。

  • 创建线程:std::thread 创建线程,可以传递函数、函数指针、lambda表达式等作为线程的执行内容。

  • 线程的生命周期管理:

  • join()主线程调用join()来等待新的线程结束,join() 确保主线程等待子线程完成,避免主线程提前结束,导致子线程被销毁。

  • detach(): 如果不需要等待线程结束,而是希望让线程独立运行,可以调用 detach()。被 detach 的线程会在后台继续执行,主线程结束后,std::thread 会自动清理线程资源。

  • 线程的返回值:std::thread()本身不支持线程返回值。如果需要线程返回结果,可以使用std::futurestd::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() vs detach() join()用于需要等待结果的场景;detach()适用于后台监控、日志写入等无需交互的任务。
  • 如何避免死锁? 按固定顺序上锁,或用std::lock(mtx1,mtx2)同时锁定多个互斥量,并配合lock_guardadopt_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而不是手动锁定和解锁互斥量可以减少死锁的风险。
  • RAIIstd::lock_guard遵循RAII(Resource Acquisition Is Initialization)原则,即在对象构造时获取资源,在对象析构时释放资源,这是C++管理资源的一种常见方式。
  • 其他同步机制:除了std::mutexstd::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
  1. 是一个RAII风格的互斥量锁管理类它在构造时自动调用互斥量的lock()方法,并在析构时自动调用unlock()方法。
  2. 不支持手动解锁和重新锁定互斥量,因此它适用于简单的锁定需求,可以防止忘记解锁导致的死锁问题。
  3. 其构造函数可以接受一个std::adopt_lock参数,表示互斥量已经被当前线程锁定,无需再次锁定。 std::unique_lock
  • std::unique_lock
  1. 也是一个RAII风格的互斥量锁管理类,但它提供了比std::lock_guard更灵活的锁定策略。
  1. 可以手动调用lock()unlock()try_lock()方法来控制互斥量的锁定状态。
  2. 支持移动语义,可以通过std::move将锁的所有权从一个unique_lock对象转移到另一个。
  3. 可以与std::condition_variable一起使用,支持等待特定条件,这是std::lock_guard无法做到的。
  4. 构造函数接受多种参数,如std::defer_lock(不立即锁定互斥量),std::try_to_lock(尝试锁定互斥量但不阻塞),std::adopt_lock(假定互斥量已被当前线程锁定)。
4.C++中thread的join和detach的区别?
  • join:等待线程执行结束,线程结束后资源被回收。
  • detach:将线程从当前线程分离,线程结束后资源自动回收,主线程不再等待该线程。

详细回答:

join

  • 当在一个线程对象上调用join()方法时,调用线程(通常是主线程)会阻塞,直到被调用的线程完成其执行。
  • join()方法确保线程函数的执行完成,并且线程的栈和资源被正确地回收。
  • 一旦线程被join,它就不能被再次joindetach
  • 如果线程在join()之前已经结束,join()会立即返回,而不会阻塞。

detch:

  • detach()方法将线程从其线程对象中分离出来,使得线程独立于其线程对象运行。
  • 一旦线程被分离,它将在后台运行,其生命周期不受创建它的线程对象的影响。
  • 分离的线程在结束时,其资源会自动被回收,不需要显式地join
  • 分离后的线程对象不再代表任何线程,因此不能对其调用join()detach()
  • 使用detach()时,需要确保线程函数不会访问任何局部变量或资源,因为线程可能在它们不再有效时仍在运行。
5. C++ 中jthreadthread的区别
特性 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
  1. 原子变量,可以保证变量的赋值,修改原子性,要不全部完成,要不完全不操作。
  2. 时C++11引入的新特性,和锁比较相似,但是原子操作比较快,适合比较简单的操作,复杂的数据结构锁还是无可替代的。
  3. 原子操作在底层时硬件用自旋锁实现的
8. 锁的底层原理是什么?

mutex的本质就是一个内存标志,这个标志可以是一个flag,也可以是一个指针,指向一个持有者的线程ID,也可以是两个都有。以及一个等待(阻塞)队列,以及其他的一些若干信息。

flag被标记成被占用的时候,或者持有者指针不为空的时候,那么它就不能被其他的线程进行访问。只有当mutex变的空闲的时候,操作系统会把等待队列的第一任务取出来,然后调度执行,如果CPU很忙,那么改任务就会就绪状态,后续如果CPU空闲了,就会被调度。

9.C++的六种内存序列
  1. memory_order_relaxed(宽松顺序)
  2. memory_order_consume(消费顺序)
  3. memory_order_require(获取顺序)
  4. memory_order_release (释放顺序)
  5. memory_order_acq_rel(获取释放顺序)
  6. memory_order_seq_cst(顺序一致性)
  • 宽松顺序
  • 特性:仅保持原子操作的原子性,不保证顺序或者同步
  • 使用场景:适用于不需要严格顺序的场景
  • 消费顺序
  • 特性:限制数据的复杂依赖顺序(以来该数据的后续操作必须在本操作之后进行操作)。C++ 17建议避免使用,因为规则复杂且实现差异大。
  • 适用场景: 理论上用于依赖数据加载的场景。实践中较少使用。
  • 获取顺序
  • 特性:保证该操作之前的写操作不回被重拍到当前操作之后。
  • 适用场景:用于写入共享数据后需要同步的场景(如锁的释放)。
  • 释放顺序
  • 特性:保证当前操作之前的写操作不会被重排到当前操作之后(防止写指令重排)。
  • 适用场景:用于写入共享数据后需要同步的场景(如锁的释放)。
  • 获取-释放顺序
  • 特性:结合acquirerelease,既防止读操作重排到前面,也防止写操作重排到后面。
  • 适用场景:适用于读-修改-写操作(如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 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐