You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

向量插入是否需用Mutex?有序/无序容器线程安全问题问询

多线程容器操作线程安全问题解答

已知在多线程环境下对std::vector执行erase操作时需要使用mutex,编写测试代码验证时发现:为erase操作添加mutex后程序看似正常,但约10分钟后foo1函数遍历vector时出现nullptr异常;若erase操作不使用mutex会立即触发段错误。针对此场景提出以下问题:

  • Q1:push_back仅在vector尾部插入元素,为何会在中间索引(如vector长度为521时,索引129)出现空指针错误?
  • Q2:使用vector、deque等有序容器时,插入操作是否需要使用mutex?
  • Q3:unordered_map等无序容器的插入操作无需mutex为何能正常运行约20分钟?

测试代码

class Test
{
public:
    Test(int idx) : m_index(idx) {}
    int     m_index = { -1 };
    int     m_count = { 0 };
};
std::vector<std::unique_ptr<Test>>  m_vec;
std::mutex                          m_mutex;


void foo1() // print element data
{
    while (true)
    {
        std::unique_lock ulock(m_mutex);
        for (auto& e : m_vec)
        {
            e->m_count++;
            printf("%d : Count : %d\n", e->m_index, e->m_count);
        }
        ulock.unlock();
        std::this_thread::sleep_for(std::chrono::milliseconds(5));
    }
}

void foo2() // Only insert element
{ 
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<int> dis(0, 99);

    while (true)
    {
        int t = dis(gen);
        if (t >= 0 && t < 10)
        {
            //std::unique_lock ulock(m_mutex);
            m_vec.push_back(std::make_unique<Test>(m_vec.size()));
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(t));
    }
}

void foo3() // Only remove element
{
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<int> dis(0, 99);

    while (true)
    {
        int t = dis(gen);
        if (t >= 0 && t < 10)
        {
            std::unique_lock ulock(m_mutex);
            if (m_vec.empty() == false)
                m_vec.erase(m_vec.begin());
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(t));
    }
}

int main()
{
    m_vec.push_back(std::make_unique<Test>(1));
    m_vec.push_back(std::make_unique<Test>(2));
    m_vec.push_back(std::make_unique<Test>(3));
    
    std::thread t1(foo1);
    std::thread t2(foo2);
    std::thread t3(foo3);

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

    return 0;
}

问题解答

Q1 解析

vector的push_back并非只会操作尾部元素:当容器当前容量不足时,会触发内存重分配——申请一块更大的新内存,将原数组中所有元素拷贝/移动到新内存,随后释放旧内存。

你的测试代码中foo2的push_back未加锁,和加锁的foo1遍历、foo3的erase操作并发执行时,会出现致命竞态:

  • 若foo1正遍历旧内存中的元素,foo2的push_back完成了内存重分配,旧内存被释放,此时foo1访问的是已失效的内存区域,里面的unique_ptr会变成野指针,表现为nullptr或随机值,即使是中间索引的元素也会受影响(因为整个底层数组都被替换了)。
  • 同时foo3在头部执行erase会导致后续元素向前移动,若此时和未加锁的push_back并发,会破坏vector的内部状态(如size、capacity计数不同步),进一步引发非法内存访问。

Q2 解析

必须使用mutex。vector、deque这类标准容器本身不具备线程安全性,即使是尾部插入操作也不例外:

  • vector的push_back可能触发内存重分配,涉及整个底层数组的替换,过程中容器的内部控制数据(size、capacity、数据指针)会被修改,并发操作会导致这些状态不一致,引发内存访问错误。
  • deque底层是分段数组结构,插入操作可能涉及分段的添加或合并,同样会修改容器的内部控制结构,并发下必然出现竞态条件。

任何对容器的修改操作(插入、删除、元素修改),以及与修改操作并发的读取操作,都必须通过锁机制同步,否则会触发未定义行为。

Q3 解析

这只是未定义行为的偶然表现,不代表unordered_map的插入操作是线程安全的:

  • unordered_map底层是哈希表,插入时需要计算哈希值、查找桶位置、修改桶内的链表/红黑树结构(不同实现细节有差异),并发插入会直接破坏哈希表的内部结构(如桶链表断裂、哈希冲突处理逻辑出错)。
  • 能运行20分钟只是因为竞态条件没有立刻触发致命错误,但随着时间推移,哈希表的内部结构会逐渐损坏,最终必然会出现崩溃、数据错乱、死循环等问题。

标准库中仅少数容器的特定操作是线程安全的(比如std::vector的const成员函数在无并发修改时安全),所有修改操作(包括插入)都必须通过同步机制保护。


内容的提问来源于stack exchange,提问作者mystes

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 17:25:19