聊聊原子操作那些事

原子操作,线程间交互数据最细粒度的同步操作,它可以保证线程间读写某个数值的原子性。

由于不需要加重量级的互斥锁进行同步,因此非常轻量,而且也不需要在内核间来回切换调度,效率是非常高的。。

那如何使用原子操作了,各个平台下都有相关api提供了支持,并且向gcc、clang这些编译器,也提供了编译器级的__builtin接口进行支持

  1. windows的Interlockedxxx和Interlockedxxx64系列api
  2. macosx的OSAtomicXXX系列api
  3. gcc的__sync_val_compare_and_swap__sync_val_compare_and_swap_8等__builtin接口
  4. x86和x86_64架构的lock汇编指令
  5. tbox的跨平台原子接口

tbox接口使用

先拿tbox的tb_atomic_fetch_and_add接口为例,顾名思义,这个api会先读取原有数值,然后在其基础上加上一个数值:

// 相当于原子进行:b = *a++;
tb_atomic_t a = 0;
tb_long_t   b = tb_atomic_fetch_and_add(&a, 1);

如果需要先进行add计算,再返回结果可以用:

// 相当于原子进行:b = ++*a;
tb_atomic_t a = 0;
tb_long_t   b = tb_atomic_add_and_fetch(&a, 1);

或者可以更加简化为:

tb_long_t b = tb_atomic_fetch_and_inc(&a);
tb_long_t b = tb_atomic_inc_and_fetch(&a);

那tbox在内部如何去适配各个平台的呢,我们可以简单看下,基本上就是对原生api进行了一层wrap而已。

windows接口封装

static __tb_inline__ tb_long_t tb_atomic_fetch_and_add_windows(tb_atomic_t* a, tb_long_t v)
{
    return (tb_long_t)InterlockedExchangeAdd((LONG __tb_volatile__*)a, v);
}
static __tb_inline__ tb_long_t tb_atomic_inc_and_fetch_windows(tb_atomic_t* a)
{
    return (tb_long_t)InterlockedIncrement((LONG __tb_volatile__*)a);
}

gcc接口的封装

static __tb_inline__ tb_long_t tb_atomic_fetch_and_add_sync(tb_atomic_t* a, tb_long_t v)
{
    return __sync_fetch_and_add(a, v);
}

x86和x86_64架构汇编实现

static __tb_inline__ tb_long_t tb_atomic_fetch_and_add_x86(tb_atomic_t* a, tb_long_t v)
{
    /*
     * xaddl v, [a]:
     *
     * o = [a]
     * [a] += v;
     * v = o;
     *
     * cf, ef, of, sf, zf, pf... maybe changed
     */
    __tb_asm__ __tb_volatile__ 
    (
#if TB_CPU_BITSIZE == 64
        "lock xaddq %0, %1 \n"          //!< xaddq v, [a]
#else
        "lock xaddl %0, %1 \n"          //!< xaddl v, [a]
#endif

        : "+r" (v) 
        : "m" (*a) 
        : "cc", "memory"
    );

    return v;
}

原子操作除了可以进行对int32和int64数值加减乘除外,还可以进行xor, or, and等逻辑计算,用法类似,这里就不多说了。

下面我们再来个简单的实例,来实际运用下,原子的应用场景还是蛮多的,比如:

  • 用于实现自旋锁
  • 用于实现无锁队列
  • 线程间的状态同步
  • 用于实现单例

等等。。

自旋锁的实现

我们先来看下如何去实现一个简单的自旋锁,为了统一规范演示代码,下面的代码都用tbox提供的原子接口为例:

static __tb_inline_force__ tb_bool_t tb_spinlock_init(tb_spinlock_ref_t lock)
{
    // init 
    *lock = 0;

    // ok
    return tb_true;
}
static __tb_inline_force__ tb_void_t tb_spinlock_exit(tb_spinlock_ref_t lock)
{
    // exit 
    *lock = 0;
}
static __tb_inline_force__ tb_void_t tb_spinlock_enter(tb_spinlock_ref_t lock)
{
    /* 尝试读取lock的状态值,如果还没获取到lock(状态0),则获取它(设置为1)
     * 如果对方线程已经获取到lock(状态1),那么循环等待尝试重新获取
     *
     * 注:整个状态读取和设置,是原子的,无法被打断
     */
    tb_size_t tryn = 5;
    while (tb_atomic_fetch_and_pset((tb_atomic_t*)lock, 0, 1))
    {
        // 没获取到lock,尝试5次后,还不成功,则让出cpu切到其他线程运行,之后重新尝试获取
        if (!tryn--)
        {
            // yield
            tb_sched_yield();

            // reset tryn
            tryn = 5;
        }
    }
}
static __tb_inline_force__ tb_void_t tb_spinlock_leave(tb_spinlock_ref_t lock)
{
    // 释放lock,此处无需原子,设置到一半被打断,数值部位0,对方线程还是在等待中,不收影响
    *((tb_atomic_t*)lock) = 0;
}

这个实现非常简单,但是tbox里面,基本上默认都是在使用这个spinlock,因为tbox里面大部分多线程实现,粒度都被拆的很细

大部分情况下,用自旋锁就ok了,无需进入内核态切换等待。。

使用方式如下:

// 获取lock
tb_spinlock_enter(&lock);

// 一些同步操作
// ..

// 释放lock
tb_spinlock_leave(&lock);

上面的代码中,省略了init和exit操作,实际使用时,在响应初始化和释放的地方,做相应处理下就行了。。

pthread_once的实现

pthread_once 可以在多线程函数内,可以保证传入的函数只被调用到一次,一般可以用来初始化全局单例或者TLS的key初始化

以tbox的接口为例,我先来来看下,这个函数的使用方式:


// 初始化函数,只会被调用到一次
static tb_void_t tb_once_func(tb_cpointer_t priv)
{
    // 初始化一些单例对象,全局变量
    // 或者执行一些初始化调用
}

// 线程函数
static tb_int_t tb_thread_func(tb_cpointer_t priv)
{
    // 全局存储lock,并初始化为0
    static tb_atomic_t lock = 0;
    if (tb_thread_once(&lock, tb_once_func, "user data"))
    {
        // ok
    }
}

我们这里拿原子操作,可以简单模拟实现下这个函数:

tb_bool_t tb_thread_once(tb_atomic_t* lock, tb_bool_t (*func)(tb_cpointer_t), tb_cpointer_t priv)
{
    // check
    tb_check_return_val(lock && func, tb_false);

    /* 原子获取lock的状态
     *
     * 0: func还没有被调用
     * 1: 已经获取到lock,func正在被其他线程调用中
     * 2: func已经被调用完成,并且func返回ok
     * -2: func已经被调用,并且func返回失败failed
     */
    tb_atomic_t called = tb_atomic_fetch_and_pset(lock, 0, 1);

    // func已经被其他线程调用过了?直接返回
    if (called && called != 1) 
    {
        return called == 2;
    }
    // func还没有被调用过?那么调用它
    else if (!called)
    {
        // 调用函数
        tb_bool_t ok = func(priv);

        // 设置返回状态
        tb_atomic_set(lock, ok? 2 : -1);

        // ok?
        return ok;
    }
    // 正在被其他线程获取到lock,func正在被调用中,还没完成?尝试等待lock
    else
    {
        // 此处简单的做了些sleep循环等待,直到对方线程func执行完成
        tb_size_t tryn = 50;
        while ((1 == tb_atomic_get(lock)) && tryn--)
        {
            // wait some time
            tb_msleep(100);
        }
    }

    /* 重新获取lock的状态,判断是否成功
     * 
     * 成功:2
     * 超时:1
     * 失败:-2
     *
     * 此处只要不是2,都算失败
     */
    return tb_atomic_get(lock) == 2;
}

64位原子操作

64位操作跟32位的接口使用方式,是完全一样的,仅仅只是变量类型的区别:

  1. tbox中类型为tb_atomic64_t,接口改为tb_atomic64_xxxx
  2. gcc中类型为volatile long long,接口改为__sync_xxxx_8系列
  3. windows上则为Interlockedxxx64

具体使用方式参考32位,这里就不详细介绍了。。


个人主页:TBOOX开源工程
原文出处:http://tboox.org/cn/2016/09/30/atomic-operation/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容