C++面試題:多個線程像數組里面添加數據,你會怎么設計這個數組?
多線程環境下向數組添加數據的結構時,我們需要考慮線程安全。一般有幾種常見的設計方案:
方案 1:互斥鎖保護動態數組
實現思路:使用互斥鎖(std::mutex)保護對數組的每次操作,確保同一時間只有一個線程修改數組。 這個方案是最簡單最常見的,編寫代碼也容易。
代碼示例:
#include <vector>
#include <mutex>
classThreadSafeVector {
private:
std::vector<int> data;
std::mutex mtx;
public:
voidadd(int value){
std::lock_guard<std::mutex> lock(mtx);
data.push_back(value);
}
};
- 優點:簡單易實現,直接利用標準庫。
- 缺點:高并發下鎖競爭可能成為性能瓶頸。
方案 2:原子索引 + 預分配內存
什么是原子索引?
原子索引是一種在多線程環境下,通過 原子操作 管理共享資源(如數組寫入位置)的
技術。它的核心思想是使用原子變量(如 std::atomic)記錄當前可寫入的位置索引,多個線程通過原子操作安全地競爭索引,避免數據競爭(Data Race)。
實現思路:預分配固定大小的數組,通過原子變量管理當前寫入位置索引。僅當空間不足時,通過鎖動態擴容。
代碼示例:
#include <vector>
#include <atomic>
#include <mutex>
classConcurrentArray {
private:
std::vector<int> data;
std::atomic<size_t> index{0};
std::mutex mtx;
staticconstsize_t INIT_SIZE = 1024;
public:
ConcurrentArray() {
data.resize(INIT_SIZE);
}
voidadd(int value){
size_t idx = index.fetch_add(1, std::memory_order_relaxed);
if (idx < data.size()) {
data[idx] = value;
} else {
// 處理擴容
std::lock_guard<std::mutex> lock(mtx);
if (idx >= data.size()) {
data.resize(data.size() * 2);
}
data[idx] = value;
}
}
};
- 優點:大部分寫入操作無鎖,性能較高。
- 缺點:預分配可能導致內存浪費,動態擴容時仍需加鎖。
方案 3:線程本地存儲(Thread-Local Storage)
什么是線程本地存儲?
線程本地存儲(TLS)是一種允許每個線程擁有獨立數據副本的機制。在多線程環境中,每個線程操作自己的本地數據,無需與其他線程競爭共享資源,從而完全避免鎖的使用。當需要全局匯總數據時,再通過同步機制(如鎖)合并各線程的本地數據。
實現思路:每個線程使用本地數組緩存數據,定期將數據合并到全局數組。
代碼示例:
#include <vector>
#include <mutex>
#include <thread>
classThreadLocalArray {
private:
thread_localstatic std::vector<int> local_data;
std::vector<int> global_data;
std::mutex mtx;
public:
voidadd(int value){
local_data.push_back(value);
if (local_data.size() >= 100) { // 定期合并
merge();
}
}
voidmerge(){
std::lock_guard<std::mutex> lock(mtx);
global_data.insert(global_data.end(), local_data.begin(), local_data.end());
local_data.clear();
}
};
- 優點:完全無鎖寫入,合并時才需同步。
- 缺點:數據訪問延遲,合并時可能阻塞。
三個方案對比測試
我們模擬寫入下:寫入 1 萬個數據和 10 萬個數據
示例代碼:
#include <iostream>
#include <vector>
#include <atomic>
#include <mutex>
#include <thread>
#include <chrono>
#include <memory>
constexprint TOTAL_DATA = 10000;
constexprint THREAD_NUM = 4;
constexprint DATA_PER_THREAD = TOTAL_DATA / THREAD_NUM;
// 測試工具函數
template<typename T>
voidrun_test(T& container, const std::string& name){
auto start = std::chrono::high_resolution_clock::now();
std::vector<std::thread> threads;
for (int i = 0; i < THREAD_NUM; ++i) {
threads.emplace_back([&]() {
for (int j = 0; j < DATA_PER_THREAD; ++j) {
container.add(j);
}
});
}
for (auto& t : threads) {
t.join();
}
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> duration = end - start;
std::cout << name << " Time: " << duration.count() << "s\n";
}
// 方案1:互斥鎖保護動態數組
classMutexVector {
std::vector<int> data;
std::mutex mtx;
public:
voidadd(int value){
std::lock_guard<std::mutex> lock(mtx);
data.push_back(value);
}
};
// 方案2:原子索引+預分配內存
classAtomicVector {
std::vector<int> data;
std::atomic<size_t> index{ 0 };
std::mutex mtx;
public:
AtomicVector() { data.resize(1024); }
voidadd(int value){
size_t idx = index.fetch_add(1, std::memory_order_relaxed);
if (idx < data.size()) {
data[idx] = value;
}
else {
std::lock_guard<std::mutex> lock(mtx);
if (idx >= data.size()) data.resize(data.size() * 2);
data[idx] = value;
}
}
};
// 方案3:線程本地存儲
classThreadLocalArray {
thread_localstatic std::vector<int> local_data;
std::vector<int> global_data;
std::mutex mtx;
public:
voidadd(int value){
local_data.push_back(value);
if (local_data.size() >= 100) {
std::lock_guard<std::mutex> lock(mtx);
global_data.insert(global_data.end(), local_data.begin(), local_data.end());
local_data.clear();
}
}
~ThreadLocalArray() {
std::lock_guard<std::mutex> lock(mtx);
global_data.insert(global_data.end(), local_data.begin(), local_data.end());
}
};
thread_local std::vector<int> ThreadLocalArray::local_data;
intmain(){
// 測試方案1
{
MutexVector vec;
run_test(vec, "MutexVector");
}
// 測試方案2
{
AtomicVector vec;
run_test(vec, "AtomicVector");
}
// 測試方案3
{
ThreadLocalArray arr;
run_test(arr, "ThreadLocalArray");
}
return0;
}
TOTAL_DATA = 10000;
我在 VS2022 上運行結果如下圖:
TOTAL_DATA = 100000;
在 VS2022 上運行結果如下圖:
可以看到數據量越大,方案 2 的性能越高。
選擇建議
- 低競爭場景:方案 1(互斥鎖)簡單可靠。
- 高并發寫入:方案 2(原子索引)性能更優。
- 允許最終一致性:方案 3(線程本地存儲)避免鎖爭用。