参考高并发之无锁编程
多线程并发
在高并发场景下往往需要用到多线程编程,又由于多个线程共享同一个进程中的地址空间,所以又可能会出现同时访问/修改同一个共享变量的情况,这就涉及到线程安全的问题,比如
- 两个线程同时修改同一个数据,可能造成某线程的修改丢失;
- 一个线程写的同时,另一个线程去读该数据时可能会读到写了一半的数据。
对此,我们可以用锁来解决线程安全的问题,在访问共享变量的代码的前后加上加锁和解锁的操作,从而保证同一时刻只有一个线程处于临界区中访问资源。但是除此之外,还有一种无锁编程的方式也可以解决线程安全的问题。
CAS
无锁编程是基于CAS(Compare And Swap)机制实现的,CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,一个经典例子是,CAS对内存地址V的值从A改成B,那么CAS总共有两步操作:
- 检查此刻内存地址V处的值是否还是旧值A。
- 如果是,将其修改为B;如果不是,说明有其他线程修改了内存地址V处的值,因此放弃修改。
如果修改失败,会继续尝试,这个操作也叫做自旋。由于CAS整体是一个原子操作,因此在CAS修改成功的情况下,能够保证是线程安全的。从思想上看,
- Mutex 属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守,所有企图访问变量的线程都需要经历加锁解锁的操作(即使没有人跟它竞争)。
- CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以直接尝试去修改,碰壁的情况下不断自旋尝试。
无锁编程实例
cankao C++性能榨汁机之无锁编程
原子操作是无锁编程的基石,原子操作是不可分隔的操作,一般通过CAS(Compare andSwap)操作实现,CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。
C++11的线程库为我们提供了一系列原子类型,同时提供了相对应的原子操作,原子类型的基本使用方法如下:
#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
#include <chrono>
using namespace std;
atomic<int> i = 0;
void iplusplus() {
int c = 10000000; //循环次数
while (c--) {
i++;
}
}
int main()
{
chrono::steady_clock::time_point start_time = chrono::steady_clock::now();//开始时间
thread thread1(iplusplus);
thread thread2(iplusplus);
thread1.join(); // 等待线程1运行完毕
thread2.join(); // 等待线程2运行完毕
cout << "i = " << i << endl;
chrono::steady_clock::time_point stop_time = chrono::steady_clock::now();//结束时间
chrono::duration<double> time_span = chrono::duration_cast<chrono::microseconds>(stop_time - start_time);
std::cout << "共耗时:" << time_span.count() << " ms" << endl; // 耗时
system("pause");
return 0;
}
代码的第8行定义了一个原子类型(int)变量i,在第13行多线程修改i的时候即可免去加锁和解锁的步骤,同时又能保证变量i的线程安全性。代码运行结果如下:
可以看到i的值是符合预期的,代码运行总耗时1.12731ms,仅为有锁编程的耗时3.37328ms的1/3,由此可以看出无锁编程由于避免了加锁而相对于有锁编程提高了一定的性能。
无锁编程优缺点
无锁编程的优点:
- 访问共享变量只需要一条语句(CAS原子操作),可以节省每次对共享变量进行操作都进行的加锁解锁动作,节省了系统开销。
- 并发竞争的时候使用自旋尝试的方法,避免了用锁的情况下线程因阻塞而频繁的切换。
无锁编程的缺点:
- 只能对简单的原子类型进行修改,不支持对大型的数据对象进行修改,或者说只能保证一条代码的原子性,不能保证代码块的原子性。
- 在并发量比较高的情况下,如果许多线程更新变量不成功,一直自旋反复尝试,会给CPU带来很大的负担。
无锁编程的扩展应用
那么这里就牵涉到一个问题了,在高并发场景下,如何高效地控制多个线程对一个很大的对象的访问呢?
- 使用无锁编程:无锁编程只能对简单的原子类型进行修改,不支持对大型的数据对象进行修改。
- 使用锁:修改一个很大的对象需要很长的一段代码和处理过程,如果写线程锁住这整个修改过程,那么会导致所有的读线程被阻塞很长一段时间。
当然如果这是一个高一致性的场景,也只能使用锁了。但是如果这并不是一个对一致性要求非常高的场景呢?比如可以允许修改内容在10s内生效而不是立即生效,那么就可以结合无锁编程使用一种高效的方法:
用指针来访问这个共享对象,写线程修改对象时,不直接在对象上进行修改,而是新建一个对象进行赋值,赋值完成后,再通过CAS将指针指向这个新的对象,然后销毁旧的对象。通过这种方法,可以将临界区从一大段代码缩减为一句代码,极大的减少了对读线程的影响。
修改指针这一步只有一条代码语句,虽然也可以用加锁的方式来保证线程安全,但是恰好无锁编程也是只能对一条语句进行操作,所以无锁编程非常符合这种方式,往往一想到无锁编程就想到这种方法。
今天的文章无锁编程的常用方法_Ds5换方向锁要编程吗[通俗易懂]分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:http://bianchenghao.cn/77070.html