atomic 无锁编程
多个线程之间共享地址空间,所以多个线程共享进程中的全局变量和堆,都可以对全局变量和堆上的数据进行读写,但是如果两个线程同时修改同一个数据,可能造成某线程的修改丢失;如果一个线程写的同时,另一个线程去读该数据时可能会读到写了一半的数据。这些行为都是线程不安全的行为,会造成程序运行逻辑出现错误。下面的程序很常见:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25// boost::atomic<int> i(0);
int i=0;
boost::mutex mut;
void thread_1(int n);
int main()
{
int n=100000; // n不够大时,i不容易出现不同的情况
boost::thread th1 = boost::thread(boost::bind(&thread_1,n));
boost::thread th2 = boost::thread(boost::bind(&thread_1,n));
th1.join();
th2.join();
cout<< i<<endl;
return 0;
}
void thread_1(int n)
{
while(n--)
{
mut.lock();
i++;
mut.unlock();
}
}
如果不加lock,i最终值是不确定的,因为两个线程同时对i进行了写操作。一般地,我们用互斥锁mutex保护临界区,保证同一时间只能有一个线程可以获取锁,持有锁的线程可以对共享变量进行修改,修改完毕后释放锁,而不持有锁的线程阻塞等待直到获取到锁,然后才能对共享变量进行修改,最后i必定为200000
Boost提供了原子类型atomic,通过使用原子类型可摆脱每次对共享变量进行操作都进行的加锁解锁动作,节省了系统开销,同时避免了线程因阻塞而频繁的切换。atomic封装了不同计算机硬件的底层操作原语,提供了跨平台的原子操作功能,解决并发竞争读写变量的困扰,只需要包含文件<boost/atomic.hpp>
,在上面的代码中使用boost::atomic<int> i(0);
,然后去掉函数中的互斥锁,运行效果是一样的,而且节省了系统开销。
atomic可以把对类型T的操作原子化,T的要求:
- 标量类型(算数,枚举,指针)
- POD类型,可以使用memcmp,memset等函数
两种方式创建atomic对象:1
2
3
4
5
6atomic<int> a(10);
assert(a==10); //安全函数,若表达式不成立结束程序
atomic<long> L;
cout << L <<endl; //初始值不确定
最重要的两个成员函数: store()
(operator=) 和 load()
(operator T() )以原子方式存取,不会因为并发访问导致数据不一致。
1 | boost::atomic<bool> b(1); |
测试代码中临界区非常短,只有一个语句,所以显得加锁解锁操作对程序性能影响很大,但在实际应用中,我们的临界区一般不会这么短,临界区越长,加锁和解锁操作的性能损耗越微小,无锁编程和有锁编程之间的性能差距也就越微小。
无锁编程最大的优势在于两点:
- 避免了死锁的产生。由于无锁编程避免了使用锁,所以也就不会出现并发编程中最让人头疼的死锁问题,对于提高程序健壮性有很大积极意义
- 代码更加清晰与简洁
参考:C++11多线程编程