python多核计算的那些坑和计算效率考量
- 基本配置
- 关于multiprocessing的大大小小坑
- 代码规划注意事项
- 计算效率的关键点
- **欢迎访问本人的知乎 ## [纳米小新的左右脑](https://www.zhihu.com/people/nanoxiaoxin)**
最近从处理股票日频、分钟频 转到 处理tick级数据,发现单核计算的能力已经跟不上现在的需求。转向在服务器上做并行计算。亲自捣鼓了一圈,总结一下碰到的各种坑,以及性能提升的一些关键点。大体上按照简单到复杂排序,讲干货:
基本配置
服务器centos7 + Mysql提供tick等行情与基本面数据服务 + Python 3.6 【multiprocessing+sqlalchemy+pandas】+ 本地局域网
关于multiprocessing的大大小小坑
1、安装multiprocessing,代码不是
pip install multiprocessing
而是
pip install multiprocess
2、multiprocessing不支持多参数输入,有人用迭代器,但本人感觉不好用,推荐基于multiprocessing的pathos!
from pathos.multiprocessing import ProcessingPool as Pool
pool = Pool(core_nmuber)
result = pool.map(function,[para1,]*core_nmuber,L_para2)
3、区分multiprocessing的同步、异步和阻塞。如果你想阻塞主进程,推荐您首先研究pool.map,本人觉得它是multiprocessing/pathos中最方便的。
import time
def f(x):
for j in range(2):
print(x*x)
time.sleep(1)
pass
return ('it is ',x)
from multiprocessing import Pool
if __name__ == '__main__':
p = Pool(3)
p_return = p.map(f, range(5)) # 三个子进程同时运行0,1,2;然后剩余两个迭代3,4由最先完成的两个子进程来处理
print(p_return)
代码规划注意事项
因为计算量大,一次测试就会等待很久,所以有必要在编写代码时提前布局。
1、 因为multiprocessing默认不实现变量共享,变量共享会比较复杂,潜在的坑肯定不少。所以推荐直接将单核计算的所有函数打包成一个总函数,然后将总函数+各类参数输入到multiprocessing中。
2、multiprocessing的子进程抛出的异常不会自动输出到控制台,需要我们自己捕获异常,输出到控制台或是日志,而且一般计算涉及多个子函数以及多次循环。所以最好给函数增加报错功能以及错误后继续运行的功能。建议以装饰器的方式批量添加【参见下面代码】,而且为需要检测的各个子函数都添加。
import logging
import functools
def create_error_log():
logger = logging.getLogger("error")
logger.setLevel(logging.INFO)
fh = logging.FileHandler("./cal_fun.err")
fmt = "\n[%(asctime)s-%(name)s-%(levelname)s]: %(message)s"
formatter = logging.Formatter(fmt)
fh.setFormatter(formatter)
logger.addHandler(fh)
return logger
def create_normal_logger():
logger = logging.getLogger("cal_fun_logger")
logger.setLevel(logging.INFO)
fh = logging.FileHandler("./cal_fun.log")
fmt = "\n[%(asctime)s-%(name)s-%(levelname)s]: %(message)s"
formatter = logging.Formatter(fmt)
fh.setFormatter(formatter)
logger.addHandler(fh)
return logger
def try_exception_and_log(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
logger_error = create_error_log()
logger_cal_fun = create_normal_logger()
try:
a = fn(*args, **kwargs)
logger_cal_fun.info(f"finished {fn.__name__}")
return a
except Exception as e:
logger_error.exception("[Error in {} ][arg: {}] msg: {}".format(__name__,*args, str(e)))
pass
# raise #报错后继续运行。
return wrapper
@try_exception_and_log
def sun_fun1(t, stockid):
print()
3、务必先单核调试,再多核调试!
4、要注意耗费时间但比较直接简单的代码写法。比如反复读取数据库会耗费很多时间
计算效率的关键点
1、 multiprocessing注意将多次循环均分到各个核心。我曾看到某人写的:最后一个核分配的计算量最重,其他核计算量均匀。结果整个程序多等最后一个苦逼的核心1个小时。
2、网络瓶颈。我所在的局域网大多数都是千兆设备,但测试时发现只有百兆。最后依次排查,发现一个不起眼的交换机惹的祸,另外网线也经常会作祟。
3、linux资源瓶颈:linux系统对文件打开数有默认限制,比如同时打开同一日志文件次数过多,会报错:open so many files。可以在sh脚本或者修改系统文件的方式。
注意:推荐用前者,因为后者数值写太高,会崩溃进不了系统的。崩溃了用‘单用户模式’修改回来。
4、用linux的资源监视器 监测多核计算的性能使用情况
5、数据库优化!!!非常重要
- 相关数据库的索引
- 多核访问是否达到访问限制:最大连接数、最大数据包等的设置
- 检测数据库的性能表现:慢查询日志、当前端口使用情况等。
6、不要反复进行费时的数据库读取操作。相连相近的读取数据最好能合并,然后再在本地内存分别调用。