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