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、不要反复进行费时的数据库读取操作。相连相近的读取数据最好能合并,然后再在本地内存分别调用。