基于CAS实现无锁结构

杨乾成

一、题目要求

  基于CAS(Compare and Swap)实现一个无锁结构,可考虑queue,stack,hashmap,freelist等。

  • 能够支持多个线程同时访问该结构
  • 不能有任何锁操作,且操作是线程安全的
  • 对上述的内存单元进行管理,至少malloc与free一次。

二、数据结构

  看到题目有一说一,不知道怎么下手,那就google一下先。稍微了解了一下CAS,原准备使用STL模板的队列,后来发现实现题目要求似乎得再Queue的插入和删除函数里面具体实现,于是决定自己实现吧(省事失败)。

  先简要介绍一下CAS算法,我先po一下wiki百科上的代码介绍:

bool compare_and_swap (int *accum, int *dest, int newval)
{
  if ( *accum == *dest ) {
      *dest = newval;
      return true;
  }
  return false;
}

  这段代码的思想大概就是看看*dest里面的值和*accum里面的值是不是一样,如果一样,则将新数据写入。个人觉得用数据库的“脏数据”概念来形容很合适:即不允许往存有脏数据的缓冲区写入数值。在数据库中,常用的方法是两段锁,但是这里不让加锁。突然想起来大二上学期修JAVA里面讲到的同步信号量,即每当对数据结构进行操作时,就对=改变信号量的值,比如只有信号量值为0的时候才允许更改变量,修改时使用者将信号量置一,但是仔细一想,这似乎也是锁的思想。

  遇事不决,再看看,害,现成的CAS都在这里,我直接用不就完事了。回归正题,CAS思想总结如下:线程操作钱保存当前版本号,也就是记录当前数据的状态的标志;当完成操作时,比较自己持有的版本号与当前版本号是否相符,相符则允许写入,否则获取的新的版本号再来一轮。就好像是大家都听说身边有个好看的姑娘处于“单身版本”,各自回去准备准备,第一个到的获得了优先权,后来的只能记住另一个“单身”的姑娘,再回家准备展开竞争,直到自己完成任务。

三、代码实现

#include<stdio.h>
#include<pthread.h>
#include <unistd.h>
#include <assert.h>
#include<stdlib.h>
#define THREAD_NUMBER 4
//队节点
typedef struct QNode
{
    int data;
    struct QNode *next;
}QNode, *QueuePtr;
//链队结构
typedef struct LinkQueue
{
    QueuePtr front;
    QueuePtr rear;
}LinkQueue;

int is_Empty(LinkQueue *q);
void *pushThread(void *arg);
void *popThread(void *arg);
void init_Queue(LinkQueue *q);void cas_push(LinkQueue *q, int e);
void cas_push(LinkQueue *q, int e);
int cas_pop(LinkQueue *q, int *e);
void show(LinkQueue *q);

int main()
{
    LinkQueue que;
    init_Queue(&que);
    int i;

    //创建四个进程
    pthread_t threadArr[THREAD_NUMBER];    //线程ID数组
    for (i = 0; i < THREAD_NUMBER; ++i)
    {
        pthread_create(&threadArr[i], NULL, pushThread, (void *)&que);    //设置线程入口
    }
    //等待线程执行结束
    for (i = 0; i < THREAD_NUMBER; ++i)
    {
        pthread_join(threadArr[i], NULL);
    }
    show(&que);

    //创建四个进线程然后执行出队操作
    for (i = 0; i < THREAD_NUMBER; ++i)
    {
        pthread_create(&threadArr[i], NULL, popThread, (void *)&que);
    }
    for (i = 0; i < THREAD_NUMBER; ++i)
    {
        pthread_join(threadArr[i], NULL);
    }
    exit(EXIT_SUCCESS);
 }

void *pushThread(void *arg)
{
    printf("进程%ld开始创建队列:\n",pthread_self());
    LinkQueue *quePtr =(LinkQueue *)arg;
    int i;
    for(i= 0;i<5;++i)
    {       
        cas_push(quePtr, i);    
    }
    printf("\n");
    printf("完成队列创建操作!\n");
    pthread_exit(NULL);     //结束当前进程
}

void *popThread(void *arg)
{
    printf("%ld开始出队操作:\n",pthread_self());
    LinkQueue *quePtr=(LinkQueue *)arg;
    int temp;
    int res;
    while(1)
    {
        res = cas_pop(quePtr,&temp);
        if(!res)
        {
            break;    
        }
        printf("PID:%ld\tpop%d\n",pthread_self(),temp);
    }
    
}

//初始化队列
void init_Queue(LinkQueue *q)
{
    q->front=q->rear=(QNode *)malloc(sizeof(QNode));
    q->front->next=NULL;
}

//判断队列是否为空
int is_Empty(LinkQueue *q)
{
    if(q->front->next ==NULL)
    {
        return 1;
    }
    return 0;
}


void cas_push(LinkQueue *q, int e) 
{
     QueuePtr newNode = (QueuePtr)malloc(sizeof(QNode));
     newNode->data = e;
     newNode->next = NULL;
     QueuePtr tmp; 
     //不断尝试将新节点进队
     do
     {
         // sleep(1);
          tmp = q->rear;
     }while (!__sync_bool_compare_and_swap((long *)(&(tmp->next)), NULL, (long)newNode));    //判断此时队尾是否被改变
        //就是在这里控制对队列的原子化操作
     sleep(1);
     q->rear = newNode;
} 
    
int cas_pop(LinkQueue *q, int *e)
{
     QueuePtr tmp;
     do
     {
          if (is_Empty(q))
          {
               return(0);     //队列已空,结束
          } 
          tmp = q->front->next; 
     } while (!__sync_bool_compare_and_swap((long *)(&(q->front->next)), (long)tmp, (long)tmp->next));
     sleep(1);     //通过休眠一秒,模拟线程操作时间,防止过快操作导致结果不明显
     *e = tmp->data;
     free(tmp); 
     return 1;
}

void show(LinkQueue *q) 
{
     printf("Current queue:\n"); 
     QueuePtr tmp = q->front->next; 
     while (tmp) 
     {
          printf("%d ", tmp->data);
          tmp = tmp->next; 
     } 
     printf("\n");
}

 

上面就是这次的代码实现。

四、运行结果

使用指令:

gcc CAS_Structure.c -o result.out -lpthread

将源码进行编译,此处要注意后面一定要加上-lpthread,不然会提示“pthread.h”头文件报错问题。

 

 

我创建了四个进程,竞争往队尾插入数据。

 

  然后竞争删除完队列的数据,直到队列为空。

  从截图可以看出来,四个进程竞争着抢占处理器,直到完成创建和删除节点的任务。这里面要注意的是,因为实际处理就是插入节点,刚开始做的时候我没有插入sleep语句,所以最后的结果看起来像是顺序完成,所以要模拟实验,一定要注意加上sleep语句模拟进程处理的时间,防止第二个进程还没创建完,第一个进程早已完成了所有事务。

posted @ 2019-12-15 00:40  一只放姑娘的羊  阅读(649)  评论(0编辑  收藏  举报