CAS定义
比较并交换(compare and swap,CAS),是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致问题。该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新值。
bool CAS( int * pAddr, int nExpected, int nNew )
atomically
{
if ( *pAddr == nExpected )
{
*pAddr = nNew ;
return true ;
}
else
return false ;
// 返回bool告知原子性交换是否成功
}
为什么需要无锁队列
锁引起的问题:
(1)cache损坏 / 失效
(2)在同步机制上的争抢队列
(3)动态内存分配
有锁导致线程切换引发cache损坏
在保存和恢复上下午的过程中还隐藏了额外的开销:Cache中的数据会失效,因为它缓存的是将被换出的任务数据,这些数据对于新换进的任务是没有用的。
CPU的运行速度比主存快很多,所以大量的处理器时间被浪费在处理器与主存的数据传输上,因此,在处理器和主存之间引入Cache。Cache是一种速度更快但容量更小的内存(也更加昂贵),当处理器要访问主存中的数据时,这些数据首先被拷贝到Cache
中,因为这些数据在不久的将来可能又会被处理器访问。Cache misses对性能有非常大的影响,因为处理器访问Cache中的数据将比直接访问主存快得多。线程被频繁抢占产生的Cache损坏将导致应用程序性能下降。
在同步机制上的争抢队列
阻塞导致系统暂停当前的任务或使其进入睡眠状态(等待,不占用CPU资源),直到资源(例如锁机制)可用,被阻塞的任务才能解除阻塞状态(唤醒)。在一个负载较重的应用程序中使用这样的阻塞队列来在线程之间传递消息会导致严重的争用问题。也就是说,任务将大量的时间(睡眠,等待,唤醒)浪费在获得保护队列数据的互斥锁,而不是处理队列中的数据上。
非阻塞机制大展伸手的机会到了。任务之间不争抢任何资源,在队列中预定一个位置,然后在这个位置上插入或提取数据。这中机制使用了一种被称之为CAS(比较和交换)的特殊操作,这个特殊操作是一种特殊的指令,它可以原子的完成以下操作:它需要3个操作数m,A,B,其中m是一个内存地址,操作将m指向的内存中的内容与A比较,如果相等则将B写入到m指向的内存中并返回true,如果不相等则直接返回false。
动态内存分配
在多线程中,需要仔细考虑动态内存分配。当一个任务从堆中分配内存时,标准的内存分配机制会阻塞所有与这个任务共享地址空间的其他任务(进程中的其他线程)。这样做的原因是让处理更简单,且其工作很好。两个线程不会被分配到一块相同的地址的内存,因为它们没有办法同时执行分配请求。显然线程频繁分配内存会导致应用程序性能下降(必须注意,向标准队列或map插入数据的时候都会导致堆上的动态内存分配)。
无锁队列的实现
无锁队列由两个类构成:ypipe_t和yqueue_t。zeromq,最快的消息队列。
(1)适用于一读一写的应用场景,比如一个epool+线程池中每个线程绑定一个唯一的队列。
(2)通过chunk模式批量分配结点,减少因为动态内存分配线程之间的互斥。写线程申请内存、读线程释放内存也会导致动态内存的互斥。
批量分配结点数量没有固定的,需要根据业务场景进行调节;一般设置比较大没有什么问题,设置小了相对容易会产生问题而已。
(3)通过spare_chunk的作用(消息队列水位局部性原理,一般消息数量在一个位置上下波动)来降低chunk的频繁分配和释放。
消息数量在一个位置上下波动时,已经读取元素的chunk不立即释放,而是放在spare_chunk存储,当下一次需要分配chunk时,检查spare_chunk(如果有保存chunk就复用,没有再执行分配)。
(4)通过预写机制,批量更新写入位置,减少CAS的调用(同时读写消息队列对于CAS是有竞争的)。
(5)巧妙的唤醒机制。读端没有数据可读时可以进行wait状态;写端在写入数据时可以根据返回值获知写入数据前消息队列是否为空,如果写入之前为空则可以唤醒读端。注意wait是业务层的,无锁消息队列本身没有wait / notify机制。
ypipe_t无锁队列的使用
yqueue.write(count,false),写入元素为count,false代表这次已经写完数据,true表示还没写完数据。
yquue.flush()使读端能看到更新后的数据;返回false表示刷新之前队列为空,可notify唤醒读端;返回true说明队列本身有数据。flush才真正调用CAS。
yqueue.read(&value)读取元素,返回true表示读到元素;返回false表示消息队列为空,可以让出CPU或者进入wait状态等待写端唤醒。
示例1:
// ...
static int s_queue_item_num = 2000000; // 每个线程插入的元素个数
ypipe_t<int, 100> yqueue;
void *yqueue_producer_thread(void *argv)
{
int count=0;
for(int i=0;i<s_queue_item_num;)
{
yqueue.write(count,false);// write
count=lxx_atomic_add(&s_count_push,1);//线程安全,原子操作
i++;
yqueue.flush();// 刷新
}
return NULL;
}
// ...
示例2:
void *yqueue_producer_thread_batch(void *argv)
{
int count = 0;
int item_num = s_queue_item_num / 10;
for (int i = 0; i < item_num;)
{
yqueue.write(count, true); // 写true
count = lxx_atomic_add(&s_count_push, 1);
yqueue.write(count, true);
count = lxx_atomic_add(&s_count_push, 1);
yqueue.write(count, true);
count = lxx_atomic_add(&s_count_push, 1);
yqueue.write(count, true);
count = lxx_atomic_add(&s_count_push, 1);
yqueue.write(count, true);
count = lxx_atomic_add(&s_count_push, 1);
yqueue.write(count, true);
count = lxx_atomic_add(&s_count_push, 1);
yqueue.write(count, true);
count = lxx_atomic_add(&s_count_push, 1);
yqueue.write(count, true);
count = lxx_atomic_add(&s_count_push, 1);
yqueue.write(count, true);
count = lxx_atomic_add(&s_count_push, 1);
yqueue.write(count, false); //最后一个元素 写false
count = lxx_atomic_add(&s_count_push, 1);
i++;
yqueue.flush();//刷新
}
return NULL;
}
示例3:
// ...
static int s_queue_item_num = 2000000; // 每个线程插入的元素个数
std::mutex ypipe_mutex_;
std::condition_variable ypipe_cond_;
ypipe_t<int, 100> yqueue;
void *yqueue_producer_thread(void *argv)
{
int count=0;
for(int i=0;i<s_queue_item_num;)
{
yqueue.write(count,false);// write
count=lxx_atomic_add(&s_count_push,1);//线程安全,原子操作
i++;
if(!yqueue.flush())// 返回false,说明读端没有读到数据
{
std::unique_lock<std::mutex> lock(ypipe_mutex);
// 注意,业务层自己实现notify,yqueue本身没有notyfy机制的
ypipe_cond_.notify_one();
}
}
std::unique_lock<std::mutex> lock(ypipe_mutex);
ypipe_cond_.notify_one();
return NULL;
}
// ...
源码分析原子操作函数
set函数,把私有成员ptr指针设置成参数ptr_的值,不是一个原子操作,需要使用者确保执行set过程没有其他线程使用ptr的值。
// This class encapsulates several atomic operations on pointers.
template <typename T> class atomic_ptr_t
{
public:
inline void set (T *ptr_); //非原子操作
inline T *xchg (T *val_); //原子操作,设置一个新的值,然后返回旧的值
inline T *cas (T *cmp_, T *val_);//原子操作
private:
volatile T *ptr;
}
源码分析yqueue_t
yqueue_t是比ypipe_t更底层的类。用于消息队列结点元素存储;不涉及CAS。
类接口和变量
#ifndef __ZMQ_YQUEUE_HPP_INCLUDED__
#define __ZMQ_YQUEUE_HPP_INCLUDED__
#include <stdlib.h>
#include <stddef.h>
// #include "err.hpp"
#include "atomic_ptr.hpp"
// 即是yqueue_t一个结点可以装载N个T类型的元素, yqueue_t的一个结点是一个数组
template <typename T, int N>
class yqueue_t
{
public:
// 创建队列.
inline yqueue_t();
// 销毁队列.
inline ~yqueue_t();
// 返回队列头部元素的引用,调用者可以通过该引用更新元素,结合pop实现出队列操作。
inline T &front(); // 返回的是引用,是个左值,调用者可以通过其修改容器的值
// 返回队列尾部元素的引用,调用者可以通过该引用更新元素,结合push实现插入操作。
// 如果队列为空,该函数是不允许被调用。
inline T &back(); // 返回的是引用,是个左值,调用者可以通过其修改容器的值
// Adds an element to the back end of the queue.
inline void push();
// 必须要保证队列不为空,参考ypipe_t的uwrite
inline void unpush();
// Removes an element from the front end of the queue.
inline void pop();
private:
// Individual memory chunk to hold N elements.
// 链表结点称之为chunk_t
struct chunk_t
{
T values[N]; //每个chunk_t可以容纳N个T类型的元素,以后就以一个chunk_t为单位申请内存
chunk_t *prev;
chunk_t *next;
};
// Back position may point to invalid memory if the queue is empty,
// while begin & end positions are always valid. Begin position is
// accessed exclusively be queue reader (front/pop), while back and
// end positions are accessed exclusively by queue writer (back/push).
chunk_t *begin_chunk; // 链表头结点
int begin_pos; // 起始点
chunk_t *back_chunk; // 队列中最后一个元素所在的链表结点
int back_pos; // 尾部
chunk_t *end_chunk; // 拿来扩容的,总是指向链表的最后一个结点
int end_pos;
// People are likely to produce and consume at similar rates. In
// this scenario holding onto the mo