C++ —— 线程安全
C++ —— 线程安全
- 线程安全的基本概念
- 定义
- 共享资源和临界区
- 三个概念的介绍
- 如何保证线程安全
- volatile 关键字
线程安全的基本概念
定义
当多
个线程同时
访问同一个
资源(例如变量、数据结构或对象)时,如果程序通过适当的同步机制
保证这些访问不会
引起数据损坏
或不一致性
,那么这段代码就是线程安全
的。也就是说,无论线程
如何调度执行
,其结果
都与单线程
环境下的行为一致
。
共享资源和临界区
多个
线程同时
操作的共享资源
(如全局变量、静态变量、共享内存等)如果没有保护
,就会产生竞争条件
(race condition)。通常,访问共享资源
的代码段
称为临界区
,需要特殊的机制来保证同一时间
只有一个
线程能进入。
示例代码:
#include <iostream>
#include <thread>
using namespace std;
// 线程任务函数
// 普通函数
void func (int n, const string& s) {
for (int i = 1; i <= 5; i++) {
cout << "No." << i << ", n = " << n << ", s = " << s << endl;
this_thread::sleep_for(chrono::seconds(1));
}
}
// lambda函数
auto f = [] (int n, const string& s) {
for (int i = 1; i <= 5; i++) {
cout << "No." << i << ", n = " << n << ", s = " << s << endl;
this_thread::sleep_for(chrono::seconds(1));
}
};
// 仿函数
class mythread_1 {
public:
void operator() (int n,const string& s) {
for (int i = 1; i <= 5; i++) {
cout << "No." << i << ", n = " << n << ", s = " << s << endl;
this_thread::sleep_for(chrono::seconds(1));
}
}
};
// 类的静态成员函数
class mythread_2 {
public:
static void func (int n, const string& s) {
for (int i = 1; i <= 5; i++) {
cout << "No." << i << ", n = " << n << ", s = " << s << endl;
this_thread::sleep_for(chrono::seconds(1));
}
}
};
// 类的非静态成员函数
class mythread_3 {
public:
void func (int n, const string& s) {
for (int i = 1; i <= 5; i++) {
cout << "No." << i << ", n = " << n << ", s = " << s << endl;
this_thread::sleep_for(chrono::seconds(1));
}
}
};
int main () {
thread t1(func, 1, "普通函数 t1");
thread t2(func, 2, "普通函数 t2");
thread t3(f, 3, "lambda函数 t3");
thread t4(mythread_1(), 4, "仿函数 t4");
thread t5(mythread_2::func, 5, "静态成员函数 t5");
mythread_3 mt;
thread t6(&mythread_3::func, &mt, 6, "非静态成员函数 t6");
cout << "=== start task ===" << endl;
for (int i = 1; i <= 5; i++) {
cout << "" << endl;
this_thread::sleep_for(chrono::seconds(1));
}
cout << "=== task completed ===" << endl;
t1.join();
t2.join();
t3.join();
t4.join();
t5.join();
t6.join();
return 0;
}
编译运行这段代码,你会发现打印出来的信息有些奇怪。这是因为,cout
是全局
对象。在目前这个程序中,所有
的线程
共享cout
对象,每个线程都会利用cout
向屏幕发送数据。如果同一时间点
有多个线程
使用cout
,打印出来的内容肯定会有些奇怪。
再看一段代码:
#include <iostream>
#include <thread>
using namespace std;
int res = 0; // 定义全局变量 res
void func_add () {
for (int i = 1; i <= 1000000; i++) {
res++;
}
}
int main () {
thread t1(func_add);
thread t2(func_add);
t1.join();
t2.join();
cout << "now res = " << res << endl;
return 0;
}
我的PC
某次运行的结果如下:
now res = 1398091
这段代码使用了两个线程t1
和t2
来并行
执行func_add
数,因此会并行
对全局变量res
进行操作。由于res
是一个全局变量
,两个线程
在对其进行修改时
可能会发生竞争
条件,即两个线程可能会同时
读取和修改res
,从而导致res
的值不准确
。不管运行多少次,结果都不会是2000000
。
三个概念的介绍
- 顺序性
程序按照代码的先后顺序执行
CPU
为了提高程序整体的执行效率,可能会对代码进行优化,按照更高的顺序执行。
顺序性
指的是:CPU
虽然不能保证完全按照代码的顺序执行,但它会保证最终的结果和按照代码的顺序执行的结果一致
。
- 可见性
线程
操作共享变量
时,会将该变量从内存
加载到CPU缓存
中,修改该变量后,CPU
会立即更新缓存
,但不一定会立即将它写入内存
。若此时有其他线程访问该变量,从内存中读到的是旧数据
,而非第一个线程操作后的数据。
可见性
指的是:当多线程并发
访问共享变量
时,一个线程对共享变量的修改,其它线程能够立即看到。
- 原子性
CPU
执行指令的顺序:读取指令、从内存读取数据、执行指令、把数据写回内存。
原子性
指的是:一个操作(有可能包含有多个步骤)要么全部执行(生效),要么全部都不执行(都不生效)。
如何保证线程安全
volatile
关键字原子
操作(原子
类型)- 线程同步(
锁
)
volatile 关键字
volatile
关键字包含两方面
- 保证内存变量的
可见性
禁止
代码优化
(重排序)
用volatile
关键字修饰res
变量(其他代码不变):
volatile int res = 0;
修改后编译运行程序,并不能解决问题。因为,volatile
关键字仅解决了可见性
问题,并不能完全解决线程安全问题。
其他两个保证线程安全的方法未完待续
感谢浏览