MST

星途 面试题库

面试题:C++ 多线程编程与内存模型

在 C++ 多线程编程中,描述一下 C++11 引入的内存模型(memory model)的关键特性。如何利用原子操作(atomic operations)和互斥锁(mutex)来保证多线程环境下的数据一致性?请给出一个简单的示例代码,展示如何使用这些机制来实现线程安全的计数器。
28.0万 热度难度
编程语言C++

知识考点

AI 面试

面试题答案

一键面试

C++11 内存模型关键特性

  1. 顺序一致性(Sequential Consistency):这是最强的内存模型,它保证所有线程都以相同的顺序观察到所有的内存访问操作,就好像所有线程的操作在一个全局的序列中执行。在这种模型下,编译器和处理器不会对内存访问操作进行重排序,以确保程序的执行顺序与代码的编写顺序一致。
  2. 释放 - 获取语义(Release - Acquire Semantics)
    • 释放语义(Release Semantics):当一个线程对某个变量执行带有释放语义的写操作时,所有在这个写操作之前的内存访问都被提交到内存中,并且对其他线程可见。
    • 获取语义(Acquire Semantics):当一个线程对某个变量执行带有获取语义的读操作时,该线程会从内存中获取最新的值,并且保证在这个读操作之后的内存访问不会被重排序到这个读操作之前。这确保了获取操作能够看到所有之前由释放操作提交到内存中的修改。
  3. 宽松原子操作(Relaxed Atomic Operations):这类原子操作不提供任何顺序保证,仅仅保证对单个变量的操作是原子的,即不会被其他线程干扰。它们适用于一些不需要严格顺序一致性,但需要保证数据完整性的场景,例如统计计数器。

利用原子操作和互斥锁保证数据一致性

  1. 原子操作:原子操作是不可分割的操作,在多线程环境下,对原子类型的操作不会被其他线程中断。C++11 提供了 <atomic> 头文件,其中定义了各种原子类型,如 std::atomic<int>。通过使用原子类型,可以避免数据竞争问题,因为对原子类型的读和写操作都是原子的。
  2. 互斥锁:互斥锁(std::mutex)用于保护共享资源,确保在同一时间只有一个线程可以访问该资源。当一个线程获取了互斥锁,其他线程必须等待直到该线程释放互斥锁。通过在访问共享资源前后分别调用 lock()unlock() 方法,可以保证多线程环境下对共享资源的安全访问。

示例代码

#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>

std::mutex mtx;
std::atomic<int> counter(0);

void increment() {
    for (int i = 0; i < 10000; ++i) {
        // 使用互斥锁保护共享资源
        std::lock_guard<std::mutex> lock(mtx);
        counter++;
    }
}

int main() {
    std::thread threads[10];
    for (int i = 0; i < 10; ++i) {
        threads[i] = std::thread(increment);
    }

    for (auto& thread : threads) {
        thread.join();
    }

    std::cout << "Final counter value: " << counter << std::endl;
    return 0;
}

在上述代码中:

  1. std::atomic<int> counter(0); 定义了一个原子类型的计数器,保证对计数器的操作是原子的。
  2. std::mutex mtx; 定义了一个互斥锁,用于保护对计数器的非原子操作(如在 increment 函数中的 counter++ 操作,虽然 counter 是原子类型,但 ++ 操作不是原子操作,所以需要互斥锁保护)。
  3. increment 函数中,使用 std::lock_guard<std::mutex> lock(mtx); 来自动获取和释放互斥锁,确保在同一时间只有一个线程可以修改计数器的值。
  4. main 函数中,创建 10 个线程,每个线程都调用 increment 函数来增加计数器的值,最后输出计数器的最终值。这样就通过原子操作和互斥锁实现了线程安全的计数器。