zoukankan      html  css  js  c++  java
  • spinlock in c++11 based on atomic_flag std::memory_order_acquire

     C++原子库中仅保证atomic_flag是保证无锁的,而atomic< int>,atomic< bool>不是

    spinlock.h

    #ifndef _SPINLOCK_H_20170410_
    #define _SPINLOCK_H_20170410_
    
    #include <atomic>
    
    class spinlock {
    public:
        spinlock() { m_lock.clear(); }
        spinlock(const spinlock&) = delete;
        ~spinlock() = default;
    
        void lock() {
            while (m_lock.test_and_set(std::memory_order_acquire));
        }
        bool try_lock() {
            return !m_lock.test_and_set(std::memory_order_acquire);
        }
        void unlock() {
            m_lock.clear(std::memory_order_release);
        }
    private:
        std::atomic_flag m_lock;
    };
    
    #endif//_SPINLOCK_H_20170410_
    root@ubuntu:~/c++# cat spinlock.cpp 
    #include "spinlock.h"
    #include <mutex>
    #include <future>
    #include <iostream>
    #include <map>
    
    spinlock lock;
    int value = 0;
    
    int loop(bool inc, int limit) {
        std::cout << "Started " << inc << " " << limit << std::endl;
        for (int i = 0; i < limit; ++i) {
            std::unique_lock<spinlock> _lock(lock);
            if (inc)
                ++value;
            else
                --value;
        }
        return 0;
    }
    
    int main() {
        auto f = std::async(std::launch::async, std::bind(loop, true, 20000000));
        loop(false, 10000000);
        f.wait();
        std::cout << value << std::endl;
    }
    root@ubuntu:~/c++# g++   -std=c++11 -pthread spinlock.cpp -o spin
    root@ubuntu:~/c++# ./spin
    Started Started 0 100000001 20000000
    
    10000000

    原子数据类型/atomic类型

    让我们先来看一下atomic模板类:

    template <class T> struct atomic
    
    //example
    #include<atomic>
    
    void test()
    {
        std::atomic_int nThreadData; // std::atomic_int  <----> std::atomic<int>
        nThreadData = 10;
        nThreadData.store(10);
        //TODO: use nThreadData here;
    }

    对于内置型数据类型,C11和C++11标准中都已经提供了实例化原子类型,如下表所示:

    表#1

    atomic类型原子操作接口如下:

    内存模型

    通常情况下,内存模型是一个硬件上的概念,表示的是机器指令(或者将其视为汇编指令也可以)是以什么样的顺序被处理器执行的。现代的处理器并不是逐条处理机器指令的。

    #include <thread>
    #include <atomic>
    #include <iostream>
    using namespace std;
    atomic<int> a{0};
    atomic<int> b{0};
    
    int ValueSet(int)
    {
        int t=1;
        a=t;
        b=2;
        return 0;
    }

    平淡无奇的代码。指令“t=1;a=t;b=2”,其伪汇编代码如下:

    按照通常的理解,指令总是按照1->2->3->4->5顺序执行的,如果处理器是按照这个顺序执行的,我们称这样的内存模型为强顺序的(strong ordered)。 这种执行方式下,指令3总是先于指令5执行,即a赋值在前,b赋值在后。

    但是指令1、2、3(a赋值)和指令4、5(b赋值)毫不相干。一些处理器可能将指令乱序执行,比如按照1->4->2->5->3这样的顺序(超标量流水线,即一个时钟周期里发射多条指令)。如果指令是“乱序”执行的,我们称这样的内存模型为弱顺序的(weak ordered)。这种执行方式下,指令5可能先于指令3被执行,即可能b赋值在前,a赋值在后。

    弱顺序的内存模型的好处在哪里?可以进一步挖掘指令中的并行性,提高指令执行的性能。

    在单线程程序中,我管你内存模型是强顺序的还是弱顺序的,管你是顺序执行还是乱序执行的,反正最终结果是a等于1,b等于2。

    但是在多线程情况下,“乱序”执行可能就会造成问题。

    书1 - 203页,代码清单 6-21

    root@ubuntu:~/c++# cat atomic9.cpp
    #include <thread>
    #include <atomic>
    #include <iostream>
    using namespace std;
    atomic<int> a{0};
    atomic<int> b{0};
    int ValueSet(int)
    {
        int t=1;
        a=t;
        b=2;
    }
    int Observer(int)
    {
        cout<<"("<<a<<","<<b<<")"<<endl;//可能有多种输出
    }
    int main()
    {
        thread t1(ValueSet,0);
        thread t2(Observer,0);
        t1.join();
        t2.join();
        cout<<"Got("<<a<<","<<b<<")"<<endl;//Got(1,2)
    }
     
    root@ubuntu:~/c++# g++ -std=c++11  atomic9.cpp -o atom -pthread
    root@ubuntu:~/c++# ./atom
    (1,2)
    Got(1,2)

    线程Observer只是试图一窥线程ValueSet的执行情况,就看看而已,所以无论ValueSet的代码是顺序执行的还是乱序执行的,都无所谓,无非就是a,b输出值的顺序可能是(0,0),或者(1,2),或者(1,0),甚至(0,2),但是最终的输出结果都是(1,2)。

    通常情况下,如果编译器认定a、b的赋值语句的执行先后顺序对输出结果没有任何的影响的话,则可以依情况将指令重排序(reorder)以提高性能。而如果a、b赋值语句的执行顺序必须是a先b后,则编译器则不会执行这样的优化。

    试想一下,如果Observer里的操作结果严重依赖于ValueSet中指令的执行顺序,会怎么样?代码字面上的执行顺序都可能被打乱了,Observer不出问题才怪!

    你一会想顺序执行,一会又想“乱序”执行,更有甚者,还想对“乱”的程度分等级……如何提供这种灵活性呢?

    在C++11标准中,设计者给出的解决方式是让程序员为原子操作指定所谓的内存顺序:memory_order。

    root@ubuntu:~/c++# g++  -std=c++11  atomic9.cpp -o atom -pthread
    root@ubuntu:~/c++# ./atom
    (1,2)
    Got(1,2)
    root@ubuntu:~/c++# cat atomic9.cpp 
    #include <thread>
    #include <atomic>
    #include <iostream>
    using namespace std;
    atomic<int> a{0};
    atomic<int> b{0};
    int ValueSet(int)
    {
        int t=1;
        a.store(t,memory_order_relaxed);
        b.store(2,memory_order_relaxed);
    }
    int Observer(int)
    {
        cout<<"("<<a<<","<<b<<")"<<endl;//可能有多种输出
    }
    int main()
    {
        thread t1(ValueSet,0);
        thread t2(Observer,0);
        t1.join();
        t2.join();
        cout<<"Got("<<a<<","<<b<<")"<<endl;//Got(1,2)
    }

    注意memory_order_relaxed的使用,其实际意义后面再解释。对原子类型而言,赋值“=”和调用store接口函数,作用都是一样的,只不过,除了要写入的值外,store还接受另外一个名为memory_order的枚举值。来看下std::atomic<T>::store接口的定义:

    void atomic<T>::store( T desired, std::memory_order order = std::memory_order_seq_cst ) volatile noexcept;

    memory_order参数的默认值是std::memory_order_seq_cst。实际上,atomic类型的其他原子操作接口都有memory_order这个参数,而且默认值都是std::memory_order_seq_cst

    枚举memory_order如下:

    typedef enum memory_order {
    	memory_order_relaxed,
    	memory_order_consume,
    	memory_order_acquire,
    	memory_order_release,
    	memory_order_acq_rel,
    	memory_order_seq_cst
    	} memory_order;

    memory_order的各个枚举值的定义规则如下:

    表#3

    让我们来逐一检视这些枚举值的意义。

    顺序一致内存顺序/memory_order_seq_cst

    定义规则:全部存取都按照顺序执行。

    memory_order_seq_cst 表示该原子操作必须顺序一致的,这是C++11中所有atomic原子操作的默认值。这样来理解“顺序一致”:即代码在线程中运行的顺序与程序员看到的代码顺序一致。也就是说,用此值提示编译器“不要给我重排序指令,不要整什么指令乱序执行,就按照我代码的先后顺序执行机器指令”。在示例代码中,a的赋值语句先于b的赋值语句执行,这种称之为”先于发生(happens-before)“关系。用memory_order_seq_cst 可以确保这种happens_before关系。

    松散内存顺序/memory_order_relaxed

    定义规则:不对执行顺序做任何保证。

    表示该原子操作指令可以任由编译器重排或者由处理器乱序执行。就是说”想怎么乱就怎么乱吧,不管了,只要能提高指令执行效率“。代码清单6-23中使用的就是松散内存模型,在Observer中打印出(0,2)这样的结果也是合理的——把我代码中的顺序都彻底整反了!

    root@ubuntu:~/c++# cat atomic9.cpp 
    #include <thread>
    #include <atomic>
    #include <iostream>
    using namespace std;
    atomic<int> a{0};
    atomic<int> b{0};
    int ValueSet(int)
    {
        int t=1;
        a.store(t,memory_order_relaxed);
        b.store(2,memory_order_relaxed);
    }
    int Observer(int)
    {
        while(b.load(memory_order_relaxed)!=2);//自旋等待
        cout<<a.load(memory_order_relaxed)<<endl;
    }
    int main()
    {
        thread t1(ValueSet,0);
        thread t2(Observer,0);
        t1.join();
        t2.join();
        cout<<"Got("<<a<<","<<b<<")"<<endl;//Got(1,2)
    }
    root@ubuntu:~/c++# g++  -std=c++11  atomic9.cpp -o atom -pthread
    root@ubuntu:~/c++# ./atom
    1
    Got(1,2)
    root@ubuntu:~/c++# 

    上述代码中,我们用memory_order_relaxed的初衷是不希望完全禁用原子类型的优化。”自旋等待“那行代码的真实用意是:先自旋等待b被赋值为2,随后再将a的值输出。但是按照松散内存顺序,a.store 和 b.store指令的先后顺序不能保证了,b.store可能先被执行,因此a的输出值可能是0,也可能是1。 这些不是代码作者想要的。

    Release-acquire内存顺序

    memory_order__acquire

    规则定义:本线程中,所有后续的读操作,必须在本条原子操作完成后执行。(本线程中,我先读,你们后读……)

    memory_order_release

    规则定义:本线程中,所有之前的写操作完成后,才能执行本原子操作。(在本线程中,你们先写,我最后写……)

    上面讲的顺序一致和松散方式对应着两个极端——一个是严格禁止”乱“,一个是允许随便”乱“。但是现实的问题是:严格禁止”乱“,指令执行不够快;允许随便”乱“,又得不到正确结果。

    ”能搞组合贷不?“

    root@ubuntu:~/c++# g++  -std=c++11  atomic9.cpp -o atom -pthread
    root@ubuntu:~/c++# ./atom
    1
    Got(1,2)
    root@ubuntu:~/c++# cat atomic9.cpp 
    #include <thread>
    #include <atomic>
    #include <iostream>
    using namespace std;
    atomic<int> a{0};
    atomic<int> b{0};
    int ValueSet(int)
    {
        int t=1;
        a.store(t,memory_order_relaxed);
        b.store(2,memory_order_release);//本原子操作前所有的写原子操作必须完成
    }
    int Observer(int)
    {
        while(b.load(memory_order_acquire)!=2);////本原子操作必须完成才能执行之后所有的读原子操作
        cout<<a.load(memory_order_relaxed)<<endl;
    }
    int main()
    {
        thread t1(ValueSet,0);
        thread t2(Observer,0);
        t1.join();
        t2.join();
        cout<<"Got("<<a<<","<<b<<")"<<endl;//Got(1,2)
    }
    root@ubuntu:~/c++# 

    注意:Thread1中,b.store采用了memory_order_release内存顺序,保证了本线程中,本算子操作前的所有写操作都必须完成,也即a.store必须发生于b.store之前。在Thread2中,b.load采用了memory_order__acquire内存顺序,保证了本线程中,本原子操作必须先完成,才能执行之后所有的读原子操作,即b.load必须先于a.load执行。

    Release-consume内存顺序

    memory_order_consume

    规则定义:本线程中,所有后续的有关本数据的操作,必须在本条原子操作完成之后执行。(本线程中,我只关心我自己,当我用memory_order_consume时,后面所有对我的读写操作都不能被提前执行……)

    相比于memory_order__acquire,memory_order_consume进一步放松了依赖关系。大家发现没有,前面讲的几种内存顺序都是在操控/安排多个atomic数据之间的读写顺序,而memory_order_consume仅仅考虑对一个atomic数据的读写顺序。

    #include <thread>
    #include <atomic>
    #include <cassert>
    #include <string>
    using namespace std;
    atomic<string*>ptr;
    atomic<int> data;
    void Producer()
    {
        string*p=new string("Hello");
        data.store(42,memory_order_relaxed);
        ptr.store(p,memory_order_release);
    }
    void Consumer()
    {
        string*p2;
        while(!(p2=ptr.load(memory_order_consume)))
        ;
        assert(*p2=="Hello");//总是相等
        assert(data.load(memory_order_relaxed)==42);//可能断言失败
    }
    int main()
    {
        thread t1(Producer);
        thread t2(Consumer);
        t1.join();
        t2.join();
    }
    root@ubuntu:~/c++# g++  -std=c++11  atomic9.cpp -o atom -pthread
    root@ubuntu:~/c++# ./atom
    root@ubuntu:~/c++# 

     注意,Consumer函数中第一个assert语句:对指针p2进行解引用操作,其实质是在ptr上调用load。 我们可以保证第一个assert不会被触发,因为通过memory_order_consume的内存顺序,保证while语句中ptr.load必须发生在*p2这个解引用操作(实际上涉及读取指针ptr.load的操作)之前。第二个断言可能失败,原因自行分析下。

    C++11 - atomic类型和内存模型

    The Purpose of memory_order_consume in C++11

  • 相关阅读:
    14-定时器
    13-JS中的面向对象
    12-关于DOM操作的相关案例
    11-DOM介绍
    10-关于DOM的事件操作
    09-伪数组 arguments
    08-函数
    07-常用内置对象
    06-流程控制
    05-数据类型转换
  • 原文地址:https://www.cnblogs.com/dream397/p/15076456.html
Copyright © 2011-2022 走看看