在多线程编程中,经常需要多个线程访问和修改共享的全局变量。假设有两个线程同时访问同一个变量,我们会遇到一个常见的问题:一个线程修改了全局变量,另一个线程却没有立刻看到这个修改。这种问题的根源就在于内存可见性。

为什么修改没有立即同步?
为了提高性能,现代计算机使用了多级存储系统(如CPU缓存、主内存等)。CPU会把一些常用的数据存放在自己的缓存中,而不是每次都去读取较慢的主内存。这是为了加速程序执行。然而,这也会导致内存可见性问题。
具体来说,当一个线程修改了全局变量的值时,修改可能只是存储在线程所在的CPU缓存中,而没有立即写回到主内存。另一个线程在访问这个变量时,可能会读取到自己缓存中的旧值,而不是最新的修改值。
举个例子
假设我们有一个全局变量 counter,并且有两个线程:线程A和线程B。线程A负责修改counter的值,线程B负责读取它的值。

- 线程A修改了
counter的值:线程A将counter的值从0改为10。修改可能只发生在线程A所在的CPU缓存中。 - 线程B访问
counter的值:线程B读取counter时,可能会读取到自己缓存中的值(比如还是0),而不是线程A刚刚修改的10。
这种现象就叫做内存可见性问题。线程B没有看到线程A的修改,因为缓存中的数据没有同步到主内存。
如何解决这个问题?
为了确保线程B能够看到线程A的修改,我们需要使用一些同步机制,保证所有线程在访问共享变量时能够看到最新的值。常见的同步机制有两种:互斥锁(mutex)和原子操作(std::atomic)。
1. 使用互斥锁
互斥锁是确保同一时刻只有一个线程能够访问或修改共享资源的机制。通过使用互斥锁,我们可以确保线程A修改counter时,线程B必须等待,直到线程A释放锁才能读取到最新的值。
举个例子:
#include <QMutex>
QMutex mutex; // 定义一个全局互斥锁
int counter = 0; // 全局变量
// 线程A修改全局变量
void threadA() {
mutex.lock(); // 获取锁
counter = 10; // 修改全局变量
mutex.unlock(); // 释放锁
}
// 线程B访问全局变量
void threadB() {
mutex.lock(); // 获取锁
int value = counter; // 访问全局变量
mutex.unlock(); // 释放锁
}
在这个例子中,线程A在修改counter之前加锁,修改完成后释放锁;线程B在访问counter之前也需要加锁,确保它看到的是最新的值。这样就避免了缓存不同步的问题。
2. 使用原子操作
对于一些简单的数据类型(比如int、bool等),我们可以使用原子操作来保证线程间的同步。原子操作确保了对变量的读写是不可中断的,且在多个线程之间修改的数据是同步的。
例如:
#include <atomic>
std::atomic<int> counter = 0; // 使用原子类型的全局变量
// 线程A修改全局变量
void threadA() {
counter.store(10, std::memory_order_relaxed); // 修改全局变量
}
// 线程B访问全局变量
void threadB() {
int value = counter.load(std::memory_order_relaxed); // 读取全局变量
}
这里我们使用了std::atomic<int>来声明counter,这意味着对counter的所有操作都是原子的,且能够保证在不同线程之间修改的数据能够立即同步。原子操作不需要显式地加锁,因此在某些简单场景下,它提供了一种更高效的同步方式。
总结
在多线程编程中,多个线程访问同一个全局变量时,可能会遇到内存可见性的问题:即一个线程的修改,另一个线程未必能立即看到。这是因为CPU可能会将数据保存在自己的缓存中,而不立刻同步到主内存。
要解决这个问题,我们可以使用互斥锁来确保同一时刻只有一个线程访问共享变量,或者使用原子操作来保证线程间的数据同步。通过这些同步机制,我们可以确保线程间的访问是安全的,避免数据不一致的情况发生。

被折叠的 条评论
为什么被折叠?



