原子操作时多线程程序中"最小且不可并行化"的操作。通常对一个共享资源的操作是原子操作的话,意味着多个线程访问该资源时,有且仅有唯一一个线程在对这个资源进行操作。
通常情况下,原子操作通过"互斥"(mutual exclusive)的访问来保证。实现互斥通常需要平台相关的特殊指令,在c++11标准之前,这常常意味着需要在c/c++代码中嵌入内联汇编代码。
来看一个具体的例子(例1):
#include
#include
using namespace std;
static long long total=0;
pthread_mutex_t m=PTHREAD_MUTEX_INITIALIZER;
void* func(void*)
{
long long i;
for(i=0;i<100000000;i++)
{
pthread_mutex_lock(&m);
total+=i;
pthread_mutex_unlock(&m);
}
}
int main()
{
pthread_t thread1,thread2;
if(pthread_create(&thread1,NULL,&func,NULL))
{
throw;
}
if(pthread_create(&thread2,NULL,&func,NULL))
{
throw;
}
pthread_join(thread1,NULL);
pthread_join(thread2,NULL);
cout<
代码中为了防止数据竞争,我们使用了pthread_mutex_t的互斥锁保证两个线程可以正确的访问total。
这种方式主要存在两个问题:
1)加锁和解锁会消耗系统资源;
2)代码移植性较差,像我们实际开发过程中,一套代码中兼容windows和linux等的地方比比皆是,这其实是程序员做了"妥协"。
c++11对数据进行了更加良好的抽象,引入“原子数据类型”(atomic),以达到对开发者掩盖互斥锁、临界区的目的。
来看一段代码(例2):
#include
#include
#include
using namespace std;
std::atomic_llong total{ 0 };//原子数据类型
void func(int)
{
for (long long i = 0; i<100000000; ++i)
{
total += i;
}
}
int main()
{
thread t1(func, 0);
thread t2(func, 0);
t1.join();
t2.join();
cout<
例2的执行结果与例1相同。主要做了如下改进:通过将total定义为原子类型std::atomic_llong,使得程序不需要显示的调用API来加锁、解锁,对于代码来说,即容易又简洁。
c++11对常见的原子操作进行了抽象,定义了统一的接口,并根据编译选项/环境产生平台相关的实现。新标准将原子操作定义为atomic模板类的成员函数,包括了大多数典型的操作——读、写、比较、交换等。
template struct atomic;
原子类型名称 | 对应的内置类型名称 |
atomic_bool | bool |
atomic_char | char |
atomic_schar | signed char |
atomic_uchar | unsigned char |
atomic_int | int |
atomic_uint | unsigned int |
atomic_short | short |
atomic_ushort | unsigned short |
atomic_long | long |
atomic_ulong | unsigned long |
atomic_llong | long long |
atomic_ullong | unsigned long long |
atomic_char16_t | char16_t |
atomic_char32_t | char32_t |
atomic_wchar_t | wchar_t |
可参考:
http://cplusplus.com/reference/atomic/atomic/?kw=atomic
UDT要满足以下5个条件,才可作为模板参数去实例化atomic模板(例3):
#include
#include
using namespace std;
struct MY_UDT
{
//TODO:data member here
};
int main()
{
auto ret = std::is_trivially_copyable::value;
ret = std::is_copy_constructible::value;
ret = std::is_move_constructible::value;
ret = std::is_copy_assignable::value;
ret = std::is_move_assignable::value;
return 0;
}
一个具体的例子如下(例4):
#include
#include
#include
using namespace std;
atomic m{0};
atomic n{0};
int main()
{
int tmp = 1;
m = tmp;
n = 3;
return 0;
}
伪汇编代码如下:
1: Loadi reg3, 1; #将1放入寄存器reg3
2: Move reg4, reg3; #将reg3的数据放入reg4
3: Store reg4, m; #将寄存器reg4中的数据存入内存地址m
4: Loadi reg5, 2; #将立即数2放入寄存器reg5
5: Store reg5, n; #将寄存器5中的数据存入内存地址n
强顺序:指令执行顺序为1->2->3->4->5
弱顺序:执行可能的执行顺序为1->4->2->5->3(指的是执行顺序存在一定的不确定性)
优势:提高指令执行的性能;
劣势:多线程下,可能会造成程序运行错误;
一个典型的例子(例5):
Singleton* Singleton::getInstance()
{
if (m_instance == nullptr)
{
std::mutex mtx; //函数结束时锁资源释放
m_instance = new(std::nothrow) Singleton();
if (m_instance == nullptr)
{
return nullptr;
}
}
return m_instance;
}
上例为单例模式中经典的double check双检查锁的实现方式,结合上述分析,发现可能会存在如下问题:
我们默认或假定的构造顺序一般如下:
实际reorder后的顺序可能为:
导致的问题:
当一个线程执行到第二步时,假如此时有另外一个线程访问,会默认m_instance不为空返回,此时实际还未调用构造器,进而导致不可预知的问题。
typedef enum memory_order {
memory_order_relaxed,
memory_order_consume,
memory_order_acquire,
memory_order_release,
memory_order_acq_rel,
memory_order_seq_cst
} memory_order;
定义如下:
枚举值 | 定义规则 |
memory_order_relaxed | 不对执行的顺序作任何保证 |
memory_order_acquire | 本线程中,所有后续的读操作必须在本条原子操作完成后执行 |
memory_order_release | 本线程中,所有之前的写操作完成后才能执行本条原子操作 |
memory_order_acq_rel | 同时包含memory_order_acquire和memory_order_release |
memory_order_consume | 在本线程中,所有后续的有关原子类型的操作,必须在本条原子操作完成之后执行 |
memory_order_seq_cst | 全部存取都按顺序执行 |
std::atomic Singleton::m_instance; //原子对象
std::mutex Singleton::m_mutex;
Singleton* Singleton::getInstance()
{
Singleton* s = m_instance.load(std::memory_order_relaxed); //屏蔽编译器的reorder
std::_Atomic_thread_fence(std::memory_order_acquire); //本线程中,所有后续的读操作必须在本条原子操作完成后执行
if (s == nullptr)
{
std::lock_guard lock(m_mutex);
s = m_instance.load(std::memory_order_relaxed); //取变量
if (s == nullptr)
{
s = new Singleton; //保证不出现reorder
std::_Atomic_thread_fence(std::memory_order_release); //释放内存fence
m_instance.store(s, std::memory_order_relaxed);
}
}
return s;
}
定义如下:
void atomic::store( T desired, std::memory_order order = std::memory_order_seq_cst ) volatile noexcept;
memory_order参数的默认值是std::memory_order_seq_cst。实际上,atomic类型的其他原子操作接口都有memory_order这个参数,而且默认值都是std::memory_order_seq_cst。
部分参考:
https://zhuanlan.zhihu.com/p/107092432
《深入理解c++11:c++11新特性解析与应用》