rn_xtcxyczjh-9 并发[线程5 原子操作(volatile, inline, typeof, ({ep1;ep2;}), 内嵌汇编) 无锁数据结构]

2015.11.01-11.05
读xtcxyczjh(系统程序员成长计划)— 学习程序设计方法。
学习的代码笔记保存地址:y15m11d03-05

2015.11.02

1. 原子操作

不可中断、线程不可切换的一个或一系列操作。

(1) 原子操作原理

<询问度娘1>:网页

硬件级的原子操作:在单处理器系统(UniProcessor)中,能够在单条指令中完成的操作都可以认为是“原子操作”,因为中断只发生在指令边缘。在多处理器结构中(Symmetric Multi-Processor)就不同了,由于系统中有多个处理器独立运行,即使能在单条指令中完成的操作也有可能受到干扰。在x86平台上,CPU提供了在指令执行期间对总线加锁的手段。CPU上有一根引线#HLOCK pin连到北桥,如果在汇编语言的程序中在一条指令前面加上前缀”LOCK”,经过汇编以后的机器代码就使CPU在执行这条指令的时候把#HLOCK pin的电位拉低,持续到这条指令结束时放开,从而把总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的原子性。对于其他平台的CPU,实现各不相同,有的是通过关中断来实现原子操作(sparc),有的通过CMPXCHG系列的指令来实现原子操作(IA64)。本文主要探讨x86平台下原子操作的实现。

软件级别的原子操作:软件级别的原子操作实现依赖于硬件原子操作的支持。

<询问度娘2>:网页

32位IA-32处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。

处理器自动保证基本内存操作的原子性
首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存当中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。奔腾6和最新的处理器能自动保证单处理器对同一个缓存行里进行16/32/64位的操作是原子的,但是复杂的内存操作处理器不能自动保证其原子性,比如跨总线宽度,跨多个缓存行,跨页表的访问。但是处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。

使用总线锁保证原子性
第一个机制是通过总线锁保证原子性。如果多个处理器同时对共享变量进行读改写(i++就是经典的读改写操作)操作,那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致,举个例子:如果i=1,我们进行两次i++操作,我们期望的结果是3,但是有可能结果是2。如下图
rn_xtcxyczjh-9 并发[线程5 原子操作(volatile, inline, typeof, ({ep1;ep2;}), 内嵌汇编) 无锁数据结构]_第1张图片
(例1)
原因是有可能多个处理器同时从各自的缓存中读取变量i,分别进行加一操作,然后分别写入系统内存当中。那么想要保证读改写共享变量的操作是原子的,就必须保证CPU1读改写共享变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存。

处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。

使用缓存锁保证原子性
第二个机制是通过缓存锁定保证原子性。在同一时刻我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,最近的处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。

频繁使用的内存会缓存在处理器的L1,L2和L3高速缓存里,那么原子操作就可以直接在处理器内部缓存中进行,并不需要声明总线锁,在奔腾6和最近的处理器中可以使用“缓存锁定”的方式来实现复杂的原子性。所谓“缓存锁定”就是如果缓存在处理器缓存行中内存区域在LOCK操作期间被锁定,当它执行锁操作回写内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时会起缓存行无效,在例1中,当CPU1修改缓存行中的i时使用缓存锁定,那么CPU2就不能同时缓存了i的缓存行。

但是有两种情况下处理器不会使用缓存锁定。第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line),则处理器会调用总线锁定。第二种情况是:有些处理器不支持缓存锁定。对于Inter486和奔腾处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。

以上两个机制我们可以通过Inter处理器提供了很多LOCK前缀的指令来实现。比如位测试和修改指令BTS,BTR,BTC,交换指令XADD,CMPXCHG和其他一些操作数和逻辑指令,比如ADD(加),OR(或)等,被这些指令操作的内存区域就会加锁,导致其他处理器不能同时访问它。

2015.11.03

(2) 验证原子操作

[1] 原子操作原理
把作者实现的C语言的自加(++)原子操作代码中不懂(或熟悉)的部分告之度娘。

#define ATOMIC_SMP_LOCK "lock ; "
/*
 * Make sure gcc doesn't try to be clever and move things around
 * on us. We need to use _exactly_ the address the user gave us,
 * not some alias that contains the same information.
 */
typedef struct { volatile int counter; } atomic_t;

/**
 * atomic_inc - increment atomic variable
 * @v: pointer of type atomic_t
 * 
 * Atomically increments @v by 1.  Note that the guaranteed
 * useful range of an atomic_t is only 24 bits.
 */ 
static __inline__ void atomic_inc(atomic_t *v)
{
    __asm__ __volatile__(
        ATOMIC_SMP_LOCK "incl %0"
        :"=m" (v->counter)
        :"m" (v->counter));
}

volatile
* 告知编译器不优化跟被volatile修饰的变量相关的C语句。
* 每次在用到用volatile变量时必须都从内存中直接读取这个变量的值,而不是使用保存在寄存器里的备份。

_inline_
c中的inline,使用在函数声明处,表示程序员请求编译器在此函数的被调用处将此函数实现插入,而不是像普通函数那样生成调用代码(省函数调用开销)。[具体见标准]

_asm_ _volatile_(“assembly code”)
gcc在C语言中内嵌汇编的语法(语法要求用双引号将汇编代码引起来)。”asm“告知gcc后面的代码为内嵌汇编;”volatile“表示编译器不要优化代码,且每次都从内存中取变量。”assembly code”表示嵌入的汇编代码。

ATOMIC_SMP_LOCK “incl %0” :”=m” (v->counter) :”m” (v->counter)
嵌入的汇编代码。

  • ATOMIC_SMP_LOCK的值为”lock ; “,作为incl指令的前缀。即CPU执行此条指令的时候把总线锁住,使incl指令的执行成为一个原子操作(在gcc下,在汇编指令前加lock;前缀是提供给用户实现原子操作的机制)。
  • incl为增1的汇编指令。
  • %0以及:”=m” (v->counter) :”m” (v->counter)部分见“C(GNU)内嵌汇编”笔记的“内敛汇编扩展”部分。

[2] 在linux下验证C语言的自/减增语句的原子操作
到该像贼一样敲敲偷偷作者代码了。新建atomic.h和atomic.c文件。
atomic.h

/* atomic.h */
/* 将C语言的某些语句的执行变为原子操作 */

#ifndef ATOMIC_H
#define ATOMIC_H

#if defined(__i386__) || defined(__x86_64__)
#define ATOMIC_SMP_LOCK "lock; "

typedef struct {
    volatile int counter;
} atomic_t;

//C语言自增(++)操作的原子操作实现
static __inline__ void atomic_inc(atomic_t *v)
{
    __asm__ __volatile__ (
        ATOMIC_SMP_LOCK "incl %0"
        :"=m"   (v->counter)
        :"m"    (v->counter)
    );
}

//C预压自减(--)操作的原子操作实现
static __inline__ void atomic_dec(atomic_t *v)
{
    __asm__ __volatile__ (
        ATOMIC_SMP_LOCK "decl %0"
        :"=m"   (v->counter)
        :"m"    (v->counter)
    );
}
#endif

#endif

atomic.c

/* atomic.c */
/* 在多线程中验证原子操作 */
#include "atomic.h"
#include <pthread.h>
#include <stdio.h>

atomic_t g_count = {.counter = 0};

static void *thread_inc(void *param)
{
    int i = 0;
    for (i = 0; i < 1000000; i++) {
        atomic_inc(&g_count);
    }

    return NULL;
}

static void *thread_dec(void *param)
{
    int i = 0;
    for (i = 0; i < 1000000; i++) {
        atomic_dec(&g_count);
    }

    return NULL;
}

int main(void)
{
    pthread_t   inc_tid = 0;
    pthread_t   dec_tid = 0;

    pthread_create(&inc_tid, NULL, thread_inc, NULL);
    pthread_create(&dec_tid, NULL, thread_dec, NULL);

    pthread_join(inc_tid, NULL);
    pthread_join(dec_tid, NULL);

    printf("count=%d\n", g_count.counter);
    return 0;
}

在linux下与两个文件同目录的终端下执行以下命令。

mgn@ubuntu:~/rbooks/xtcxyczjh/y15m11d03/atomic$ gcc atomic.c -o atomic -lpthread -Wall
mgn@ubuntu:~/rbooks/xtcxyczjh/y15m11d03/atomic$ ./atomic
count=0

运行结果为0,表明两个线程在运行过程中都没有进入g_count.counter的临界区。这即是将g_count.counter自加和自减操作利用CPU的特殊指令(机制)实现为原子操作的原因。

2015.11.04

2. 无锁数据结构

(1) 无锁数据结构的优点

(作者说)在并发的环境里,加锁可以保护共享的数据,但是加锁也会存在一些问题:
- 由于临界区无法并发运行,进入临界区就需要等待,加锁带来效率的降低。
- 在复杂的情况下,很容易造成死锁,并发实体之间无止境的互相等待。
- 在中断/信号处理函数中不能加锁,给并发处理带来困难。[目前还不太理解这条]
- 优先级倒置造成实时系统不能正常工作。低级优先进程拿到高优先级进程需要的锁,结果是高/低优先级的进程都无法运行,中等优先级的进程可能在狂跑。

由于并发与加锁(互斥)的矛盾关系,无锁数据结构自然成为程序员关注的焦点(读写锁可以有效的解决单写多读问题,但更高效的办法是使用无锁数据结构 —- 无锁数据结构是想出来的应用于多线程程序(如多读单写共享数据)的一种方法所对应的数据结构)。

(2) 用无锁数据结构实现双向链表的多线程读单线程写

根据作者的思路:使用两份数据结构,一份数据结构用于读取(所有线程都可以在不加锁的情况下读取这个数据结构)。另外一份数据结构用于修改(由于只有一个线程会修改它,所以也不用加锁)。先修改用于修改的双向链表,修改完成之后等到没有线程读取时,交换读/写两个链表,再修改另一个链表,此时两个链表状态保持一致。

使用无锁数据结构避免了写线程中的加锁;在使用无锁数据结构过程中,需要加锁的部分用原子操作代替。

回归到像贼(zuwei)一样抄看作者代码的主题。
2015.11.05
[1] 双向链表接口
将(y15m9d25-27)下的dlist.c、dlist.h、tk.h文件以及atomic目录下的atomic.h拷贝到y15m11d03-05/rock_free目录下。

2015.11.04-11.05

[2] 交换操作的原子操作

/* rock_free_dlist.c */
/* 实现双向链表多线程读单线程写的无锁数据结构 */

#define CAS(_a, _o, _n) \
({ __typeof__(_o) __o = _o;                                \
   __asm__ __volatile__(                                   \
       "lock cmpxchg %3,%1"                                \
       : "=a" (__o), "=m" (*(volatile unsigned int *)(_a)) \
       :  "0" (__o), "r" (_n) );                           \
   __o;                                                    \
})

({ep1;ep2;})
printf(“%d\n”, ( { 1;2;} ) );语句输出的值为2。CAS的值为__o;。

_typeof_(http://gcc.gnu.org/onlinedocs/gcc/Typeof.html#Typeof)
__o为_o的类型变量,值为_o。

内嵌汇编语法( http://blog.csdn.net/littlehedgehog/article/details/2259665, http://blog.csdn.net/misskissC/article/details/16805749)

“=a” (__o), “=m” ((volatile unsigned int )(_a)) : “0” (__o), “r” (_n)依次对应%0, %1, %2, %3

“=a” (__o)中的”=”表示__o是输出操作数,”a”表示eax寄存器,__o的值首先会赋给eax寄存器代__o参加运算(若有),等指令执行完毕后eax的值会再赋回__o(同理”=m” ((volatile unsigned int )(_a)),”m”表示引用内存)

“0” (__o)引号中无”=”表示__o为输入操作数,”0”表示%2和%0引用同一个存储地址即__o的值将被存在eax中代__o参与运算(同理”r” (_n),”r”表示通用寄存器)。

cmpxchg r/m32, r32 功能(AT&T
将eax和目的操作数(r32)比较,如果相同,ZF被设置且源操作数(r/32)被载入目的操作数(r32)中。如果不相同,清ZF标志且将目的操作数(r32)载入到eax中。

那么,CAS宏的功能为:比较*_a与_o,若二者相等则将_n赋值给*_a并设置ZF,CAS最终的值为_o;若不等,则将*_a的值赋给eax且清ZF,CAS的最终值为*_a。

2015.11.05
[3] 无锁数据结构读单写双向链表接口
描述无锁数据结构的结构体。

/* rock_free_dlist.c */
/* 实现双向链表多线程读单线程写的无锁数据结构 */
#include "dlist.h"
#include "atomic.h"
#include "tk.h"
#include <stdlib.h>
#include <stdio.h>

//……
typedef struct _RFDlist {
    atomic_t        rd_index_and_ref;   //最高字节用于记录读取的双向链表的索引,低24位用于记录读取线程的引用计数
    DlistManageT    *dlists[2];         //管理两个双向链表的指针
}RFDlist;

dlists一个用来管理读-双向链表,另一个用来管理写-双向链表。

创建/释放无锁数据结构下的双向链表。

/* rock_free_dlist.c */
/* 实现双向链表多线程读单线程写的无锁数据结构 */
//……

//创建无锁数据结构环境下的双向链表
RFDlist *create_rf_dlist(unsigned int len)
{
    int ret = -1;
    RFDlist *prf_dlist = NULL;

    return_val_if_p_invalid(len > 0, NULL);

    prf_dlist   = (RFDlist *)malloc(sizeof(RFDlist));
    if (prf_dlist == NULL) return NULL;

    do {
        if ((prf_dlist->dlists[0] = create_dlist(len)) == NULL) {
            break;
        }

        if ((prf_dlist->dlists[1] = create_dlist(len)) == NULL) {
            break;
        }
        ret = 0;
    }while(0);

    if (ret != 0) {
        free(prf_dlist);
        prf_dlist   = NULL;
    }

    return prf_dlist;
}

//释放双向链表内存
void free_rf_dlist(RFDlist *prf_dlist)
{
    if (prf_dlist != NULL) {
        if (prf_dlist->dlists != NULL) {
            free(prf_dlist->dlists[0]);
            free(prf_dlist->dlists[1]);
            prf_dlist->dlists[0]    = NULL;
            prf_dlist->dlists[1]    = NULL;
        }
        free(prf_dlist);
    }
}

获取双向链表长度。

/* rock_free_dlist.c */
/* 实现双向链表多线程读单线程写的无锁数据结构 */
//……

//获取双向链表长度
unsigned int get_rf_dlist_length(RFDlist *prf_dlist)
{
    size_t rd_index = 0;
    unsigned int ret = 0;
    return_val_if_p_invalid(NULL != prf_dlist && NULL != prf_dlist->dlists, ret);

    //读线程计数加1
    atomic_inc(&(prf_dlist->rd_index_and_ref));

    //采取与insert操作互斥的取值得到读双向链表的索引<见insert_rf_dlist>
    rd_index    = (prf_dlist->rd_index_and_ref.counter >> 24) & 0x01;
    ret         = get_dlist_length(prf_dlist->dlists[rd_index]);

    //读线程计数减1
    atomic_dec(&(prf_dlist->rd_index_and_ref));

    return ret;
}

由于dlist.c中创建的链表没有被初始化,所以用获取双向链表长度作为读双线链表。

修改双向链表。

/* rock_free_dlist.c */
/* 实现双向链表多线程读单线程写的无锁数据结构 */
//……

//在i节点前插入数据为*data的节点
int insert_rf_dlist(RFDlist *prf_dlist, unsigned int i, void *data)
{
    int             ret = -1;
    size_t          wr_index = 0;

    return_val_if_p_invalid(NULL != prf_dlist && NULL != prf_dlist->dlists, -1);

    //采取与get操作互斥的取值得到修改双向链表的索引<见get_rf_dlist_length>
    wr_index    = !((prf_dlist->rd_index_and_ref.counter >> 24) & 0x1);

    if ((ret = insert_dlist_node(prf_dlist->dlists[wr_index], i, data)) == 0) {
        int rd_index_old    = prf_dlist->rd_index_and_ref.counter & 0xff000000; //读-双向链表的索引
        int rd_index_new    = wr_index << 24;   //将写-双向链表的索引赋值给rd_index_new

        //比较prf_dlist->rd_index_and_ref.counter和rd_index_old,若不等则CAS的值为前者;
        //相等(表示读线程引用计数为0)则CAS的值为rd_index_old且prf_dlist->rd_index_and_ref.counter = rd_index_new
        //&(prf_dlist->rd_index_and_ref)即为&(prf_dlist->rd_index_and_ref.counter)
        do {
            usleep(100);
        }while(CAS(&(prf_dlist->rd_index_and_ref), rd_index_old, rd_index_new));

        //修改读-双向链表,使得读-双向链表立即跟写-数据双向链表保持一致
        wr_index    = rd_index_old >> 24;
        ret = insert_dlist_node(prf_dlist->dlists[wr_index], i, data);
    }
    return ret;
}

**do {
usleep(100);
}while(CAS(&(prf_dlist->rd_index_and_ref), rd_index_old, rd_index_new));**
prf_dlist->rd_index_and_ref.counter跟rd_index_old不等时(读线程引用计数不为0,CAS的值为prf_dlist->rd_index_and_ref.counter,执行usleep(100)语句让读线程继续运行);prf_dlist->rd_index_and_ref.counter跟rd_index_old相等时(无读线程访问链表,CAS将rd_index_new赋值给prf_dlist->rd_index_and_ref.counter即将写-双向链表的索引给counter(表示在写-双向链表),CAS的值为rd_index_old)。rd_index_old和rd_index_new(赋给*_a作为CAS的值)必将有一个为0,所以while不会轮为死循环。

(3) 调用接口

/* rock_free_dlist.c */
/* 实现双向链表多线程读单线程写的无锁数据结构 */
#include "dlist.h"
#include "atomic.h"
#include "tk.h"
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

//……

#define RN 100
static void *reader(void *param)
{
    int i = 0;
    int j = 0;
    RFDlist *prf_dlist = NULL;

    return_if_p_invalid(NULL != param);
    prf_dlist   = (RFDlist *) param;

    for (i = 0; i < RN; i++) {
        for (j = 0; j < get_rf_dlist_length(prf_dlist); j++) {
            ;
        }
    }
    printf("%d\n", get_rf_dlist_length(prf_dlist));
    return NULL;
}

static void *writer(void *param)
{
    int i = 0;
    int len = 0;
    RFDlist *prf_dlist = NULL;

    return_if_p_invalid(NULL != param);
    prf_dlist   = (RFDlist *) param;

    len = get_rf_dlist_length(prf_dlist);
    for (i = 0; i < len; i++) {
        insert_rf_dlist(prf_dlist, i + 1, (void*)i);
    }
    return NULL;
}

#define RD_N 1000

int main(void)
{
    int i = 0;
    pthread_t wr_tid = 0;
    pthread_t rd_tids[RD_N] = {0};
    RFDlist *prf_dlist = NULL;

    prf_dlist   = create_rf_dlist(10);
    if ( prf_dlist == NULL) return -1;

    pthread_create(&wr_tid, NULL, writer, prf_dlist);
    for (i = 0; i < RD_N; i++) {
        pthread_create(rd_tids+i, NULL, reader, prf_dlist);
    }

    for (i = 0; i < RD_N; i++) {
        pthread_join(rd_tids[i], NULL);
    }

    pthread_join(wr_tid, NULL);
    free_rf_dlist(prf_dlist);
    prf_dlist    = NULL;
    return 0;
}

创建RD_N个读双向链表线程和1个写双向链表线程。一旦修改写-双向链表就等待OS调度(一直到无读线程读 读-双向链表为止),更新读-双向链表跟写-双向链表保持一致。这个过程中,只有增加读线程引用计数以及交换读-/写-双向链表索引过程中使用了原子操作。整个过程无加锁过程。

(4) Makefile

#Makefile
#make命令默认执行make all或者第一条规则
all:        rock_free_dlist.o dlist.o
    $(CC) $^ $(CFLAGS) $@ $(LTHREAD_LIB)

rock_free_dlist.o:  rock_free_dlist.c dlist.h tk.h atomic.h
    $(CC) $(CFLAG_OBJ) $< $(CFLAGS_POSTFIX) $(LTHREAD_LIB)

dlist.o:    dlist.c dlist.h tk.h
    $(CC) $(CFLAG_OBJ) $< $(CFLAGS_POSTFIX)


clean:
    -rm *.o

#定义变量
CC=gcc
CFLAGS=-o
CFLAG_OBJ=-c
CFLAGS_POSTFIX=-Wall
LTHREAD_LIB= -lpthread

在与各文件同目录的linux终端执行make。命令后得到可执行的all可执行程序。

[2015.11.05-17:03]

你可能感兴趣的:(c,程序设计方法)