C11的原子性操作

1. 原子性

1.1 原子性的含义

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

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

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

2. C11的atomic operation

2.1 理解多线程

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

#include 
#include 
 
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 
#include 
#include                  // 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模板类

4.1 atomic的定义

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

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

你可能感兴趣的:(C11的原子性操作)