C++多线程数据错乱
C++多线程数据错乱(也称为线程安全问题或数据竞争)主要是由于多个线程在没有正确同步的情况下,并发访问和修改共享数据导致的。其主要原因包括以下几个方面:
一、线程交替执行导致的非原子操作
线程在执行时,可能会在中途被挂起,然后另一个线程修改了同一个数据,导致数据不一致。
如下代码,两个线程分别对sum进行相加操作
long sum= 0L;void fun(size_t num)
{for (size_t i = 0; i < num; ++i){sum++;}
}int main()
{cout << "Before joining,sum = " << sum<< std::endl;thread t1(fun, 10000000);thread t2(fun, 10000000);t1.join();t2.join();cout << "After joining,sum = " << sum << std::endl;system("pause");return 0;
}
该程序结果为任意值,并不是我们期望的20000000。原因如下:
-
复合操作的分解风险
自增(sum++
)、修改对象属性等看似单行的操作,实际由多个汇编指令构成(读取→计算→写入)。当多线程交替执行这些步骤时,可能出现部分操作结果被覆盖,导致最终数据偏差。例如两个线程同时执行sum++
可能导致总增量只有1而非2。 -
无同步机制的并发写入
多个线程直接修改同一变量时(如共享状态标志),若未通过锁或原子操作强制串行化,后写入的数据可能覆盖前一次修改,造成逻辑错误 - 线程A读取
sum=0
,计算sum=1
,但未写回 - 线程B读取
sum=0
,计算count=1
,然后写回 - 线程A继续写回
sum=1
- 最终 sum 只加了1,而不是2
二、指令重排序引发的时序问
2.1 编译器优化与CPU乱序执行
编译器为提高性能可能调整代码顺序,而现代CPU也会对指令进行动态重排。若共享变量的访问顺序影响业务逻辑(如先更新数据再标记完成),重排序会导致其他线程读取到中间状态。
// 示例:看似顺序执行的代码可能被重排序
std::unique_lock<std::mutex> lock(mu_tex);
account_list[from].subMoney(money);
++times; // 可能被重排到addMoney前执行
account_list[to].addMoney(money);
2.2 内存屏障缺失
x86架构虽保证存储顺序一致性,但在弱内存模型架构(如ARM)中,缺少内存屏障可能导致写入延迟对其他线程可见,产生“过期读取”现象
三、内存可见性与缓存一致性
-
线程私有缓存未同步
各线程可能将共享变量缓存在CPU核心私有缓存中,修改后未及时刷新到主内存,导致其他线程读取旧值。MESI协议虽解决硬件级缓存一致性问题,但编程时仍需显式同步。 -
volatile关键字的局限性
仅用volatile
修饰变量能禁止编译器优化重排,但无法保证多线程操作的原子性,仍需配合锁或原子类型实现完整同步。
下图是数据在计算机中的具体位置,可以看到,数据在CPU,高速缓存(在CPU内部),内存,DMA,GPU中都有副本,我们的程序要保证这些副本的值都一致
四、同步机制使用不当
-
锁粒度不合理
粗粒度锁(如全局锁)会降低并发性能,细粒度锁(如分段锁)若设计不当则可能遗漏保护关键区域,两者均可能间接引发数据竞争36。 -
死锁与锁争用
嵌套锁使用错误会导致线程永久阻塞,而高并发下锁竞争会加剧线程切换开销,两者都会影响系统可靠性。例如:
// 错误示例:两个线程按不同顺序获取锁导致死锁
void thread1() {lock(mutexA);lock(mutexB); // 若thread2已锁B,则阻塞
}void thread2() {lock(mutexB);lock(mutexA); // 若thread1已锁A,则阻塞
}
五、解决方案的工程实践
- 原子类型:使用
std::atomic
确保单变量操作的原子性 - 内存序控制:通过
memory_order
参数精细控制内存可见性 - 无锁数据结构:基于CAS(Compare-And-Swap)实现高性能并发结构
- 事务内存:C++ STM(Software Transactional Memory, STM)提供原子代码块声明
std::atomic<int> testValue(0);void fun(size_t num)
{for (size_t i = 0; i < num; ++i)testValue.fetch_add(1, std::memory_order_relaxed); // 使用memory_order_relaxed保证性能
}int main()
{cout << "Before joining,sum = " << testValue << std::endl;thread t1(fun, 1000000);thread t2(fun, 1000000);t1.join();t2.join();cout << "After joining,sum = " << testValue << std::endl;system("pause");return 0;
}
总结
- 多线程数据错乱的主要原因:
- 非原子操作导致的并发修改问题
- 多个线程同时修改共享变量
- 内存可见性问题(缓存不同步)
- 指令重排序导致的执行顺序异常
- 解决方案:
- 使用
lock
、volatile
- 使用
atomic
变量 - 使用线程安全的集合
- 使用
参考:
https://zhuanlan.zhihu.com/p/20055436026
多线程数据错乱_c++ 多线程并发访问导致的数据不一致性。-CSDN博客
【面试】解释两个线程为什么不能及时同步_多线程中网口线程回复数据时长不固定是怎么回事-CSDN博客