目录
原子操作
原子操作:对变量的读写操作,不可分割。
简单写一个程序:
#include <iostream>
#include <thread>
using namespace std;
int value = 0;
void testFunc()
{
for (int i=0;i<10000;i++)
{
value++;
}
}
int main()
{
thread t1(testFunc);
thread t2(testFunc);
t1.join();
t2.join();
cout << value;
return 0;
}
这段代码中有两个线程(t1和t2),它们同时对同一个全局变量value进行自增操作。
由于这两个线程是并行执行的,因此它们可能同时读取到value的当前值,然后同时进行自增操作,并写回新的值,当一个线程读取value时,value的值可能已经被另一个线程自增。这种情况下,线程的自增操作就会产生竞争条件,导致最终的结果无法预测。
在这段代码中,如果两个线程同时读取到value的初始值0,然后同时进行10000次自增操作,最终value的结果就会是20000[一般电脑好的话,这种情况比较多]。
有的人电脑上的结果可能会是一万多的随机数值,是由于线程的调度和执行顺序不确定导致的。不同的操作系统、硬件以及其他因素都可能会影响线程的调度和执行顺序,从而导致不同的结果。
atomic创造原子变量
可以通过atomic<int> atm_Value=0和atomic<int> atm_Value(0)的方式创建原子变量。
值得注意的是atomic中的拷贝构造和赋值运算被删除了。
#include <iostream>
#include <thread>
#include <atomic>//模板类 创建原子变量
using namespace std;
int value = 0;
atomic<int> atm_Value(0);
atomic<int> atm_Value2(0);
void testFunc()
{
for (int i=0;i<10000;i++)
{
value++;
atm_Value++;//如果有一个线程对他进行操作,则另一个线程需要等他做完操作之后,才能进行操作,这个是及其稳定的
atm_Value2 = atm_Value2 + 1;//一般情况下,分为两步的操作是不满足原子操作的
}
}
int main()
{
thread t1(testFunc);
thread t2(testFunc);
t1.join();
t2.join();
cout << value<<endl;
cout << atm_Value << endl;
cout << atm_Value2 << endl;
return 0;
}
value2结果不是2w的原因:
在多线程环境下,如果多个线程同时对atm_Value2进行+1操作,会发生竞争条件,即多个线程同时读取atm_Value2的值并尝试进行加法操作,然后再将结果赋值给atm_Value2。
由于这个过程中存在读-修改-写的操作,如果多个线程同时执行,可能会导致数据的不一致性。
atomic的原子类型
先简单写3个操作:
atomic<int> atm_Num1(2);
atomic_int atm_Num2(2);
//原子类型操作
//写操作
atm_Num1.store(2, memory_order::memory_order_relaxed);
//读操作
atm_Num1.load(memory_order::memory_order_relaxed);
//修改
atm_Num1.exchange(2, memory_order::memory_order_relaxed);
在代码中,笔者使用了两种原子类型:atomic和atomic_int。
atomic是一个通用的原子类型,可以用于任意可复制的数据类型;
而atomic_int是atomic的特化版本,专门用于int类型。
里面展示了几种常见的原子类型操作:
1. store函数用于将一个值存储到原子对象中,并指定了内存顺序。
2. load函数用于从原子对象中读取值,并指定了内存顺序。
3. exchange函数用于将原子对象的值替换为新值,并返回原来的值。
在这段代码中,笔者使用了memory_order_relaxed作为内存顺序参数,它表示的是不需要关注内存访问的顺序性,那么具体的memory_order里面究竟还有什么呢?
在实际使用时,会根据具体需求选择合适的内存顺序参数。
介绍:
1.atomic的特化类型
在知道了atomic是有专门用于不同类型的特化版本之后,我们可以进入源码去查看有多少的特化类型:
using atomic_bool = atomic<bool>;
using atomic_char = atomic<char>;
using atomic_schar = atomic<signed char>;
using atomic_uchar = atomic<unsigned char>;
using atomic_short = atomic<short>;
using atomic_ushort = atomic<unsigned short>;
using atomic_int = atomic<int>;
using atomic_uint = atomic<unsigned int>;
using atomic_long = atomic<long>;
using atomic_ulong = atomic<unsigned long>;
using atomic_llong = atomic<long long>;
using atomic_ullong = atomic<unsigned long long>;
2.memory_order内部枚举
enum memory_order {
memory_order_relaxed,
memory_order_consume,
memory_order_acquire,
memory_order_release,
memory_order_acq_rel,
memory_order_seq_cst
};
1. memory_order_relaxed:
松散顺序,允许编译器和硬件对原子操作进行优化,不保证任何顺序关系和可见性。
2. memory_order_consume:
消费顺序,保证当前线程对该原子操作的读取不会比其他线程加载该原子操作的某个依赖值更早。
3. memory_order_acquire:
获取顺序,保证当前线程对该原子操作的读取不会比其他线程加载该原子操作的后续读取操作更早。
4. memory_order_release:
释放顺序,保证当前线程对该原子操作的写操作不会比其他线程存储该原子操作的前续写操作更早。
5. memory_order_acq_rel:
获取-释放顺序,同时具有memory_order_acquire和memory_order_release的特性。
6. memory_order_seq_cst:
顺序一致性,最严格的内存顺序,保证所有线程对该原子操作的读写操作都按照全局顺序进行。
为什么会有这些顺序呢:
比如我写一个函数:
atomic<int> a = 0;
atomic<int> b = 0;
void test()
{
int t = 1;
a = t;
b = 1;
}
多线程中的原子操作可以保证每个操作的原子性,但是无法保证多个原子操作的顺序。
所以在这种情况下,`b = 1`有可能会比其他代码快执行完毕。
强顺序
代码顺序和执行顺序是一样的
弱顺序
代码的执行顺序会处理器被适当调整,并不会影响程序的正常执行
小案例:
其实atomic里面的函数是有重载的,所以其实也可以不写相关的 memory_order::枚举。
直接填写数值即可,比如:a.store(t);即可。
#include<iostream>
#include<thread>
#include <atomic>
using namespace std;
atomic<int> a = 0;
atomic<int> b = 0;
void setValue()
{
int t = 1;
a.store(t,memory_order::memory_order_release);//相当于a=t
b.store(2, memory_order::memory_order_relaxed);
}
void print()
{
//cout << a << " : " << b << endl;//这样子输出不一定是1:2
cout << a.load(memory_order::memory_order_relaxed)<<" " << b.load(memory_order::memory_order_relaxed) << endl;//这样子输出也不一定是1:2
b.exchange(9999, memory_order::memory_order_relaxed);
}
int main()
{
thread t1(setValue);
thread t2(print);
t1.join();
t2.join();
cout << b.load(memory_order::memory_order_relaxed);
return 0;
}