MST
星途 面试题库

面试题:C++多线程下内存模型与数据竞争问题分析

在多线程环境下,C++的内存模型是如何工作的?请举例说明可能出现的数据竞争问题,并阐述如何利用C++11引入的原子操作和内存序来避免数据竞争,同时分析不同内存序对程序性能和正确性的影响。
21.2万 热度难度
编程语言C++

知识考点

AI 面试

面试题答案

一键面试

C++内存模型在多线程环境下的工作原理

C++内存模型定义了多线程访问内存时的规则,确保不同线程间的内存访问操作有一致的语义。它规定了一个线程对内存的写入何时对其他线程可见。例如,当一个线程修改了某个变量的值,内存模型决定了其他线程何时能看到这个修改。

数据竞争问题举例

#include <iostream>
#include <thread>

int shared_variable = 0;

void increment() {
    for (int i = 0; i < 10000; ++i) {
        shared_variable++;
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

    t1.join();
    t2.join();

    std::cout << "Final value: " << shared_variable << std::endl;
    // 预期结果应为20000,但由于数据竞争,实际结果可能小于20000
    return 0;
}

在上述代码中,两个线程同时对shared_variable进行自增操作。由于shared_variable++不是原子操作,它包含读取、增加和写入三个步骤,不同线程的这些步骤可能交错执行,导致数据竞争,最终结果可能小于预期的20000。

利用原子操作和内存序避免数据竞争

原子操作

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

std::atomic<int> shared_variable(0);

void increment() {
    for (int i = 0; i < 10000; ++i) {
        shared_variable++;
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

    t1.join();
    t2.join();

    std::cout << "Final value: " << shared_variable << std::endl;
    // 此时结果应为20000
    return 0;
}

使用std::atomic<int>类型,shared_variable++操作变为原子操作,避免了数据竞争。

内存序

内存序决定了原子操作与其他内存操作之间的顺序关系。例如std::memory_order_seq_cst(顺序一致性内存序),它保证所有线程以相同顺序看到所有原子操作,这是最严格的内存序。

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

std::atomic<bool> ready(false);
std::atomic<int> data(0);

void producer() {
    data.store(42, std::memory_order_seq_cst);
    ready.store(true, std::memory_order_seq_cst);
}

void consumer() {
    while (!ready.load(std::memory_order_seq_cst));
    std::cout << "Data: " << data.load(std::memory_order_seq_cst) << std::endl;
}

int main() {
    std::thread t1(producer);
    std::thread t2(consumer);

    t1.join();
    t2.join();

    return 0;
}

在这个例子中,使用std::memory_order_seq_cst保证了producer线程先存储数据,再设置readytrueconsumer线程先读取ready,再读取data,确保数据的一致性。

不同内存序对程序性能和正确性的影响

性能影响

  • 顺序一致性内存序(std::memory_order_seq_cst:最严格,性能开销最大。因为它要求所有线程对原子操作有全局一致的顺序,需要更多的同步操作。
  • 释放 - 获取内存序(std::memory_order_releasestd::memory_order_acquire:性能较好。std::memory_order_release在释放操作时,确保之前的写操作对其他获取该变量的线程可见;std::memory_order_acquire在获取操作时,确保之后的读操作能看到之前释放操作的结果。
  • 松弛内存序(如std::memory_order_relaxed:性能最优,因为它仅保证原子操作自身的原子性,不保证任何内存顺序,可能导致数据竞争,仅适用于一些特殊场景,如计数。

正确性影响

  • 越严格的内存序(如std::memory_order_seq_cst)越能保证程序正确性,但可能牺牲性能。
  • 较宽松的内存序(如std::memory_order_relaxed)如果使用不当,会导致数据竞争,破坏程序正确性。在选择内存序时,需要在性能和正确性之间权衡,根据程序的具体需求选择合适的内存序。