欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C11的原子性操作

程序员文章站 2022-07-14 08:07:34
...

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的指针或引用的时候可能会造成死锁。