为什么需要原子操作
最最典型的例子:x++
我们知道它有三个步骤,1.从内存中读x的值到寄存器中 2.对寄存器加1 3.再把新值写回x所处的内存地址
假设x的初始值为0,我们使用两个线程对x++,我们期待x的值为3,但实际上可能为2。
原因是有可能多个处理器同时从各自的缓存中读取变量i,分别进行加一操作,然后分别写入系统内存当中。那么想要保证读改写共享变量的操作是原子的,就必须保证CPU1读改写共享变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存。
要想安全得到正确的答案:
1.使用互斥锁,但是多个线程访问势必出现锁竞争,锁竞争性能杀手之一,不推荐
2.使用原子性操作,这就是本文的核心,原子性,就是不可再分,把三个过程作为一个整体。
首先看看gcc自带的原子操作:
// 原子自增操作,*ptr+value
type __sync_fetch_and_add (type *ptr, type value)
// 原子比较和交换(设置)操作,先比较,后交换(设置)
//若*ptr == oldval,则*ptr=newvl,并返回oldval
//返回bool类型,若*ptr == oldval,则返回为真,再设置;若比较失败,则返回false,也不会设置
type __sync_val_compare_and_swap (type *ptr, type oldval type newval)
bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval)
// 原子赋值操作,*ptr=value
type __sync_lock_test_and_set (type *ptr, type value)
//一般设置native,让系统自动检测本地cpu的类型
使用这些原子性操作,编译的时候需要加-march=cpu-type
Atomic.h代码分析
#ifndef MUDUO_BASE_ATOMIC_H
#define MUDUO_BASE_ATOMIC_H
#include <boost/noncopyable.hpp>
#include <stdint.h>
namespace muduo
{
namespace detail
{
template<typename T>
class AtomicIntegerT : boost::noncopyable
{
public:
AtomicIntegerT()
: value_(0)
{
}
// uncomment if you need copying and assignment
//
// AtomicIntegerT(const AtomicIntegerT& that)
// : value_(that.get())
// {}
//
// AtomicIntegerT& operator=(const AtomicIntegerT& that)
// {
// getAndSet(that.get());
// return *this;
// }
T get() //返回value_的值,如果value_=0,把它和0交换。
{
// in gcc >= 4.7: __atomic_load_n(&value_, __ATOMIC_SEQ_CST)
return __sync_val_compare_and_swap(&value_, 0, 0);
}
T getAndAdd(T x) //先获取没有修改的value_的值,再给value_+x
{
// in gcc >= 4.7: __atomic_fetch_add(&value_, x, __ATOMIC_SEQ_CST)
return __sync_fetch_and_add(&value_, x);
}
T addAndGet(T x) //先加,后获取,get_and_add + x相当于先加后获取
{
return getAndAdd(x) + x;
}
T incrementAndGet() //先加1后获取
{
return addAndGet(1);
}
T decrementAndGet() //先减1后获取
{
return addAndGet(-1);
}
void add(T x) //加x,无返回值
{
getAndAdd(x);
}
void increment() //自加1,无返回值
{
incrementAndGet();
}
void decrement() //自减1,无返回值
{
decrementAndGet();
}
T getAndSet(T newValue) //先get然后设置为新的值
{
// in gcc >= 4.7: __atomic_store_n(&value, newValue, __ATOMIC_SEQ_CST)
return __sync_lock_test_and_set(&value_, newValue);
}
private:
volatile T value_; //使用volatile修饰,避免编译器优化。
};
}
typedef detail::AtomicIntegerT<int32_t> AtomicInt32;
typedef detail::AtomicIntegerT<int64_t> AtomicInt64;
}
#endif // MUDUO_BASE_ATOMIC_H
原子性操作可以实现无锁队列
- 无锁队列的链表实现
EnQueue(Q, data) //进队列
{
//准备新加入的结点数据
n = new node();
n->value = data;
n->next = NULL;
//下面的p可以指向尾节点,也可以不指向尾节点
do {
p = Q->tail; //取链表尾指针的快照
} while( CAS(p->next, NULL, n) != TRUE);
//while条件注释:如果没有把结点链在尾指针上,再试
//若插入成功,Q->tail和p指针一定是相等的,置尾结点 Q->tail = n;
CAS(Q->tail, p, n);
}
说明:
(1)CAS是:原子比较与设置
if (p->next==null)
{
p->netx=n;//p如果指向的是尾节点,就将新节点添加到链表尾部
return TRUE;
}
else
return FALSE;
(2)但是你会看到,为什么我们的“置尾结点”的操作(第13行)不判断是否成功,因为:
如果有一个线程T1,它的while中的CAS如果成功的话,那么其它所有的/随后线程的CAS都会失败(因为其它线程插入的都不是尾节点),然后就会再循环;
此时,如果T1 线程还没有更新tail指针,其它的线程继续失败,因为tail->next不是NULL了;
直到T1线程更新完 tail 指针,于是其它的线程中的某个线程就可以得到新的 tail 指针,继续往下走了;
所以,只要线程能从 while 循环中退出来,意味着,它已经“独占”了,tail 指针必然可以被更新;
-
volatile的作用: 作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。
(1)简单地说就是防止编译器对代码进行优化
(2)当要求使用volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,而不是使用保存在寄存器中的备份。
即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存
对于多线程很重要!!! -
参考
无锁队列的实现
https://coolshell.cn/articles/8239.html
Type.h的研究
template<typename To, typename From>
inline To implicit_cast(From const &f)//隐式转型函数
{
return f;
}
template<typename To, typename From> // use like this: down_cast<T*>(foo);
inline To down_cast(From* f) //向下转换 // so we only accept pointers
{
//永远不会满足下面的if
if (false)
{
implicit_cast<From*, To>(0);
}
//!defined(NDEBUG)表示是调试状态
//!defined(GOOGLE_PROTOBUF_NO_RTTI)表示开启运行时的类型识别
#if !defined(NDEBUG) && !defined(GOOGLE_PROTOBUF_NO_RTTI)
assert(f == NULL || dynamic_cast<To>(f) != NULL); // RTTI: debug mode only!
#endif
return static_cast<To>(f);
}