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

  • 相关阅读:
    SGU 271 Book Pile (双端队列)
    POJ 3110 Jenny's First Exam (贪心)
    HDU 4310 Hero (贪心)
    ZOJ 2132 The Most Frequent Number (贪心)
    POJ 3388 Japanese Puzzle (二分)
    UVaLive 4628 Jack's socks (贪心)
    POJ 2433 Landscaping (贪心)
    CodeForces 946D Timetable (DP)
    Android Studio教程从入门到精通
    Android Tips – 填坑手册
  • 原文地址:https://www.cnblogs.com/dream397/p/15076456.html
Copyright © 2011-2022 走看看