前言
判断一个程序属于IO密集型还是计算密集型有利于后续对程序进行并行优化,尤其是Cpython解释器下的python程序(GIL锁)。本文将先介绍判断方法,再结合一个实际案例进行分析。
上下文切换
上下文切换一般指用户态和内核态间的切换,通常切换的发生是因为用户程序在运行过程中产生了系统调用。在Linux中,上下文切换可分为自愿上下文切换(voluntary_ctxt_switches)和非自愿上下文切换(nonvoluntary_ctxt_switches),自愿上下文切换如字面意思,是一种主动的行为,通常是程序主动地产生了系统调用行为,如IO操作,IO操作越多,自愿上下文切换次数越多。非自愿上下文切换和前者相反,它更多体现为“被切换”、“被中断”,我们知道操作系统会通过相应硬件产生时钟脉冲中断进行进程调度,那么一个程序计算得越久,它被时钟脉冲信号“中断”、被剥夺时间片给其他进程的次数就越多,因此非自愿上下文切换的次数越多,基于此我们就可以初略判断一个程序属于IO密集型还是计算密集型。
在Linux中,进程的上下文切换信息存储在/proc/${PID}/status文件中,${PID}表示当前进程的进程号,因此在目标程序运行结束前开一个子进程去找到有关ctxt_switches的信息就可以分析出当前进程属于IO密集型还是计算密集型。
来验证一下,我服务器搭了个NodeJs服务,FTP用得比较多,一般用来上传下载文件,目前运行了2年多左右,理论上应该属于IO密集型(云服务一般大量时间在监听wait,计算不密集)。NodeJs服务进程为8606,cat一下8606的status文件:
[root@izwz99t7wh8lhysugop73tt ~]# cat /proc/8606/status | grep ctxt
voluntary_ctxt_switches: 151581101
nonvoluntary_ctxt_switches: 42009
[root@izwz99t7wh8lhysugop73tt ~]#
可见自愿上下文切换数量远大于非自愿上下文切换的数量,因此NodeJS的FTP服务属于IO密集型,验证正确。
实际案例
对方用我写的python程序提取1个T的日志信息,一个程序大概要跑好几天,由于程序中涉及到IO操作,潜意识地认为1个T的IO提取任务应该是IO密集型,所以用多线程模型比较好(python一般IO密集型用多线程,计算密集型用多进程),但为验证直觉还是要进行实际测试。我这边提取程序为extract_new.py,在程序末尾添加两行代码即可:
import os
import subprocess
...
pid = os.getpid()
subprocess.call(['cat /proc/{}/status | grep ctxt'.format(pid)],shell=True)
subprocess表示开启子进程调用shell,voluntary_ctxt_switches和nonvoluntary_ctxt_switches会作为结果在最后输出。依葫芦画瓢,其他语言如java则可选择Runtime类进行实现。
对方一份日志大概近百万行,第一次实验generate几百行的模拟日志测试
[root@izwz99t7wh8lhysugop73tt extract]# python3 extract_new.py
当前进度:0.685%
...
当前进度:99.315%
当前进度:100.0%
voluntary_ctxt_switches: 32
nonvoluntary_ctxt_switches: 900
发现自愿上下文切换居然要比非自愿少这么多,难道提取程序属于计算密集型?我怀疑是模拟日志的行数太少导致IO未凸显出,于是我将日志行数调到20w行左右再进行测试:
[root@izwz99t7wh8lhysugop73tt extract]# python3 extract_new.py
当前进度:0.685%
...
当前进度:99.315%
当前进度:100.0%
voluntary_ctxt_switches: 16
nonvoluntary_ctxt_switches: 1120
实验结果显示差距更大了,属于典型计算密集型,个人猜测是因为程序中用到了正则等方法处理数据,虽然IO的数据变多但需计算处理的数据变得更多,从而导致上述结果。至此,我们就可以采用多进程的方式进行python任务并行提取了。