数据共享问题分析
在多个线程中共享数据时,需要注意线程安全问题。如果多个线程同时访问同一个变量,并且其中至少有一个线程对该变量进行了写操作,那么就会出现数据竞争问题。数据竞争可能会导致程序崩溃、产生未定义的结果,或者得到错误的结果。
为了避免数据竞争问题,需要使用同步机制来确保多个线程之间对共享数据的访问是安全的。常见的同步机制包括互斥量、条件变量、原子操作等。
数据共享问题示例
#include<iostream>
#include<thread>
int a = 0;
void func() {
for (int i = 0; i < 1000000; i++)
a += 1;
}
int main() {
std::thread t1(func);
std::thread t2(func);
t1.join();
t2.join();
std::cout << a << std::endl;
return 0;
}
代码运行的结果a并非2000000
上面的代码中,定义了一个名为 shared_data 的全局变量,并在两个线程中对其进行累加操作。在 main 函数中,创建了两个线程,并分别调用了 func 函数。在 func 函数中,对 shared_data 变量进行了累加操作。
由于 shared_data 变量是全局变量,因此在两个线程中共享。对于这种共享的情况,需要使用互斥量等同步机制来确保多个线程之间对共享数据的访问是安全的。如果不使用同步机制,就会出现数据竞争问题,导致得到错误的结果
互斥量概念
互斥量(mutex)一种用于实现多线程同步的机制,用于确保多个线程之间对共享资源的访问互斥。互斥量通常用于保护共享数据的访问,以避免多个线程同时访问同一个变量或者数据结构而导致的数据竞争问题。
互斥量提供了两个基本操作:lock() 和 unlock()。当一个线程调用 lock() 函数时,如果互斥量当前没有被其他线程占用,则该线程获得该互斥量的所有权,可以对共享资源进行访问。如果互斥量当前已经被其他线程占用,则调用 lock() 函数的线程会被阻塞,直到该互斥量被释放为止。
#include<iostream>
#include<thread>
#include<mutex>
int a = 0;
std::mutex mtx;
void func() {
for (int i = 0; i < 1000000; i++) {
mtx.lock();
a += 1;
mtx.unlock();
}
}
int main() {
std::thread t1(func);
std::thread t2(func);
t1.join();
t2.join();
std::cout << a << std::endl;
return 0;
}
上面的代码中,定义了一个名为 shared_data 的全局变量,并使用互斥量 mtx 来确保多个线程对其进行访问时的线程安全。在两个线程中,分别调用了 func 函数,并传递了不同的参数。在 func 函数中,先获取互斥量的所有权,然后对 shared_data 变量进行累加操作,并输出变量的当前值。最后再释放互斥量的所有权
互斥量死锁
概念
互斥量死锁是指两个或多个线程在执行过程中,因争夺互斥量资源而造成的一种互相等待的现象,导致这些线程都无法继续执行下去,程序陷入停滞状态。简单来说,就是每个线程都持有其他线程继续执行所需的互斥量,同时又在等待其他线程释放自己所需的互斥量,从而形成一个循环等待的局面。
产生原因
互斥量死锁通常由以下四个必要条件同时满足时产生:
- 互斥条件:线程对所分配到的互斥量资源进行排他性使用,即在一段时间内某互斥量只由一个线程占用(加锁)。如果此时还有其他线程请求该互斥量,则请求者只能等待,直至占有该互斥量的线程用毕释放。
- 请求和保持条件:线程已经保持了至少一个互斥量,但又提出了新的互斥量请求,而该互斥量已被其他线程占有,此时请求线程阻塞,但又对自己已获得的其他互斥量保持不放。
- 不剥夺条件:线程已获得的互斥量,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
- 循环等待条件:在发生死锁时,必然存在一个线程——互斥量的循环链,即线程集合 {T0,T1,T2,…,Tn} 中的 T0 正在等待一个 T1 占用的互斥量;T1 正在等待 T2 占用的互斥量,……,Tn 正在等待已被 T0 占用的互斥量。
示例代码
#include<mutex>
#include<iostream>
std::mutex m1, m2;
void func_1() {
for (int i = 0; i < 500; i++) {
m1.lock();
m2.lock();
m1.unlock();
m2.unlock();
}
}
void func_2() {
for (int i = 0; i < 500; i++)
{
m2.lock();
m1.lock();
m1.unlock();
m2.unlock();
}
}
int main() {
std::thread t1(func_1);
std::thread t2(func_2);
t1.join();
t2.join();
std::cout << "over" << std::endl;
return 0;
}
解决方案都改成先获取m1再获取m2(或者都先获取m2在获取m1)
避免方法
- 统一加锁顺序:确保所有线程按照相同的顺序获取互斥量,避免循环等待的情况发生。
- 使用 RAII 风格的锁管理器:使用
std::lock_guard
或std::unique_lock
等 RAII 风格的锁管理器,它们会在对象析构时自动解锁,避免手动管理锁可能带来的错误。 - 使用
std::lock
同时锁定多个互斥量:std::lock
可以一次性锁定多个互斥量,避免因分步加锁导致的死锁问题。例如:
std::lock(mtx1, mtx2);
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
呦呦游鹿,衔草鸣麑。
翩翩飞鸟,挟子巢棲。 —曹丕