联系我们
简单又实用的WordPress网站制作教学
当前位置:网站首页 > 程序开发学习 > 正文

速度优化:任务调度优化

作者:访客发布时间:2023-10-26分类:程序开发学习浏览:98


导读:前面讲过,任务调度是影响速度的本质因素之一,这一章我们就来讲讲任务调度的优化.针对任务调度,我们很自然地就能想到:提高任务的优先级或者减少任务调度的耗时这两条优化方法论那就是。减少...

前面讲过,任务调度是影响速度的本质因素之一,这一章我们就来讲讲任务调度的优化.

针对任务调度,我们很自然地就能想到:提高任务的优先级或者减少任务调度的耗时这两条优化方法论那就是。减少调度耗时有不少优化方案,比如线程保活和使用协程等,这些我们都在前面的章节中讲过了,也就不再重复了.

这一章,我们就围绕如何提高任务的优先级,来介绍2种优化方案:

  1. 提升核心线程的优先级;

  2. 核心线程绑定cpu大核。

提升核心线程优先级

想要提升线程的优先级,我们需要先了解线程优先级这一概念的原理.第二章我们讲过、linux中的进程分为实时进程和普通进程这两类。实时进程一般通过实时优先级值来描述优先级,取值范围是0到99。普通进程一般使用Nice值来描述进程的优先级,取值范围是-20到19那就是。但是为了架构设计上的统一,LINUX系统会将Nice对齐成Prio值,即Nice取-20时,该进程的Prio值为0,此时它的优先级依然比任何一个实时进程的优先级都要低那就是。由于线程的本质就是进程,因此上述优先级规则也适用于线程.

我们可以通过进入手机的Shell界面,执行PS-p-t(高版本系统上这个指令可能失效了)查看所有进程的Nice值和Prio值.部分数据如下:

ps -p -t 

USER      PID  PPID     VSIZE  RSS   PRIO  NICE  RTPRI SCHED   WCHAN    PC          NAME
root      393   1     1554500 5256    20    0     0     0     ffffffff 000 S       zygote
system    762   328   338336  9844    12    -8    0     0     ffffffff 00000000 S surfaceflinger
……
//测试demo的主线程和渲染线程
u0_a45    16632 393   2401604 60140   20    0     0     0     ffffffff 00000000 S com.example.test
u0_a45    16725 16632 2401604 60140   16    -4    0     0     ffffffff 00000000 S RenderThread
……

在安卓中只有部分底层核心进程才是实时进程,如界面、音频等进程,大部分的进程都是普通进程,从上面数据也可以看到,我们演示主线程的Nice值默认为0,渲染线程的Nice值默认值为-4。我们没法将普通进程调整成实时进程,也没法将实时进程调整成普通进程,只有操作系统有这个权限.但有一个例外,在根手机中,将/系统目录下的构建.prop文件中的sys.Use_FIFO_UI字段修改成1,就能将应用的主线程和渲染线程调整成实时进程、不过这需要Root设备才能操作,正常设备这个值都是0,方案不具备通用性,就不展开讲了.

应用中的所有线程都属于普通进程的级别,所以针对线程优先级这一点,我们唯一能操作的就是修改线程的Nice值了,并且我们有两种方式来调整线程的Nice值.

调整线程优先级的方式

我们有2种方式改变线程的NICE值:

  1. Process.setThreadPriority(int优先级)/Process.setThreadPriority(int PID,int优先级);

  2. Thad.setPriority(整型优先级)。

第一种方式是安卓系统中提供的接口接口。入参Pid就是线程id,也可以不传,会默认为当前线程,入参优先可以传-20到19之间的任何一个值,但建议直接使用安卓提供的优先定义常量,这样我们的代码具有更高的可读性,如果直接传我们自定义的数字进去,不利于代码的理解。

系统常量NICE值使用场景
Process.THREAD_PRIORITY_DEFAULT0默认优先级
Process.THREAD_PRIORITY_LOWER19个最低优先级
Process.THREAD_PRIORITY_BACKGROUND10后台线程建议优先级
Process.THREAD_PRIORITY_LESS_PROCESSIVE1比默认略低
Process.THREAD_PRIORITY_MORE_PROCESSIVE-1比默认略高
Process.THREAD_PRIORITY_FORTROUND-2前台线程优先级
Process.THREAD_PRIORITY_DISPLAY-4显示线程建议优先级
Process.THREAD_PRIORITY_URGRENT_DISPLAY-8显示线程的最高级别
Process.THREAD_PRIORITY_AUDIO-16音频线程建议优先级
Process.THREAD_PRIORITY_URGRENT_AUDIO-19音频线程最高优先级

在不进行调整前,我们主线程的尼斯值默认为0,渲染线程的默认尼斯值为-4。音频线程建议是最高级别优先级,因为如果音频线程优先级太低,就会出现音频播放卡顿的情况.

第二种方式是JAVA提供的API接口、JAVA有自己对线程优先级的定义和规则,但是最后都会将这些规则转换成对应的Nice值大小。JAVA线程提供的优先级以及转换成Nice值的规则如下:

常量值NICE值对应安卓系统应用编程接口
线程.MAX_PRIORITY10-8线程优先级紧急显示
线程.最小优先级019个线程优先级最低
线程.NORM_PRIORITY5.0线程优先级默认

第二种方式能设置的优先级较少,不太灵活,并且因为系统的一个时序问题Bug,在设置子线程的优先级时,可能因为子线程没创建成功而设置成了主线程的,会导致优先级设置异常,所以这里建议使用第一种方式来设置线程的优先级,避免使用第二种方式.

需要调整优先级的线程

了解了调整线程优先级的方式,我们再看看哪些线程需要调整,主要有两类:主线程和渲染线程(渲染线程)那就是。

为什么要调整这两个线程呢?因为这两个线程对任何应用来说都非常重要.从Android 5开始,主线程只负责布局文件的Measure和Layout工作,渲染的工作放到了渲染ls线程,这两个线程配合工作,才让我们应用的界面能正常显示出来.所以通过提升这两个线程的优先级,便能让这两个线程获得更多的中央处理器时间,页面显示的速度自然也就更快了那就是。

主线程的优先级好调整,我们直接在应用程序的附加生命周期中,调用进程。set线程优先级(-19),将主线程设置为最高级别的优先级即可那就是。但是Render线程怎么调整呢?这时我们需要知道Render线程的线程id,然后依然调用Process.setThreadPriority就可以了。下面我们就一起看一下如何找到渲染线程的线程PID。

应用中线程的信息记录在/PROC/PID/TASK的文件中,可以看到TASK文件中记录了当前应用的所有线程。以11548这个进程的数据为例,数据如下:

/proc/11548/task $ ls
11548  11554  11556  11558  11560  11564  11566  12879  12883  12890  12917  14501  14617  15596  15598  15600  15602  15614
11553  11555  11557  11559  11562  11565  12878  12881  12884  12894  12920  14555  15585  15597  15599  15601  15613  15617

我们接着查看该目录里线程的Stat节点,就能具体查看到线程的详细信息,如Name、Pid等等。11548进程的主线程id就是11548,它的Stat数据如下:

blueline:/proc/11548/task $ cat 11548/stat                                                                                                                                                                                             
11548 (ndroid.settings) S 1271 1271 0 0 -1 1077952832 12835 0 1617 0 52 19 0 0 10 -10 36 0 59569858 15359959040 23690 18446744073709551615 1 1 0 0 0 0 4612 1 1073775864 0 0 0 17 4 0 0 0 0 0 0 0 0 0 0 0 0 0

在第十章中,我们已经详细介绍了Stat数据中每个参数的含义,如果记不清了可以看看前面的知识点。上面的数据中,第一个参数是ID、第二个参数是名称。

所以我们只需要遍历这个文件,查找名称为“Render”的线程,就能找到渲染线程的PID了。那么下面就看一下具体的代码如何实现吧.

public static int getRenderThreadTid() {
    File taskParent = new File("/proc/" + Process.myPid() + "/task/");
    if (taskParent.isDirectory()) {
        File[] taskFiles = taskParent.listFiles();
        if (taskFiles != null) {
            for (File taskFile : taskFiles) {
                //读线程名
                BufferedReader br = null;
                String cpuRate = "";
                try {
                    br = new BufferedReader(new FileReader(taskFile.getPath() + "/stat"), 100);
                    cpuRate = br.readLine();
                } catch (Throwable throwable) {
                    //ignore
                } finally {
                    if (br != null) {
                        br.close();
                    }
                }

                if (!cpuRate.isEmpty()) {
                    String param[] = cpuRate.split(" ");
                    if (param.length < 2) {
                        continue;
                    }

                    String threadName = param[1];
                    //找到name为RenderThread的线程,则返回第0个数据就是 tid
                    if (threadName.equals("(RenderThread)")) {
                        return Integer.parseInt(param[0]);
                    }
                }
            }
        }
    }
    return -1;
}

当我们拿到渲染线程的PID后,同样调用Process.setThreadPriority(PID,-19)将渲染线程设置成最高优先级即可。

当然,我们要提高的优先级线程并非只有这两个,我们可以根据业务需要,来提高核心线程的优先级,同时降低其他非核心线程的优先级,该操作可以在线程池中通过线程工厂来统一调整.提高核心线程优先级,降低非核心线程优先级,两者配合使用,才能更高效地提升应用的速度.

核心绑定中央处理器大核

接着,我们来看第二种优化方案:绑定cpu大核。这种方案虽然和操作系统的任务调度关系不大,但也属于一种提升线程优先级的方案,只不过它提升的是线程运行在性能更好的cpu上的优先级。

目前手机设备的CPU都是多核的,如下图的骁龙888CPU这款就有8个核,其中大核的性能是最好的,时钟周期频率为2.84 GHZ,其他的核性能都要差很多。

最差的核只有1.8 GHz的时钟周期频率,如果用它来执行我们核心线程的任务,性能就会差很多。这主要体现在主线程和渲染线程上,页面的显示速度会变慢.所以,如果我们能将核心线程绑定在大核上,那么应用的速度就会提升很多那就是。

线程绑核方案

线程绑核并不是很复杂的事情,因为linux系统有提供相应的接口接口,系统提供的p线程_集合亲和力_NP和SCHRED_集合亲和力这里两个函数,都能实现线程绑核。但是在安卓系统中,限制了p线程_集合亲和力_NP函数的使用,所以我们只能通过SCHRED_集合亲和力函数来进行绑核操作。

#INCLUDE<;Schedul.h>;

Int sched_集合亲和力(id_tPID,大小_tCpuset大小**,cpu_set_t*遮罩);

第一个入参是线程的PID、如果PID的值为0、则表示指定的是主线程

第二个入参CpusetSize是掩码所指定的数的长度

第三个入参是需要绑定的中央处理器序列的掩码

下面,我们就一起来看一下,如何通过这个函数实现线程绑核的操作.

void bindCore(){
    cpu_set_t mask;     //CPU核的集合     
    CPU_ZERO(&mask);     //将mask置空     
    CPU_SET(0,&mask);    //将需要绑定的cpu核设置给mask,核为序列0,1,2,3……     
    if (sched_setaffinity(0, sizeof(mask), &mask) == -1){     //将线程绑核
         printf("bind core fail");
    }
}

我们可以看到,将主线程绑定序列为0的操作只要数行代码就能实现了。实现起来很简单,只需要在本机层指定需要绑定线程的ID、核序列的掩码掩码、作为入参调用Sched_set亲和力函数即可。

可我们需要的是将主线程和渲染线程绑定大核,所以上面的代码并不完整,我们还需要传入正确的Pid和核序列。主线程的PID入参传0、渲染线程的PID也知道如何获取了,现在就差找到那个性能最高的大核了。

获取大核序列

我们可以通过/sys/Device/System/cpu/目录下的文件,查看当前设备有几个cpu.用来测试的是一台像素3、可以看到有8个处理器、也就是8核。

/sys/devices/system/cpu $ ls 
core_ctl_isolated  cpu1  cpu3  cpu5  cpu7     cpuidle                hang_detect_gold    hotplug   kernel_max  offline  possible  present
cpu0               cpu2  cpu4  cpu6  cpufreq  gladiator_hang_detect  hang_detect_silver  isolated  modalias    online   power     uevent

然后进入到cpuX/cpufreq文件,查看具体序列的cpu详情。

/sys/devices/system/cpu/cpu0/cpufreq $ ls
affected_cpus     cpuinfo_max_freq  cpuinfo_transition_latency  scaling_available_frequencies  scaling_boost_frequencies  scaling_driver    scaling_max_freq  scaling_setspeed  stats
cpuinfo_cur_freq  cpuinfo_min_freq  related_cpus                scaling_available_governors    scaling_cur_freq           scaling_governor  scaling_min_freq  schedutil

这个文件中的cpuinfomax_freq节点就是当前cpuinfomax_freq cpufo的时钟周期频率。下面就是Piex13骁龙845芯片的每个核的时钟周期频率。

/sys/devices/system/cpu $ cat cpu0/cpufreq/cpuinfo_max_freq                                                                                                                                                                   
1766400
/sys/devices/system/cpu $ cat cpu1/cpufreq/cpuinfo_max_freq                                                                                                                                                                   
1766400
/sys/devices/system/cpu $ cat cpu2/cpufreq/cpuinfo_max_freq                                                                                                                                                                   
1766400
/sys/devices/system/cpu $ cat cpu3/cpufreq/cpuinfo_max_freq                                                                                                                                                                   
1766400
/sys/devices/system/cpu $ cat cpu4/cpufreq/cpuinfo_max_freq                                                                                                                                                                   
2803200
/sys/devices/system/cpu $ cat cpu5/cpufreq/cpuinfo_max_freq                                                                                                                                                                   
2803200
/sys/devices/system/cpu $ cat cpu6/cpufreq/cpuinfo_max_freq                                                                                                                                                                   
2803200
/sys/devices/system/cpu $ cat cpu7/cpufreq/cpuinfo_max_freq                                                                                                                                                                   
2803200

可以看到,4、5、6、7序列都是大核.如果查看845的参数,也可以发现是符合这个特性的。

所以我们在代码实现中,只需要遍历/sys/Device/System/cpu/目录下的cpuinfomax_freq节点,然后读取节点下的就能找到大核了。下面就看一下如何实现吧.

  1. 统计该设备cpu有多少个核。
public static int getNumberOfCPUCores() {
    int cores = new File("/sys/devices/system/cpu/").listFiles((file) -> {
        String path = file.getName();
        if (path.startsWith("cpu")) {
            for (int i = 3; i < path.length(); i++) {
                if (path.charAt(i) < '0' || path.charAt(i) > '9') {
                    return false;
                }
            }
            return true;
        }
        return false;
    }).length;

    return cores;
}
  1. 遍历每个核,找出时钟频率最高的那个核.
public static int getMaxFreqCPU() {
    int maxFreq = -1;
    try {
        for (int i = 0; i < getNumberOfCPUCores(); i++) {
            String filename = "/sys/devices/system/cpu/cpu" + i + "/cpufreq/cpuinfo_max_freq";
            File cpuInfoMaxFreqFile = new File(filename);
            if (cpuInfoMaxFreqFile.exists()) {
                byte[] buffer = new byte[128];
                FileInputStream stream = new FileInputStream(cpuInfoMaxFreqFile);
                try {
                    stream.read(buffer);
                    int endIndex = 0;
                    //Trim the first number out of the byte buffer.
                    while (buffer[endIndex] >= '0' && buffer[endIndex] <= '9'
                            && endIndex < buffer.length) endIndex++;
                    String str = new String(buffer, 0, endIndex);
                    Integer freqBound = Integer.parseInt(str);
                    if (freqBound > maxFreq) maxFreq = freqBound;
                } catch (NumberFormatException e) {
                   
                } finally {
                    stream.close();
                }
            }
        }
    } catch (IOException e) {
        
    }
    return maxFreq;
}

至此,我们便找出了大核的序列,然后将大核序列以及线程PID传入本机层,调用Sched_set亲和力进行绑核即可。当然,我们也可以直接在本机通过C++代码来解析文件获取渲染线程和大核,这样效率会更好一些,具体代码就不在这儿实现了。除了主线程和渲染线程,我们也可以根据业务需要,将其他核心线程绑定大核,比如上面提到的骁龙845有四个大核,我们就可以每个大核都绑定一个核心线程。

当我们通过上面的逻辑将主线程和渲染线程绑定大核后,可以通过sched_GetAffiliation函数或者通过ps-p-t等命令查看线程运行在在哪个核上,以此来确认是否绑定成功。到这里,你就学会了如何将线程绑定大核这一优化方案了,可以在课后试一试,看看绑定大核后,应用的启动速度和页面的打开速度提升了多少.

小结

这一章介绍的两种方案的代码实现都不难,很容易落地.但是我们真正需要掌握的不仅仅是这两种方案的实现,还有能诞生出这两种优化方案的方法论,即提高任务调度优先级的方法论.

那除了能想到本章中提到的两种方案,大家可以想想还有哪些方案能提升优先级.比如:替换或者优化调度算法,将普通进程变成实时进程,等等.只要我们能从原理出发,自下而上地思考,就一定能产生源源不断的灵感和思路!


标签:速度


程序开发学习排行
最近发表
网站分类
标签列表