关于Boost无锁freelist_stack中C++未定义行为的疑问
Boost freelist_stack的C++未定义行为验证
问题背景
发现Boost freelist_stack可能存在C++未定义行为,需确认推理是否正确:该结构的节点内存被用时存储T类型对象,未用时作为freelist_node。简化后的核心代码如下:
//All in freelist_stack class definition: struct freelist_node { tagged_ptr<freelist_node> next; }; atomic<tagged_ptr<freelist_node>> pool_; T* construct (void) { T* node = allocate_impl(); new(node) T(); //A:构造T对象,覆盖原freelist_node内存 } T* allocate_impl (void) { tagged_ptr<freelist_node> old_pool = pool_.load(memory_order_consume); for(;;) { //old_pool-> calls old_pool.get_ptr(), returning freelist_node* freelist_node * new_pool_ptr = old_pool->next.get_ptr(); //B:读取freelist_node的next指针 tagged_ptr<freelist_node> new_pool (new_pool_ptr, old_pool.get_next_tag()); if (pool_.compare_exchange_weak(old_pool, new_pool)) { void * ptr = old_pool.get_ptr(); return reinterpret_cast<T*>(ptr); } } }
场景假设与疑问
假设有两个线程进入allocate_impl循环:
- 线程1成功执行CAS,拿到节点内存后调用
construct,在标记A处构造T对象,覆盖了原freelist_node的内存; - 线程2此时还在循环中,执行标记B处的代码,访问的是线程1已经覆盖为T对象的同一块内存。
针对该场景,提出以下疑问:
- 这是否属于C++未定义行为?
- 构造T对象与读取
freelist_node之间无先行发生关系,既存在数据竞争,又违反严格别名规则,这个解读是否有误? - 是否存在安全保障机制?即便CAS失败会重试,但未定义行为下编译器行为不可预测,是否是因为无优化空间才看似可用?
分析结论
1. 确实属于C++未定义行为
该场景同时触发了两类未定义行为:
- 数据竞争:线程1对内存进行写入(构造T对象本质是写入内存),线程2对同一块内存进行读取(访问
freelist_node::next),两者之间没有任何同步机制(无先行发生关系),符合C++标准中数据竞争的定义,直接导致未定义行为。 - 严格别名违规:同一块内存被同时当作
freelist_node和T两种无关类型访问(线程2读freelist_node,线程1写T),违反了C++严格别名规则——除非是char/unsigned char类型,否则不同类型的指针不能别名同一块内存。
2. 原解读无误
构造T对象的操作(线程1的标记A)和读取freelist_node的操作(线程2的标记B)之间,没有通过原子操作建立先行发生关系:线程1的CAS成功只是修改了pool_,但线程2读取的是old_pool指向的内存,该内存的修改并没有被pool_的原子操作同步到线程2。因此数据竞争和严格别名违规的判定是准确的。
3. 无内置安全保障机制,看似可用是巧合
Boost的这个实现没有针对该场景的安全保障:
- CAS失败重试的逻辑只能保证
pool_本身的原子性,但无法解决已被取出的节点内存被覆盖后,其他线程仍在读取该内存的问题; - 看似可用的原因通常是:在常见的编译器和硬件平台上,这种内存访问的实际行为可能符合直觉(比如没有激进的别名优化,硬件内存模型相对宽松),但这完全依赖于具体实现,不属于C++标准的保障范围。一旦编译器开启激进优化(比如基于严格别名规则的优化),或者运行在更严格的内存模型硬件上,就可能出现崩溃、数据错乱等不可预测的问题。
内容的提问来源于stack exchange,提问作者P. Mattione
相关产品推荐
相关产品推荐

