美文网首页C++复习
C11的<atomic>原子性操作

C11的<atomic>原子性操作

作者: 凉拌姨妈好吃 | 来源:发表于2018-05-24 22:26 被阅读91次

    1. 原子性

    1.1 原子性的含义

    多个线程访问同一全局资源的时候,能够确保所有其他的线程不在同一时间内访问该资源。也就是说,它确保了同一时间内只有唯一线程能对资源进行访问

    1.2 在并发编程时如何原子操作

    合理选择平台下的atomic API,如果底层并没有该模式的API,只能使用锁机制。

    2. C11的atomic operation

    2.1 理解多线程

    下面我们通过一个例子来理解一下多线程:开两个线程对sum进行运算,要让sum从1加到1000

    #include <iostream>
    #include <thread>
     
    long sum = 0L;
     
    void fun()
    {
        for(int i=1;i<100000;++i)
            sum += i;
    }
     
    int main()
    {
        std::cout << "Before joining,sun = " << sum << std::endl;
        std::thread t1(fun);
        std::thread t2(fun);
        t1.join();
        t2.join();
        std::cout << "After joining,sun = " << sum << std::endl;
    }
    

    我们可以通过不断运行该程序得出,最后的结果只会小于等于我们的预期值,永远不会大于我们的预期值。
    为什么呢?
    这里引用我是一只C++小小鸟的例子来解释一下原因

    你和朋友合租在一间房子里边,房子里面只有一间厨房,你们共用一个锅。有一天你准备做一道西红柿炒蛋,当你把西红柿放入锅中的时候,你的电话响了,你离开厨房去接电话。而这时候你的室友也要做饭,他要做一道红烧鱼,于是他把洗好的鱼放入了锅中煮,然后也离开了厨房(由于某种原因他不知道锅里还有你的食材,在程序中线程也不会知道其他线程对共享的数据做了什么)。当你回来的时候继续往里边放入鸡蛋,最后你得到的是一盘西红柿炒鸡蛋鱼。而你的室友回来厨房的时候他要的红烧鱼就会不见了。

    2.2 解决上面多线程引起的问题

    我们使用C11的atomic_long对我们的sum变量进行限定,这样多线程对它的操作只能是原子性的。此时最后的sum才是我们预期所要的答案。

    #include <iostream>
    #include <thread>
    #include <atomic>                 // modified
     
    std::atomic_long sum = {0L};    //  modified
     
    void fun()
    {
        for(int i=0;i<100000;++i)
            sum += i;
    }
     
    int main()
    {
        std::cout << "Before joining,sun = " << sum << std::endl;
        std::thread t1(fun);
        std::thread t2(fun);
        t1.join();
        t2.join();
        std::cout << "After joining,sun = " << sum << std::endl;
    }
    

    3. C11的自旋锁

    3.1 为什么要引入自旋锁

    因为唤醒线程需要时间,所以现在为了避免性能降低。在另一个线程访问对象且该对象已被占用的时候,设置一个循环访问次数,在这个次数内不断循环访问临界区对象,如果该对象被释放,这个线程就不会进入休眠。如果该对象在循环次数内依旧没有释放,线程就会进入线程。

    3.2 自旋锁的定义

    原子操作+自循环(线程不休眠,不断尝试对资源进行访问)

    3.3 atomic_flag自旋锁的使用

    atomic_flag其实就是锁,当某个线程在访问某共享变量的时候,另一个线程也想要访问,就会不停的调用函数去查看该锁有没有被释放,如果该锁被释放,这个线程才能够访问该共享变量。
    下面有一个类似的例子

    std::atomic_flag lock = ATOMIC_FLAG_INIT;   //初始化,此时lock处于clear状态
     
    void f(int n)
    {
        while(lock.test_and_set())    //获取锁的状态
            std::cout << "Waiting ... " << std::endl;
        std::cout << "Thread " << n << " is starting working." << std::endl;
    }
     
    void g(int n)
    {
        sleep(3);
        std::cout << "Thread " << n << " is going to clear the flag." << std::endl;
        lock.clear();   // 解锁
    }
     
    int main()
    {
        lock.test_and_set();
        std::thread t1(f,1);
        std::thread t2(g,2);
     
        t1.join();
        t2.join();
    }
    

    在线程2调用g函数的时候,线程1不断的循环获取atomic_flag的状态,直到线程2释放了atomic_flag

    3.4 atomic_flag自旋锁的主要函数
    • atomic_flag::test_and_set():首先检查这atomic_flag类中的bool成员_M_i是否被设置成true,如果没有就先设置成true,并返回之前的值(flase),如果atomic_flag中的bool成员已经是true,则直接返回true。
    • atomic_flag::clear():将atomic_flag的bool值得标志成员_M_i设置成flase,没有返回值。
    3.5 atomic_flag封装成一个锁类
    class MyLock
    {
    private:
        std::atomic_flag m_flag;
    public:
        MyLock();
        void lock();
        void unlock();
    };
     
    MyLock::MyLock()
    {
        m_flag.clear();                    //if not do this,m_flag will be unspecified
    }
     
    void MyLock::lock()
    {
        while(m_flag.test_and_set())
            ;
    }
     
    void MyLock::unlock()
    {
        m_flag.clear();
    }
    

    4. atomic<T>模板类

    4.1 atomic<T>的定义

    它定义了一个T类型的原子对象,并且提供了一系列原子操作的成员函数。

    有一个重要的规则:不要在保护数据中通过用户自定义类型T的指针或引用使得共享数据超出它的保护的作用域。atomic<T>编译器通常会使用一个内部锁保护,当用户使用自定义类型T的指针或引用的时候可能会造成死锁。

    相关文章

      网友评论

      本文标题:C11的<atomic>原子性操作

      本文链接:https://www.haomeiwen.com/subject/wbusjftx.html