无锁编程最大的优势是什么?

无锁编程是一种并发编程的方法,它不使用传统的同步原语(如互斥锁、条件变量等),而是使用硬件支持的原子操作(如CAS)来实现对共享数据的安全访问。

无锁编程的优点是:

可以避免锁的开销和问题,如上下文切换、死锁、优先级反转等,提高程序的性能和可扩展性。

无锁编程的缺点是:

比较复杂和困难,需要对硬件和内存模型有深入的理解,同时需要处理一些特殊的情况,如ABA问题、内存回收、帮助策略等。

无锁编程的常见应用是无锁数据结构,如无锁队列、无锁栈、无锁哈希表等,它们可以在高并发的场景下提供高效的数据操作。

无锁编程最大的优势是什么?是性能提高吗?

其实并不是,我们的测试代码中临界区非常短,只有一个语句,所以显得加锁解锁操作对程序性能影响很大,但在实际应用中,我们的临界区一般不会这么短,临界区越长,加锁和解锁操作的性能损耗越微小,无锁编程和有锁编程之间的性能差距也就越微小。

我认为无锁编程最大的优势在于两点:

  1. 避免了死锁的产生。由于无锁编程避免了使用锁,所以也就不会出现并发编程中最让人头疼的死锁问题,对于提高程序健壮性有很大积极意义
  2. 代码更加清晰与简洁。对于一个多线程共享的变量,保证其安全性我们只需在声明时将其声明为原子类型即可,在代码中使用的时候和使用一个普通变量一样,而不用每次使用都要在前面写个加锁操作,在后面写一个解锁操作。

我写的C++期货高频交易软件中,有一个全局变量fund,存储的是当前资金量,程序采用线程池运行交易策略,交易策略中频繁使用到fund变量,如果采用加锁的方式,使用起来极其繁琐,为了保护一个fund变量需要非常频繁的加锁解锁,后来将fund变量改为原子类型,后面使用就不用再考虑加锁问题,整个程序阅读起来清晰很多。

多线程并发读写

在编写多线程程序时,最重要的问题就是多线程间共享数据的保护。多个线程之间共享地址空间,所以多个线程共享进程中的全局变量和堆,都可以对全局变量和堆上的数据进行读写,但是如果两个线程同时修改同一个数据,可能造成某线程的修改丢失;如果一个线程写的同时,另一个线程去读该数据时可能会读到写了一半的数据。这些行为都是线程不安全的行为,会造成程序运行逻辑出现错误。举个最简单的例子:

#include <iostream>
#include <thread>

using namespace std;

int  i = 0;
mutex mut;

void iplusplus() {
    int c = 10000000;  //循环次数
    while (c--) {
        i++;
    }
}
int main()
{
    thread thread1(iplusplus);  //建立并运行线程1
    thread thread2(iplusplus);  //建立并运行线程2
    thread1.join();  // 等待线程1运行完毕
    thread2.join();  // 等待线程2运行完毕
    cout << "i = " << i << endl;
    return 0;
}

上面代码main函数中建立了两个线程thread1和thread2,两个线程都是运行iplusplus函数,该函数功能就是运行i++语句10000000次,按照常识,两个线程各对i自增10000000次,最后i的结果应该是20000000,但是运行后结果却是如下:

i并不等于20000000,这是在多线程读写情况下没有对线程间共享的变量i进行保护所导致的问题。

有锁编程

对于保护多线程共享数据,最常用也是最基本的方法就是使用C++11线程标准库提供的互斥锁mutex保护临界区,保证同一时间只能有一个线程可以获取锁,持有锁的线程可以对共享变量进行修改,修改完毕后释放锁,而不持有锁的线程阻塞等待直到获取到锁,然后才能对共享变量进行修改,这种方法几乎是并发编程中的标准做法。大体流程如下:

#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
#include <chrono>

using namespace std;
int  i = 0;
mutex mut; //互斥锁

void iplusplus() {
    int c = 10000000;  //循环次数
    while (c--) {
        mut.lock();  //互斥锁加锁
        i++;
        mut.unlock(); //互斥锁解锁
    }
}
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;
}

代码14行和16行分别为互斥锁加锁和解锁代码,29行我们打印程序运行耗时,代码运行结果如下:

可以看到,通过加互斥锁,i的运行结果是正确的,由此解决了多线程同时写一个数据产生的线程安全问题,代码总耗时1.74121ms。

无锁编程

原子操作是无锁编程的基石,原子操作是不可分隔的操作,一般通过CAS(Compare and
Swap)操作实现,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的值是符合预期的,代码运行总耗时0.381574ms,仅为有锁编程的耗时1.74121ms的1/4,由此可以看出无锁编程由于避免了加锁而相对于有锁编程提高了一定的性能。

总结

如果是为了提高性能将程序大幅改写成无锁编程,一般来说结果可能会让我们失望,而且无锁编程里面需要注意的地方也非常多,比如ABA问题,内存顺序问题,正确实现无锁编程比实现有锁编程要困难很多,除非有必要(确定了性能瓶颈)才去考虑使用无锁编程,否则还是使用互斥锁更好,毕竟程序的高性能是建立在程序正确性的基础上,如果程序不正确,一切性能提升都是徒劳无功。

如果你想了解更多关于无锁编程的内容,你可以参考这些网页:

C++性能榨汁机之无锁编程 - 知乎
(2) 上篇|说说无锁(Lock-Free)编程那些事 - 知乎 - 知乎专栏. https://zhuanlan.zhihu.com/p/55178835.
(3) C++11原子操作与无锁编程 - 知乎 - 知乎专栏. https://zhuanlan.zhihu.com/p/24983412.
(4) C++性能榨汁机之无锁编程 - 知乎 - 知乎专栏. https://zhuanlan.zhihu.com/p/38664758.
(5) 上篇|说说无锁(Lock-Free)编程那些事 - 知乎 - 知乎专栏. https://zhuanlan.zhihu.com/p/55178835.
(6) C++11原子操作与无锁编程 - 知乎 - 知乎专栏. https://zhuanlan.zhihu.com/p/24983412.

  • 7
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

aFakeProgramer

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值