如果在多线程程序中对全局变量的访问没有进行适当的同步控制(例如使用互斥锁、原子变量等),会导致多个线程同时访问和修改全局变量时发生竞态条件(race condition)。这种竞态条件可能会导致一系列不确定和严重的后果。
在C++中,可以通过使用互斥锁(mutex)、原子操作、读写锁来实现对全局变量的互斥访问。
数据竞争发生在多个线程同时访问同一个变量,并且至少有一个线程在写该变量时没有进行同步。由于缺少同步机制,多个线程对全局变量的操作可能会相互干扰,导致变量的值不可预测。
示例:
#include
#include
int globalVar = 0;
void increment() {
globalVar++;
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Global variable: " << globalVar << std::endl;
return 0;
}
globalVar++
并不是一个原子操作。它由多个步骤组成:读取值、增加值、写回。在这段代码中,t1
和 t2
可能会同时读取globalVar
的值,导致两个线程同时修改它的值,最终的结果会小于预期的2
。这就是典型的数据竞争。在没有同步控制的情况下,多个线程可能会对全局变量进行同时读写操作,导致变量处于不一致的状态。例如,多个线程可能会同时读取和修改相同的变量,导致最终状态不符合预期。
示例: 假设你有一个程序要求维护一个全局的计数器。如果没有加锁来确保线程安全,两个线程同时执行时,计数器可能会被写成一个无意义的值。
#include
#include
int counter = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
counter++; // 非线程安全操作
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Counter: " << counter << std::endl;
return 0;
}
counter++
可能会导致多个线程在同一时刻读取到相同的计数器值,并同时将相同的更新值写回变量,这会使得counter
的最终值远小于预期的200000
。由于数据竞争或者不一致的状态,程序可能会进入一个不可预测的状态,导致崩溃。全局变量的值在多线程的竞争中可能会发生损坏,从而导致未定义的行为(undefined behavior)。
例如:
std::mutex
是C++标准库中的一种机制,用于避免多个线程同时访问同一个资源(如全局变量)时发生竞争条件。
下面是一个示例,展示了如何使用std::mutex
来保护全局变量:
#include
#include
#include
std::mutex mtx; // 定义全局互斥锁
int globalVar = 0; // 定义全局变量
void threadFunction() {
std::lock_guard<std::mutex> lock(mtx); // 上锁,确保互斥
// 访问和修改全局变量
++globalVar;
std::cout << "Global variable: " << globalVar << std::endl;
// 锁会在lock_guard离开作用域时自动释放
}
int main() {
std::thread t1(threadFunction);
std::thread t2(threadFunction);
t1.join();
t2.join();
return 0;
}
std::mutex
: 用于保护共享资源(如全局变量)。std::lock_guard
: 是一个RAII风格的封装器,它在构造时自动上锁,在析构时自动解锁,确保了线程安全。threadFunction
中,每个线程在访问globalVar
之前都会先获得互斥锁,这样就能确保线程之间不会同时访问和修改全局变量。使用std::mutex
可以防止不同线程之间因竞争访问全局变量而引发的错误或不一致问题。
有时如果你需要更细粒度的控制,还可以考虑使用std::unique_lock
,它比std::lock_guard
更灵活,允许手动控制锁的获取和释放。
std::unique_lock
是 C++11 标准库中的一种互斥锁包装器,它提供了比 std::lock_guard
更灵活的锁管理方式。std::unique_lock
允许手动控制锁的获取和释放,而不仅仅是在对象生命周期结束时自动释放锁(如 std::lock_guard
所做的那样)。这使得它比 std::lock_guard
更加灵活,适用于更复杂的场景,比如需要在同一作用域内多次锁定或解锁,或者需要在锁定期间进行一些其他操作。
std::unique_lock
的关键特性:std::unique_lock
支持手动解锁和重新锁定,它比 std::lock_guard
更加灵活。std::unique_lock
支持与条件变量一起使用,这是 std::lock_guard
无法做到的。std::unique_lock
默认会在构造时自动加锁。
#include
#include
#include
std::mutex mtx;
void threadFunction() {
std::unique_lock<std::mutex> lock(mtx); // 构造时自动上锁
std::cout << "Thread is running\n";
// 临界区的操作
// 锁会在 lock 对象超出作用域时自动释放
}
int main() {
std::thread t1(threadFunction);
std::thread t2(threadFunction);
t1.join();
t2.join();
return 0;
}
std::unique_lock
允许你在锁定期间手动解锁和重新加锁,这对于一些需要临时释放锁的场景非常有用。
#include
#include
#include
std::mutex mtx;
void threadFunction() {
std::unique_lock<std::mutex> lock(mtx); // 构造时自动上锁
std::cout << "Thread is running\n";
// 临界区的操作
lock.unlock(); // 手动解锁
std::cout << "Lock released temporarily\n";
// 临界区之外的操作
lock.lock(); // 重新加锁
std::cout << "Lock acquired again\n";
// 临界区操作继续进行
}
int main() {
std::thread t1(threadFunction);
std::thread t2(threadFunction);
t1.join();
t2.join();
return 0;
}
std::unique_lock
也允许你延迟锁定,通过传递一个 std::defer_lock
参数给构造函数来实现。这会创建一个未锁定的 std::unique_lock
,你可以在稍后手动调用 lock()
来加锁。
#include
#include
#include
std::mutex mtx;
void threadFunction() {
std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 延迟加锁
std::cout << "Thread is preparing to run\n";
// 做一些不需要加锁的操作
lock.lock(); // 手动加锁
std::cout << "Thread is running under lock\n";
// 临界区的操作
}
int main() {
std::thread t1(threadFunction);
std::thread t2(threadFunction);
t1.join();
t2.join();
return 0;
}
std::unique_lock
是与条件变量一起使用的理想选择,它支持对互斥锁的手动解锁和重新加锁。这在条件变量的使用场景中非常有用,因为在等待条件时需要解锁互斥锁,而在条件满足时重新加锁。
#include
#include
#include
#include
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void threadFunction() {
std::unique_lock<std::mutex> lock(mtx); // 上锁
while (!ready) { // 等待 ready 为 true
cv.wait(lock); // 等待,自动解锁并挂起线程
}
std::cout << "Thread is running\n";
}
void notify() {
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟一些操作
std::cout << "Notifying the threads\n";
std::unique_lock<std::mutex> lock(mtx); // 上锁
ready = true;
cv.notify_all(); // 通知所有线程
}
int main() {
std::thread t1(threadFunction);
std::thread t2(threadFunction);
std::thread notifier(notify);
t1.join();
t2.join();
notifier.join();
return 0;
}
std::condition_variable
和 std::unique_lock
:
threadFunction
中,cv.wait(lock)
会释放锁并等待条件变量的通知。std::unique_lock
能够在调用 wait
时自动释放锁,并且在 wait
返回时会重新加锁,这使得 std::unique_lock
成为使用条件变量的最佳选择。cv.notify_all()
:通知所有等待该条件的线程,thread1
和 thread2
都会在条件满足时继续执行。
std::shared_mutex
是 C++17 引入的一个同步原语,它提供了一种读写锁机制,允许多个线程共享读取同一资源,而只有一个线程能够独占写入该资源。相比于传统的 std::mutex
(只支持独占锁),std::shared_mutex
可以提高并发性,特别是在读操作远多于写操作的情况下。
std::shared_mutex
的工作原理:std::shared_mutex
:std::shared_mutex
提供了两种类型的锁:
std::unique_lock
:用于获取独占锁。std::shared_lock
:用于获取共享锁。#include
#include
#include
#include
std::shared_mutex mtx; // 定义一个 shared_mutex
int sharedData = 0;
void readData(int threadId) {
std::shared_lock<std::shared_mutex> lock(mtx); // 获取共享锁
std::cout << "Thread " << threadId << " is reading data: " << sharedData << std::endl;
}
void writeData(int threadId, int value) {
std::unique_lock<std::shared_mutex> lock(mtx); // 获取独占锁
sharedData = value;
std::cout << "Thread " << threadId << " is writing data: " << sharedData << std::endl;
}
int main() {
std::vector<std::thread> threads;
// 启动多个线程进行读取操作
for (int i = 0; i < 5; ++i) {
threads.push_back(std::thread(readData, i));
}
// 启动一个线程进行写入操作
threads.push_back(std::thread(writeData, 100, 42));
// 等待所有线程结束
for (auto& t : threads) {
t.join();
}
return 0;
}
std::shared_lock
):线程 readData
使用 std::shared_lock
获取共享锁,这允许多个线程同时读取 sharedData
,因为读取操作是线程安全的。std::unique_lock
):线程 writeData
使用 std::unique_lock
获取独占锁,这确保了只有一个线程可以写 sharedData
,并且写操作会阻塞所有其他线程(包括读操作和写操作)。在这个示例中,多个读线程可以并行执行,因为它们都获取了共享锁。只有当写线程(获取独占锁)执行时,其他线程(无论是读线程还是写线程)会被阻塞。
std::shared_mutex
主要适用于读多写少的场景。假设有一个资源(如缓存、数据结构),它在大部分时间内被多个线程读取,但偶尔需要被更新。在这种情况下,std::shared_mutex
可以让多个读操作并行执行,同时避免写操作导致的不必要的阻塞。
例如:
std::shared_mutex
与 std::mutex
比较:std::mutex
:提供独占锁,适用于写操作频繁且不需要并发读的场景。每次加锁时,其他线程都无法进入临界区。std::shared_mutex
:适用于读多写少的场景,允许多个线程同时读取共享资源,但写操作会阻塞所有其他操作。std::shared_mutex
可以提高并发性,因为多个线程可以同时读取数据。std::mutex
,因为写操作需要独占资源并阻塞所有其他操作。与 std::mutex
一样,std::shared_mutex
也可以与条件变量(std::condition_variable
)一起使用,不过在使用时要注意,不同的线程需要加锁和解锁对应的锁。
#include
#include
#include
#include
std::shared_mutex mtx;
std::condition_variable_any cv;
int sharedData = 0;
void readData() {
std::shared_lock<std::shared_mutex> lock(mtx); // 获取共享锁
while (sharedData == 0) { // 等待数据可用
cv.wait(lock); // 等待数据被写入
}
std::cout << "Reading data: " << sharedData << std::endl;
}
void writeData(int value) {
std::unique_lock<std::shared_mutex> lock(mtx); // 获取独占锁
sharedData = value;
std::cout << "Writing data: " << sharedData << std::endl;
cv.notify_all(); // 通知所有等待的线程
}
int main() {
std::thread reader(readData);
std::thread writer(writeData, 42);
reader.join();
writer.join();
return 0;
}
std::shared_lock
:用于共享读锁,允许多个线程同时读取。cv.wait(lock)
:使用共享锁来等待某些条件的变化。cv.notify_all()
:通知所有等待线程,唤醒它们继续执行。std::atomic
是 C++11 标准引入的一种类型,用于实现原子操作。原子操作指的是操作在执行过程中不可被中断,因此能够保证数据的一致性和正确性。
std::atomic
提供了一些基本的原子操作方法,这些操作是不可分割的,保证了在多线程环境下线程安全。它主要用于数据的同步与协作,避免了传统同步原语(如锁、条件变量)所带来的性能瓶颈。
std::atomic
允许通过内存顺序来显式指定不同线程间的同步行为。std::atomic
提供的原子操作:std::atomic
支持的内存顺序(Memory Ordering):std::memory_order_acquire
:确保前面的操作在加载之后执行,即它会阻止后续的操作在此之前执行。std::memory_order_release
:确保后面的操作在存储之前执行,即它会阻止前面的操作在此之后执行。通常情况下,在使用 std::atomic
进行同步时,使用 memory_order_release
在 store
操作时,使用 memory_order_acquire
在 load
操作时,是一种常见的模式,特别是在生产者-消费者模式或者其他类似的同步模式下。
memory_order_release
和 memory_order_acquire
一般搭配使用。
这种组合是为了确保 内存顺序的一致性,并且保证数据正确的可见性。具体来说:
memory_order_release
:在执行 store
操作时,它会确保在 store
之前的所有操作(如数据写入)不会被重排序到 store
之后,保证当前线程的写操作对其他线程是可见的。因此,store
操作保证所有前置的写操作都会在这个 store
完成后被其他线程看到。
memory_order_acquire
:在执行 load
操作时,它会确保在 load
之后的所有操作(如数据读取)不会被重排序到 load
之前,保证当前线程在读取共享数据后,后续的操作可以看到正确的数据。在 load
之前的所有操作(包括对共享变量的写入)会在读取这个值之后对当前线程可见。
这两者配合使用,确保了线程间的同步,避免了数据竞态条件。
考虑一个生产者-消费者模型,生产者负责写入数据并通知消费者,消费者负责读取数据并处理。
#include
#include
#include
std::atomic<int> data(0);
std::atomic<bool> ready(false);
void consumer() {
while (!ready.load(std::memory_order_acquire)) {
// 等待 ready 为 true
}
std::cout << "Data: " << data.load(std::memory_order_relaxed) << std::endl;
}
void producer() {
data.store(42, std::memory_order_relaxed); // 写数据
ready.store(true, std::memory_order_release); // 设置 ready 为 true
}
int main() {
std::thread t1(consumer);
std::thread t2(producer);
t1.join();
t2.join();
return 0;
}
ready.store(true, std::memory_order_release)
:生产者线程在写入 ready
时使用 memory_order_release
,这意味着在 ready
设置为 true
之后,所有在此之前的操作(如对 data
的写入)对消费者线程是可见的。
ready.load(std::memory_order_acquire)
:消费者线程在读取 ready
时使用 memory_order_acquire
,这意味着消费者线程在读取 ready
后,确保它能够看到生产者线程在 store
ready
之前所做的所有修改(如 data
的值)。
这种组合保证了生产者线程的写操作(例如 data.store(42)
)对于消费者线程是可见的,且在读取 ready
后,消费者线程可以安全地读取到更新后的 data
。