C++性能优化实践 三

C++性能优化实践 三


 书接上回, 这篇文章继续来谈谈C++ 并发编程性能优化相关的内容。

参考文章: hhttps://boolan.com/
先形象的说明一下并发与并行:
并发:类似与足球场踢足球, 大家为了抢一个球(数据)可能会发生碰撞, 足球在某一时刻只能在一个球员手里, 所以为了避免恶意抢球就需要裁判(数据同步), 最后球在谁手里由上帝(CPU)决定。
并行:类似于刘翔跨栏时的场景, 大运动员之间各自拼命跑, 互相之间不共享东西(数据)。

一、C++标准库线程间的通信

  这里首先纠正自己之前存在的一个知识点误区, 就是关于 std::condition_variable_any 的。先看如下代码:

void WorkFunc(int& result, std::condition_variable_any& cv, std::mutex& cv_mut, bool& ready_flag)
{
    result = 100;
    {
        std::unique_lock w_lock(cv_mut);
        ready_flag = true;
    }
    
    cv.notify_one();
    return;
}

int main()
{
    std::condition_variable_any cv;
    std::mutex cv_mut;
    bool ready_flag{false};
    int result{0};

    std::thread workth(std::ref(result), std::ref(cv), std::ref(cv_mut), std::ref(ready_flag));
    std::unique_lock r_lock(cv_mut);
    cv.wait(r_lock, [&ready_flag](){return ready_flag;});
    std::cout<<"result is:"<< result <<std::endl;
    
    if (workth.joinable()){
        workth.join();
    }
    
    return 0;
}       

 在早期我一直认为线程函数 WorkFunc() 内将 std::condition_variable_any cv 条件变量需要的 ready_flag 标志置为 true 是不需要加锁的, 当时以为条件变量的 wait() 函数会先循环查询 ready_flag 这个标志位为真后再去判断是否有信号量的通知, 所以就不用加锁, 看来还是太年轻了, 理解不到位。
 而实际情况是 wait() 函数先判断一次标志位, 然后就直接调用实际的 wait()。在复杂时序的情况下可能会出现在检测完 ready_flag 标志为 false 后, 线程函数先执行完了, 主线程再去调用实际的 wait(), 这个时候因为错过了通知就死锁了。 标志位的作用: ①避免错过 notify_one()通知; ②避免假醒。
 还有就是标准库线程类的构造函数都是值传参, 不会因线程函数的形参都是引用就推导出引用类型, 所有这个需要加上 std::ref

二、内存屏障、获得与释放语义

 为了保证多线程情况下对数据计算的正确性, 一方面当然是使用 std::mutex 来保证数据的同步, 此外, 某些数据较为简洁的应用场景则可以使用标准库提供的原子类型, 比如通知线程退出的标志位。 这里记录一下关于原子类型标准库提供的相关内存模型:

//保证当前语句之后的所有读写操作不乱序到当前语句之前
std::memory_order_acquire;

//保证当前语句之前的所有读写操作不乱序到当前语句之后
std::memory_order_release;

//松散类型, 只保证当前语句对数据操作的原子性, 没有任何内存屏障
std::memory_order_relaxed;

//同时拥有 std::memory_order_acquire 和 std::memory_order_release 的特点
std::memory_order_acq_rel;

//同时拥有 std::memory_order_acquire 和 std::memory_order_release 的特点外, 同时还拥有全局的内存顺序, 保证所有线程的执行顺序一致
std::memory_order_seq_cst   

关于内存序的典型应用场景就是单例模式里面创建实例时的双重检查锁定, 原始代码如下:

//线程非安全版本
Singleton* Singleton::getInstance() {
    if (m_instance == nullptr) {
        m_instance = new Singleton();
    }
    return m_instance;
}

//线程安全版本, 但锁的代价过高
//当对象被创建后, 其他所有线程其实就不需要再等待持有锁了
Singleton* Singleton::getInstance() {
    Lock lock;
    if (m_instance == nullptr) {
        m_instance = new Singleton();
    }
    return m_instance;
}


//双检查锁, 但由于内存读写reorder不安全
Singleton* Singleton::getInstance() {
    
    if(m_instance==nullptr){
        Lock lock;
        if (m_instance == nullptr) {
            m_instance = new Singleton();
        }
    }
    return m_instance;
}

双检查实现里面, 写代码时期望的执行顺序是先申请一块内存, 到后调用 Singleton() 构造函数, 最后将内存地址赋值给 m_instance。但是编译器出于优化的目的, 实际的顺序可能是 先申请一块内存, 然后将内存地址赋值给 m_instance, 最后再构造。 此时其他线程进函数后发现 m_instance 不为空, 然后直接返回, 此时这个实例是没有初始化的, 就可能会出现问题。所以安全的实现如下:

std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;

Singleton* Singleton::getInstance() {
    Singleton* tmp = m_instance.load(std::memory_order_relaxed);

    //获取内存fence
    std::atomic_thread_fence(std::memory_order_acquire);
    
    if (tmp == nullptr) {
        std::lock_guard<std::mutex> lock(m_mutex);
        tmp = m_instance.load(std::memory_order_relaxed);
        if (tmp == nullptr) {
            tmp = new Singleton;

            //释放内存fence
            std::atomic_thread_fence(std::memory_order_release);

            m_instance.store(tmp, std::memory_order_relaxed);
        }
    }
    return tmp;
}


//或者不用 fence 直接用获得释放语义实现
Singleton* Singleton::getInstance() {

    Singleton* tmp = m_instance.load(std::memory_order_acquire);
    if (tmp == nullptr) {
        std::lock_guard<std::mutex> lock(m_mutex);
        tmp = m_instance.load(std::memory_order_relaxed);
        if (tmp == nullptr) {
            tmp = new Singleton;
            m_instance.store(tmp, std::memory_order_release);
        }
    }
    return tmp;
}

这样就可以保证申请内存, 构造再赋值的执行顺序。

三、多线程优化总结

 首先需要知道的是, 多线程加锁和数据竞争是性能杀手。有以下几点需要注意:
①能用 std::atomic 原子类型就不要使用 std::mutex;
②如果多线程读比写多很多时, 优先考虑使用读写锁 std::shared_mutex, 其他情况还是使用 std::mutex;
③考虑使用 thread_local 变量, 这个相当于不需要加锁的全局变量, 当线程第一次访问的时候对象才会被创建, 线程退出时对象就会被销毁;

④能用标准库里面的高级接口就不要自己写, 比如 std::future, std::async等;

  • 12
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
《C性能优化指南》是一本涵盖了C语言程序性能优化的重要知识和技巧的PDF文档。本指南旨在帮助开发人员提高他们编写和优化C语言程序的能力,以达到更高的性能和效率。 首先,在《C性能优化指南》中,作者详细介绍了C语言程序性能优化的重要性和意义。他们解释了性能优化对程序速度、内存占用和功耗的影响,以及对用户体验和系统资源利用率的重要性。 其次,本指南列举了一系列常见的性能优化技巧和最佳实践,包括优化代码结构、减少内存占用、减少函数调用次数、使用高效的数据结构和算法等方面。这些技巧都是基于C语言的特性和机制,针对C语言程序的性能瓶颈进行了详细的介绍和讲解。 另外,本指南还提供了大量实用的示例代码和性能优化案例分析,帮助读者更好地理解和应用性能优化技巧。这些案例涉及到不同类型的C语言程序,涵盖了计算密集型、内存密集型和I/O密集型等不同场景和需求。 最后,在《C性能优化指南》中,还包括了一些常见的工具和技术,比如性能分析工具、调试工具和代码优化器等,帮助开发人员更方便地进行性能优化和测试。 总之,这本PDF文档为开发人员提供了丰富的C语言程序性能优化知识和实用技巧,是一本深入浅出、实用性强的性能优化指南。阅读本指南可以帮助开发人员更好地提升C语言程序的性能和效率,提高程序的质量和用户体验。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值