算法优化之c++多线程优化:思考与总结

最近的项目中要用多线程来对代码进行优化,期间查阅了一些资料,主要是踩过一些坑,在此记录一下,给自己提个醒。

1.什么是多线程优化

首先我们要知道什么是线程,这点没有谁比维基百科说的更好了,直接点击查看线程英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

正因为线程是调度的最小单位,控制好了线程,也就相当于控制好了计算资源。之所以叫计算资源,是因为我们不确定程序运行在什么类型的硬件设备上,多线程优化这个概念,更多的是强调资源的高利用率,而不是低使用率,即每份计算资源都能被充分的利用,这才是优化的本质,而并不是要强行减少资源的使用。多线程优化,就是利用多线程来并行处理多个任务,提高计算资源的利用率,从而提升系统效率。

2.为什么多线程能够提高利用率

刚才我们有了多线程优化的概念,里面提到关键一点是计算资源的高利用率,那么为什么多线程能够提高利用率呢?是不是只要加上多线程,程序就跑得快了呢?显然不是这样的,这里面还涉及到什么样的任务适合多线程,我们留到后面再说,现在只是说说多线程相较于单线程,优化了什么。现在的通用硬件设备大多都是单CPU多处理核心的架构,比如iPhone X的A11芯片的CPU部分为六核心设计,由2颗代号为“季风”(Monsoon)的高性能核心及4颗代号为“西北风”(Mistral)的节能核心。多核心意味着CPU能同时进行多个“最小单位”的调度和处理,这个最小单位就是我们前面提到的线程。换句话说,多核心使得多个线程能够同时运行,达到并行的效果。

举个例子,一个班40个同学春游,如果只有一辆双人车,那么不算司机一次只能运1人,一共需要40次才能运完,如果同时有4辆车同时运,那么只需要10次就能运完。类比到计算任务,运40个人就是总体任务,车就是线程,“双人”是线程的处理逻辑,如果单一线程,那么一共需要40次处理才能完成任务,而我们刚才反复说到线程是最小调度单元,也就是说,对于这个任务,单一线程意味着CPU每时每刻都只有一个核心在工作,其他核心都在睡觉,即使把这个核心累死,也就只能开一辆车,这样效率是不高的。如果有四辆车,也就是同时有4个核心在工作,显然系统的计算资源得到了更好的利用。

例子讲完了,肯定有人会问,为什么一辆车只能坐一个人呢,我为什么不找一辆7座的,一次运6个同学多好。这是一个很好的问题,问题的核心在于,提高单一线程的处理效率,这是在多线程优化之前就应该完成的任务,可以通过更好的逻辑设计以及单指令多数据(SIMD)来进行优化,提高单一线程的处理能力,根据实际情况,可以升级为4座甚至7座车,这点可以参考SIMD相关资料。那还有人会问,为什么我不多叫几辆车,叫40辆双人车,一次运完,岂不更省事?这个想法很直接很美好,但是往往我们找不到这么多车,经费不够!类比到计算设备上,就是CPU核心数不会太多,受限于材料、工艺和能耗,CPU的核心数会有限制,像我们提到的iPhone X,也就6个核心,所以40辆车是不可能了。那6辆车呢?不是6个核心吗,6辆车,不是刚好吗?这里的确存在一个问题,6核心运行6线程是否比6核心运行4线程好。这个答案是不确定的,与计算设备的状态有关,因为我们知道,计算设备不可能同时只处理一个任务,除了你安排的任务,还有其他人安排的任务,以及它为了维护自身正常运行所进行的任务,所以线程的数量并不是越多越好,要根据实际情况进行测试得到最优结果。

对于典型可分离的任务,如春游运同学,多线程能够使得CPU的多个核心同时处理任务,从而提高CPU的利用率,来提升性能。

3.什么任务适合多线程

下雨了,双人车不挡雨,同学都需要雨衣,否则没法出去,刚才提到的40个同学里面,只有39件雨衣,剩下一个倒霉蛋李明没有,但是,韩梅梅愿意和李明共享一件雨衣,解决出行难题,一共10里路,他们每走一段,就换一个人穿雨衣,剩下的那个就只能等。本来很快的一段行程,因为他们俩需要中途等雨衣,导致他们比其他同学迟到了一个小时。他们俩都说,下次如果再下雨,我就等你到了再出发,也比这样中途换雨衣方便。同样的任务,因为存在耦合以及数据竞争,导致任务执行的效率下降,例子中的雨衣就是两个任务都要访问的全局变量,如果俩人同时抢雨衣,那么雨衣很可能会被撕坏,系统会出现各种未定义的行为,主要以段错误和内存错误为主,但实际上是因为抢雨衣导致的。这种耦合任务,多线程执行效率可能比单线程只想两次还要低。

任务间耦合度高、独立性低的,不适合多线程。

那是不是说这种情况就没有办法优化了呢?也不是。李明和韩梅梅的雨衣比较高级,可以分成两半,而且还可以再接起来,再加上他俩感情深厚,于是决定每人只要一半,哪怕会淋点与,但是也无所谓,到了目的地再把雨衣拼成完好的一件,并不影响结果。在多线程里,如果一个全局变量被每条线程都访问,那么可以用局部变量来代替全部变量作为参数传入多线程中,在计算完成之后,统一恢复到全局变量中。

第二次春游,学校统一组织,400人,找了个很近的地方,双人车过去只要1分钟,还不如上下车换人的时间长,还是之前的车,还是之前的人。但是这次春游的组织却让大家都很头疼,因为时间全浪费在上下车换人上了,每个人在车上只坐一分钟,而上下车换人可能都不止这点时间,不停的在上下人,导致现场非常拥挤。

如果单次任务执行时间非常短,而循环次数又非常多,不适合多线程。

这个时候,可以从逻辑上将任务拆分,拆成几个独立的任务,再使用多线程。

4.多线程使用方法

前面讲了一些小故事,都是瞎扯淡,现在才是干货。

多线程的使用,请直接参考破晓的博客,以及程序员的自我修养系列

5.线程池的使用

又是干货,请参考100行c11线程池,里面有例子,可以直接运行:

#include <iostream>
#include <vector>
#include <chrono>

#include "ThreadPool.h"

int main()
{
    
    ThreadPool pool(4);
    std::vector< std::future<int> > results;

    for(int i = 0; i < 8; ++i) {
        results.emplace_back(
            pool.enqueue([i] {
                std::cout << "hello " << i << std::endl;
                std::this_thread::sleep_for(std::chrono::seconds(1));
                std::cout << "world " << i << std::endl;
                return i*i;
            })
        );
    }

    for(auto && result: results)
        std::cout << result.get() << ' ';
    std::cout << std::endl;
    
    return 0;
}

6.一点思考

1)除了逻辑和语法错误,多线程的错误都与数据竞争与线程安全有关,所以遇到任何莫名其妙的问题,首先静态检查代码,确保线程安全

2)不要害怕加锁和解锁,不管是互斥锁std::mutex,还是std::unique_lock,它们的效率都很高,上万次加锁解锁操作,也不会操作1毫秒

3)不要胡乱加锁,虽然加锁解锁操作本身不耗时,但是锁住的是一个关键数据或者关键过程,那么必然将影响程序性能,比如整个函数就是为了计算残差,而你为了线程安全,把残差的实际计算过程加了锁,整个多线程任务变成了等待锁的单线程,得不偿失

4)如果线程池效率不高,有两点要考虑,1是任务是否适合多线程,2是能不能把锁去掉,比如用局部变量代替全局变量

5)尽可能把线程对象的初始化弄得简单点,不要太频繁的初始化线程然后又释放线程,最好是放在系统的初始化接口中

6)openMP还有tbb还有GCD等多线程框架的实现已经很好,如果能使用,尽量学会使用这些库,一般会比自己管理好一点,如果是编程大牛,请忽略

7)多线程与异步计算的关系密切,一般可以使用异步计算的,都可以用多线程来实现,而多线程加速也依赖于异步计算

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值