Q:CAS的实现
A:gcc提供了两个函数
bool __sync_bool_compare_and_swap (type *ptr, type oldval, type newval, ...)
//type 的类型有限制 只能是 1,2,4,8字节的整形 或者是指针类型 type __sync_val_compare_and_swap (type *ptr, type oldval, type newval, ...)
这两个函数提供原子的比较和交换,如果*ptr == oldval,就将newval写入*ptr,
第一个函数在相等并写入的情况下返回true,这个函数比第二个好在,返回bool值可以知道有没有更新成功.
第二个函数在返回操作之前的值。
第二个函数可以用c语言描述成:
type __sync_val_compare_and_swap (type *ptr, type oldval, type newval, ...) { type cur = *ptr; if (cur == oldval) { *ptr = newval; } return cur;// 返回操作之前的值 }
官方文档里有一段:GCC will allow any integral scalar or pointer type that is 1, 2, 4 or 8 bytes in length.
意思是GCC允许任何长度为1, 2, 4或8字节的整型和指针
不然就会有下面这种错误
Q:简单介绍一下无锁队列
A:
之所以有无锁队列,是因为在多线程环境下有以下情景,
对同一链队列进行入队操作时 一号线程正在将 新的队列节点A 挂载到 队尾节点的next上 可是还没来的及更新队尾节点 但同一时刻二号线程也在进行入队操作 将 新的队列节点B 也挂载到了 没更新的队尾节点的next上 那么一号线程挂载的节点A就丢失了
为了解决多线程环境下的这类问题
我们第一时间肯定想到了加上互斥锁 控制同一时刻只能有一个线程可以对队列进行写操作
但是加锁的操作太消耗系统资源了 很繁重
因为对临界区的操作只有一步 就是对队列的尾节点进行更新
只要让这一步进行的是原子操作就可以了
所以使用到了CAS操作 也就是实现无锁队列
在博客的最后有无锁队列的源代码
Q: 操作系统级别是如何实现的
A: X86中有一个CMPXCHG的汇编指令
Q: CAS指令有什么缺点
A:
1.存在ABA问题因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
2.循环时间长开销大自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
3. 只能保证一个共享变量的原子操作对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。
与CAS相关的一些原子操作
gcc从4.1.2提供了__sync_*系列的built-in函数,用于提供加减和逻辑运算的原子操作。
其声明如下:
原子操作的后置加加type __sync_fetch_and_add (type *ptr, type value, ...)
原子操作的前置加加type __sync_add_and_fetch (type *ptr, type value, ...)
其他类比
type __sync_fetch_and_sub (type *ptr, type value, ...)
type __sync_fetch_and_or (type *ptr, type value, ...)
type __sync_fetch_and_and (type *ptr, type value, ...)
type __sync_fetch_and_xor (type *ptr, type value, ...)
type __sync_fetch_and_nand (type *ptr, type value, ...)
type __sync_sub_and_fetch (type *ptr, type value, ...)
type __sync_or_and_fetch (type *ptr, type value, ...)
type __sync_and_and_fetch (type *ptr, type value, ...)
type __sync_xor_and_fetch (type *ptr, type value, ...)
type __sync_nand_and_fetch (type *ptr, type value, ...)
这两组函数的区别在于第一组返回更新前的值,第二组返回更新后的值。
无锁队列源代码
为了有一个对比
写了一份thread_queue.c是用锁对临界区进行控制访问的,也就是有锁队列
另一份是lock_free_queue.c是用CAS确保对临界区的操作是原子操作,也就是无锁队列
依赖关系
lock_free_queue.c -> queue.h -> queue.c
thread_queue.c -> queue.h -> queue.c
gcc lock_free_queue.c queue.c -lpthread -o lock_free_queue
gcc thread_queue.c queue.c -lpthread -o thread_queue
1 #ifndef QUEUE_H_ 2 #define QUEUE_H_ 3 4 #include <stdio.h> 5 #include <stdlib.h> 6 7 /* 8 普通的 9 链式队列 10 */ 11 typedef struct QNode 12 { 13 int data; 14 struct QNode *next; 15 }QNode, *QueuePtr; 16 17 typedef struct LinkQueue 18 { 19 QueuePtr front; 20 QueuePtr rear; 21 }LinkQueue; 22 23 24 void init_Queue(LinkQueue *q);//初始化队列 25 void push_Queue(LinkQueue *q, int e);//队尾入队 26 int pop_Queue(LinkQueue *q, int *e);//队头出队 27 int is_Empty(LinkQueue *q); 28 void show(LinkQueue *q); 29 30 31 #endif /* QUEUE_H_ */
1 #include "queue.h" 2 3 /* 4 初始化 5 为队列构建一个头结点 6 让front和rear都指向这个头结点 7 */ 8 void init_Queue(LinkQueue *q) 9 { 10 q->front = q->rear = (QNode *)malloc(sizeof(QNode)); 11 q->front->next = NULL; 12 } 13 14 /* 15 普通的入队操作 16 */ 17 void push_Queue(LinkQueue *q, int e) 18 { 19 QueuePtr newNode = (QueuePtr)malloc(sizeof(QNode)); 20 newNode->data = e; 21 newNode->next = NULL; 22 q->rear->next = newNode; 23 q->rear = newNode; 24 } 25 26 /* 27 cas的入队操作 28 和普通的入队操作一样 29 新建节点后 30 要将新节点挂在队尾时需要进行cas操作 31 */ 32 void cas_push(LinkQueue *q, int e) 33 { 34 QueuePtr newNode = (QueuePtr)malloc(sizeof(QNode)); 35 newNode->data = e; 36 newNode->next = NULL; 37 38 QueuePtr tmp; 39 do 40 { 41 tmp = q->rear; 42 }while (!__sync_bool_compare_and_swap((&(tmp->next)), NULL, (newNode)); 43 44 q->rear = newNode; 45 } 46 47 /* 48 以前的判空是 q->front == q->rear 49 但是这样子会增加出队的操作 当出的是最后一个元素时, q->rear需要指向 q->front 50 我把这一步省了 暂时没有发现有什么副作用 51 所以我改成了 q->front->next == NULL 52 */ 53 int is_Empty(LinkQueue *q) 54 { 55 if (q->front->next == NULL) 56 { 57 return(1); 58 } 59 return(0); 60 } 61 62 /* 63 普通的出队操作 64 如果队空 返回0 也就是false 65 e作为接受元素的缓冲 66 */ 67 int pop_Queue(LinkQueue *q, int *e) 68 { 69 if (is_Empty(q)) 70 { 71 return(0); 72 } 73 QueuePtr tmp; 74 tmp = q->front->next; 75 q->front->next = tmp->next; 76 77 *e = tmp->data; 78 free(tmp); 79 return(1); 80 } 81 82 /* 83 cas的出队操作 84 每一次都要判断这个队列是不是空 85 然后执行cas的出队操作: 86 (1)tmp = q->front 把旧的队头存起来 87 (2)执行原子操作:看 旧的队头 是否等于 现在的队头 tmp == *(&(q->front->next)) 如果相等执行 *(&(q->front->next)) = tmp->next 返回true 88 否则,即执行这一步原子操作的时候,别的线程修改了队列,导致队尾指向改变了,返回false ,while(!false)回到第一步重新执行 89 */ 90 int cas_pop(LinkQueue *q, int *e) 91 { 92 QueuePtr tmp; 93 do { 94 if (is_Empty(q)) 95 { 96 return(0); 97 } 98 tmp = q->front; 99 } while (!__sync_bool_compare_and_swap(&(q->front->next), tmp, tmp->next->next); 100 101 *e = tmp->data; 102 free(tmp); 103 return(1); 104 } 105 106 /* 107 遍历队列 打印里面的元素 为了求证队列里面的元素 108 */ 109 void show(LinkQueue *q) 110 { 111 printf("void show(LinkQueue *q)\n"); 112 QueuePtr tmp = q->front->next; 113 while (tmp) 114 { 115 printf("%d ", tmp->data); 116 tmp = tmp->next; 117 } 118 printf("\n"); 119 }
#include "queue.h" #include <pthread.h> #include <unistd.h> #include <assert.h> #define THREAD_NUMBER 4//开启的线程数,电脑是4核,所以用4 void *thread_push(void *arg); void *thread_pop(void *arg); /* 初始化空队列 为了模拟线程对资源的抢占 开启4个线程 每个线程push 20个元素 0~19 等待4个线程结束 打印队列元素 验证push 开启四个线程 每个线程都对队列进行 pop操作 */ int main() { LinkQueue que; init_Queue(&que); int i; /* 创造四个新线程 每个线程都执行 thread_push(&que) */ pthread_t threadArr[THREAD_NUMBER]; for (i = 0; i < THREAD_NUMBER; ++i) { pthread_create(&threadArr[i], NULL, thread_push, (void *)&que); } /* 等待四个线程都执行完 要不然主线程一下子就跑完了 程序就结束了 还有就是 为了show函数 可以验证元素是不是都push进去了 */ for (i = 0; i < THREAD_NUMBER; ++i) { pthread_join(threadArr[i], NULL); } show(&que); /* 创造四个新线程 每个线程都执行 thread_pop(&que) */ for (i = 0; i < THREAD_NUMBER; ++i) { pthread_create(&threadArr[i], NULL, thread_pop, (void *)&que); } for (i = 0; i < THREAD_NUMBER; ++i) { pthread_join(threadArr[i], NULL); } exit(EXIT_SUCCESS); } void *thread_push(void *arg) { printf("start push\n"); LinkQueue * quePtr = (LinkQueue *)arg; int i; for (i = 0; i < 20; ++i) { cas_push(quePtr, i); } printf("finish push\n"); pthread_exit(NULL); } void *thread_pop(void *arg) { printf("start pop\n"); LinkQueue * quePtr = (LinkQueue *)arg; int tmp; int res; while (1) { res = cas_pop(quePtr, &tmp); if (!res) { break; } printf("%d ", tmp); } printf("finish pop\n"); pthread_exit(NULL); }
1 #include "queue.h" 2 #include <pthread.h> 3 #include <unistd.h> 4 #include <semaphore.h> 5 #include <assert.h> 6 7 #define THREAD_NUMBER 4 8 9 sem_t queue_sem;//同步锁 10 11 void *thread_push(void *arg); 12 void *thread_pop(void *arg); 13 14 int main() 15 { 16 LinkQueue que; 17 init_Queue(&que); 18 19 /*初始化二进制信号量 初始值为1 代表每一次只有1个线程可以访问 20 本来更加应该用互斥量 比较贴合情景 但是不太熟 就用了信号量 21 */ 22 int res = sem_init(&queue_sem, 0, 1); 23 assert(res != -1); 24 25 int i; 26 pthread_t threadArr[THREAD_NUMBER]; 27 for (i = 0; i < THREAD_NUMBER; ++i) 28 { 29 pthread_create(&threadArr[i], NULL, thread_push, (void *)&que); 30 } 31 32 for (i = 0; i < THREAD_NUMBER; ++i) 33 { 34 pthread_join(threadArr[i], NULL); 35 } 36 37 show(&que); 38 39 for (i = 0; i < THREAD_NUMBER; ++i) 40 { 41 pthread_create(&threadArr[i], NULL, thread_pop, (void *)&que); 42 } 43 44 for (i = 0; i < THREAD_NUMBER; ++i) 45 { 46 pthread_join(threadArr[i], NULL); 47 } 48 49 sem_destroy(&queue_sem); 50 51 exit(EXIT_SUCCESS); 52 } 53 54 void *thread_push(void *arg) 55 { 56 printf("start push\n"); 57 LinkQueue * quePtr = (LinkQueue *)arg; 58 int i; 59 for (i = 0; i < 20; ++i) 60 { 61 sem_wait(&queue_sem); 62 push_Queue(quePtr, i); 63 sem_post(&queue_sem); 64 } 65 printf("finish push\n"); 66 pthread_exit(NULL); 67 } 68 69 void *thread_pop(void *arg) 70 { 71 printf("start pop\n"); 72 LinkQueue * quePtr = (LinkQueue *)arg; 73 int tmp; 74 int res; 75 while (1) 76 { 77 sem_wait(&queue_sem); 78 res = pop_Queue(quePtr, &tmp); 79 sem_post(&queue_sem); 80 if (!res) 81 { 82 break; 83 } 84 printf("%d ", tmp); 85 } 86 printf("finish pop\n"); 87 pthread_exit(NULL); 88 }
博客,参考:http://blog.csdn.net/syzcch/article/details/8075830
libcds库,参考:http://blog.jobbole.com/90810/
CAS缺点,参考:https://www.cnblogs.com/zhuawang/p/4196904.html
CAS函数,参考: http://blog.csdn.net/youfuchen/article/details/23179799
gcc Atomic Builtins官方:https://gcc.gnu.org/onlinedocs/gcc-4.1.1/gcc/Atomic-Builtins.html