什么是原子操作
原子操作(Atomic Operation)是指不可中断的操作,即在多线程环境下,当一个线程在执行原子操作时,不会被其他线程的调度和中断所影响。这种操作在多线程编程中尤为重要,因为它能保证操作的原子性,从而避免数据竞争和不一致。
原子操作的特性
- 原子性:操作不可分割,即不可中断。
- 可见性:操作完成后,其他线程能立即看到结果。
- 有序性:编译器和处理器不会重排序原子操作。
c++ 原子操作的支持
在C++中,原子操作可以通过<atomic>
库来实现。<atomic>
库提供了一组模板类,如std::atomic<T>
,其中T
可以是整型、指针类型等。这些模板类提供了一系列成员函数,如load()
, store()
, exchange()
, compare_exchange_weak()
, compare_exchange_strong()
等,以实现原子操作。
示例使用不适用原子操作和使用原子操作比对
示例我们创建二十个线程,同时分别对同一个对象的成员变量(m_aa(初值为0))做10000自增运算,按照正常运算,所有线程运行完成后,对象的成员变量值应该是20*10000 = 200000.
不使用原子操作
#include<iostream>
#include<thread>
#include<vector>
#include<atomic>
class ThreadTsetAtomic
{
private:
int m_aa;
// std::atomic<int> m_aa;
public:
void add()
{
int b=10000;
while(b--)
{
m_aa++;
}
}
ThreadTsetAtomic(const int a):m_aa(a)
{}
void showValue()
{
std::cout<<m_aa<<std::endl;
}
};
void testFun(ThreadTsetAtomic* sub)
{
sub->add();
}
int main()
{
ThreadTsetAtomic test1(0);
std::vector<std::thread> threadVec;
for(int i=0;i<20;i++)
{
std::thread test(testFun,&test1);
threadVec.push_back(std::move(test));
// threadVec.emplace_back(testFun,test1);
}
for (auto& t : threadVec)
{
t.join();
}
test1.showValue();
return 0;
}
编译运行
可以看到这里运行的结果是30799 和我们实际预期的200000值相差很大
使用原子操作
示例
#include<iostream>
#include<thread>
#include<vector>
#include<atomic>
class ThreadTsetAtomic
{
private:
// int m_aa;
std::atomic<int> m_aa; //原子整型变量m_aa
public:
void add()
{
int b=10000;
while(b--)
{
m_aa++;
}
}
ThreadTsetAtomic(const int a):m_aa(a)
{}
void showValue()
{
std::cout<<m_aa<<std::endl;
}
};
void testFun(ThreadTsetAtomic* sub)
{
sub->add();
}
int main()
{
ThreadTsetAtomic test1(0);
std::vector<std::thread> threadVec;
for(int i=0;i<20;i++)
{
std::thread test(testFun,&test1);
threadVec.push_back(std::move(test));
// threadVec.emplace_back(testFun,test1);
}
for (auto& t : threadVec)
{
t.join();
}
test1.showValue();
return 0;
}
编译运行
运行结果为200000和我们预期的结果一致
为什么示例一中不使用原子操作运行的结果和我们预期的值相差这么大呢?
示例
#include<iostream>
int main()
{
int a =0;
a++;
return 0;
}
编译运行
我们查看想加的汇编代码
0x0000555555554745 <+11>: addl $0x1,-0x4(%rbp) 这一句的实现
-
地址计算: 首先,CPU 将寄存器
%rbp
的值与偏移量-0x4
相加,得到内存地址-0x4(%rbp)
。 -
内存访问: CPU 访问计算得到的内存地址,读取其中的值。这个值是存储在该内存位置中的数据,可能是一个整数值。
-
加法操作: CPU 将从内存中读取的值与立即数
0x1
相加,得到一个新的结果。 -
写回内存: 最后,CPU 将加法结果写回到内存地址
-0x4(%rbp)
所指向的内存位置中。这会覆盖原来的值,更新为新的结果。
也就是这个过程并非原子操作,因为涉及多个步骤,其中可能会发生中断、上下文切换或其他并发操作。要确保该操作是原子的,可能需要使用硬件支持的原子操作指令或锁来确保在多线程环境下的原子性。
结合上面示例不加原子操作分析,也就是多线程运行时,整形变量的自加不是原子操作的,当一个线程的操作还未完成可能这时候cpu就进行了线程切换,从而导致计数值不准。
补充
std::atomic
API
-
加载(Load)和存储(Store):
T load(std::memory_order order = std::memory_order_seq_cst) const noexcept;
void store(T desired, std::memory_order order = std::memory_order_seq_cst) noexcept;
这对函数允许加载和存储原子变量的值。load
函数会返回当前原子变量的值,而 store
函数会将给定的值存储到原子变量中。
#include <atomic>
#include <iostream>
std::atomic<int> value(0);
int main() {
value.store(10); // 存储值为 10 到原子变量
int loaded_value = value.load(); // 加载原子变量的值
std::cout << "Loaded value: " << loaded_value << std::endl;
return 0;
}
-
交换(Exchange):
T exchange(T desired, std::memory_order order = std::memory_order_seq_cst) noexcept;
这个函数会原子地将给定的值存储到原子变量中,并返回原子变量之前的值。
#include <atomic>
#include <iostream>
std::atomic<int> value(0);
int main() {
int previous_value = value.exchange(10); // 原子地将值 10 存储到原子变量,并返回之前的值
std::cout << "Previous value: " << previous_value << std::endl;
return 0;
}
-
比较并交换(Compare and Exchange):
bool compare_exchange_weak(T& expected, T desired, std::memory_order success, std::memory_order failure) noexcept;
bool compare_exchange_strong(T& expected, T desired, std::memory_order success, std::memory_order failure) noexcept;
这对函数尝试原子地将原子变量的值与期望值进行比较,如果相等,则将新值存储到原子变量中,并返回 true;否则,返回 false。compare_exchange_weak
和 compare_exchange_strong
的区别在于当原子变量的值与期望值不同时,compare_exchange_weak
可能会失败,而 compare_exchange_strong
会循环直到操作成功。
#include <atomic>
#include <iostream>
std::atomic<int> value(0);
int main() {
int expected = 0;
int desired = 10;
bool success = value.compare_exchange_weak(expected, desired); // 尝试将值从 0 替换为 10
if (success) {
std::cout << "Exchange successful" << std::endl;
} else {
std::cout << "Exchange failed" << std::endl;
}
return 0;
}