Python多进程是如何实现的?
1 背景
在之前的博客中,笔者尝试介绍了多进程和多线程的原理,以及相关的实现,详情见 : Python | 多线程和多进程 。不过当时跑的都是示例性的代码,而最近刚好做一个项目的时候真刀真枪的用到了Python多进程!于是特此总结一下
2 Python实现多进程案例1
2.1 我们想要干嘛?
现在已经有了15万的商户编号(在mids.csv文件中),希望从数据库的商户表中查询到这15万商户的商户信息!怎么办呢?
- 思路1:直接用SQL语句查询具体见下方
select * from table where mer_id in minds_merchant
但存在以下的问题:
- 需要对15万商户的商户号改成SQL的形成,即括号括起来,里面每一个元素都是字符型!
- 直接查询速度会很慢!!!速度很慢!速度很慢!
所以有了第二种思路:
- 采用多进程的方式,同时开20个进程,每个进程同步的去取数据
- 每个进程当中又同步的去开很多个小块,这样做的目的是为了减小内存的压力,进一步提升效率!
2.2 Python实现
2.2.1 主进程代码
- 主要使用到的库是multiprocessing的Process函数
知识点总结:
- 显示主进程PID是为了进行监控任务的情况,可以在linux中输入 top 查看!相当于任务管理器!
- 记录了主进程PID也可以方便后续的kill掉任务(一旦不需要继续进行这个任务的时候)
- 那kill掉任务为什么不直接control+C呢?因为涉及到庞大的任务的时候,一般我们在后台运行,这时候可以加一个nohup,具体用法见下面:
nohup python cmd.py &
即 nohup + Python + 要执行的Python脚本 + &
- 因此一旦程序后台执行了,使用control+C就无法让其停止,那我们怎么停止任务呢?
- 需要强制关闭子进程时,需要用linux的kill命令。由于我们开启了多个子进程,一个进程一个进程地kill费时费力,所以我们利用刚才记录进程号的文件,读取其中所有pid,并用linux shell脚本直接依次kill掉。
- 步骤1:输入vim stop.sh
- 步骤2:复制粘贴下面的代码
for i in `cat pid`
do
echo $i
kill -9 $i
done
- 步骤3:保存脚本。(依次按 [ESC]、[:],然后输入[wq],回车)
- 步骤4:停止脚本赋执行权限。chmod +x stop.sh
- 步骤5:运行stop.sh脚本。./stop.sh
知识点补充:
- 可以直接import一个py脚本文件 比如 import score_mer
- import之后如果想用其中的一个函数,可以这么干:score_mer.concat() concat就是score_mer.py文件中的一个函数
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
控制台
"""
from multiprocessing import Process
import pandas as pd
import os
import math
import shutil
import time
import score_mer
def log(rec):
with open("log", 'a') as f:
f.write(rec+'\n')
def cmd():
# 显示主进程PID 这是为了监控
main_pid = os.getpid()
with open("pid", "w") as f:
f.write(str(main_pid)+'\n')
# 控制台
ur_time0 = time.time()
# 确定目标商户总数量 mids.csv是需要自己提前准备好的!
mid_csv = pd.read_csv('mids.csv')
# 商户号的列表
mids = list(mid_csv['MERC_ID'])
n = len(mids)
# 初始化日志
with open("log", 'w') as f:
f.write("====[Start: %d]====\n" % n)
with open('error_log', 'w') as f:
f.write("errors:\n" )
# 清空计算指标目录
index_fold = "index_res/"
'''
知识点1:判断一个文件夹/文件是否存在:os.path.exists()
知识点2:删除一个文件夹:shutil.rmtree()【其中要先导入 import shutil】
知识点3:创建一个文件夹:os.mkdir()
'''
if os.path.exists(index_fold):
shutil.rmtree(index_fold)
os.mkdir(index_fold)
# [1].多进程-计算交易特征
# 每个进程计算结果作为一小块放在index_res目录下
log("*** index computation start.")
time_all0 = time.time()
# 进程列表
ps = []
# 进程数
n_thread = 20
# 单个进程处理数据量
block = int(n / n_thread)
# 补充一个进程补完数据
n_thread = n_thread + 1
log(" block size: %d" % block)
# 设置子进程
for i in range(0, n_thread):
p = Process(target = score_mer.generate_index,
args=(i, i*block, (1+i)*block,))
ps.append(p)
# 启动子进程
for i in range(0, len(ps)):
time.sleep(2)
ps[i].daemon = True
ps[i].start() # 通过调用start方法,来启动进程
log(" [ ] thread [%d] launched." % i)
# 等待所有进程结束
for i in range(0, len(ps)):
ps[i].join() # 阻塞当前的进程,直到调用join方法的那个进程执行完毕
#thread_t1 = time.time()
#log(" [*] thread [%d] finished. (%.2fh)" % (i, (float(thread_t1 - time_all0)/ 3600)))
time_all1 = time.time()
log(" index computation finished. (%.2fs)" % (time_all1-time_all0))
score_mer.concat(index_fold, 'complete_index.csv')
cmd()
2.2.2 target代码
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
从商户表提取商户信息
"""
import pandas as pd
import time
import pymysql
import copy
import re
import os
# 最新时间点
now_year = 2018
now_mon = 12
# 最新时间(根据不同数据的时间节点然后进行相应的修改)
zjsj = '20181231235959'
def log(rec):
with open("log", 'a') as f:
f.write(rec+'\n')
def generate_index(tid, st, ed):
# 三个参数分别为 进程号;起始商户序号;终止商户序号
# 读取数据,计算特征指标
# 输出参数,st,ed,指定对商户号列表的[st, ed]部分进行计算,即对特定子块计算
# 记录PID
with open("pid", "a") as f:
f.write(str(os.getpid())+'\n')
all_t0 = time.time()
print("*** [score]-[%d] start %d - %d" % (tid, st, ed))
tablename = 'V_WSXD_MERC' # 商户表
# 数据库连接
a = pymysql.connect(host='你的host',port=3306,
user='XXXX',passwd='XXXX',db='XX',charset="utf8")
# [0].获取数据
# 读取需要计算的商户号列表
mid_csv = pd.read_csv('mids.csv')
mids = list(mid_csv['MERC_ID'])
real_ed = min(ed, len(mids))
# 目标:抽取指定部分
#print(st, real_ed)
mids = mids[st:real_ed]
n = len(mids)
print("*** [score]-[%d] %d - %d: %d merchants." % (tid, st, real_ed, n))
# 分块大小,每块有多少商户
block = 50
end = int( n / block ) + 1
#end = 4
print("*** [score]-[%d] Block size: %d | Block amount: %d" % (tid, block, end))
# 全局结果
global_res = []
# 上一次存储位置
last_pos = 0
for pos in range(0, end):
try:
t_0 = time.time()
#print(" [score] Block [%d]" % pos)
# 组装sql语句
merchants = "('"
# 检索商户号时的偏移量
offset = pos*block
right_pos = min(offset+block, n)
for i in range(offset, right_pos-1):
merchants += str(mids[i]) + "','"
merchants += str(mids[right_pos-1]) + "')"
#print merchants
sql = 'select * from %s where %s in %s' %\
(tablename, 'MERC_ID', merchants)
# 获取mysql数据
t0 = time.time()
d = pd.read_sql(sql, con=a)
t1 = time.time()
#print(" [score] get %d merchants. (%.2fs)" % (d['MERC_ID'].nunique(), t1-t0))
d.to_csv('index_res/thread_'+str(tid)+'_'+str(st+last_pos)+'_'+str(st+right_pos)+'.csv', index=False, encoding = 'gbk')
# 记录当前进程进度
log(" thread [%d] block[%d] finished: %d - %d" % (tid, pos, (st+last_pos), (st+right_pos)))
# 清空结果缓存
last_pos = right_pos
except Exception as e:
print(e)
with open('error_log', 'a') as f:
f.write('[%d]-block %d error \n' % (tid, pos))
all_t1 = time.time()
a.close()
rec = "Thread over - [%d] %.2f h" % (tid, (float(all_t1-all_t0)/3600))
print(rec)
# 记录计算时间
log(rec)
def concat(res_fold, final_name):
# 合并所有计算指标文件
#res_fold = 'index_res/'
ds = []
t0 = time.time()
for filename in os.listdir(res_fold):
if 'csv' in filename:
d = pd.read_csv(res_fold+filename, encoding = 'gbk')
ds.append(d)
data = pd.concat(ds, axis=0, sort=True)
t1 = time.time()
log("*** [concat scores starting] %d merchants." % (len(data)))
# 去重
data.drop_duplicates('MERC_ID', 'first', inplace=True)
# first 表示 删除重复项并保留第一次出现的项
# inplace=True 表示直接在原来的DataFrame上删除重复项
data.to_csv(final_name, index=False, encoding = 'gbk')
log("*** [concat scores finished] %d merchants (%d). %.2fs" % (data['MERC_ID'].nunique(), len(data), t1-t0))
##################################################################################################
总结:
-
确定目标。首先要明确我们用多进程来干吗!比如说这个例子是多进程来获取商户表的信息。多进程具体体现在对商户号分多个进程去取!
-
搭建多进程。使用multiprocessing的process函数来搭建。
-
准备好日志函数。在多进程的过程中做好日志的记录
-
启动子进程。
-
待所有进程全部结束后(join函数判断),记录时间,评估效率。
-
其中在第二步搭建多进程的时候,就需要确定target了!即我们到底想要干嘛的落实工作!
-
这时候需要进一步去分block!进一步的提高效率
-
所以总的来说就是多个进程同时启动,每一个进程下面的block按顺序来进行处理的!
-
上述过程中主要耗时的地方在于表的查询!
3 Python实现多进程案例2
需求:根据原始交易流水,计算每个商户最近6个月中以3个月为维度进行移动加权平均,并取其中的最大值和最小值!
cmd代码思路保持一致。
核心的score代码思路:
- 首先把最近6个月的总交易金额分别都算出来,然后存到一个list中
- 然后用一个for循环,3个月为维度,权重为3 2 1 结果存到一个list
- 最后取list的最大值和最小值。同时返回的结果也可以把每个月的总交易金额返回,以防后面有其余的需求!
3.1 cmd主代码
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
控制台
"""
from multiprocessing import Process
import pandas as pd
import os
import math
import shutil
import time
import score_wa
# import rank
def log(rec):
with open("log", 'a') as f:
f.write(rec+'\n')
def cmd():
# 显示主进程PID
main_pid = os.getpid()
with open("pid", "w") as f:
f.write(str(main_pid)+'\n')
# 控制台
ur_time0 = time.time()
# 确定目标商户总数量
mid_csv = pd.read_csv('mids.csv')
# 商户号的列表
mids = list(mid_csv['MERC_ID'])
n = len(mids)
# 初始化日志
with open("log", 'w') as f:
f.write("====[Start: %d]====\n" % n)
with open('error_log', 'w') as f:
f.write("errors:\n" )
# 清空计算指标目录
index_fold = "index_res/"
if os.path.exists(index_fold):
shutil.rmtree(index_fold)
os.mkdir(index_fold)
# [1].多进程-计算交易特征
# 每个进程计算结果作为一小块放在index_res目录下
log("*** index computation start.")
time_all0 = time.time()
# 进程列表
ps = []
# 进程数
n_thread = 20
# 单个进程处理数据量
block = int(n / n_thread)
# 补充一个进程补完数据
n_thread = n_thread + 1
log(" block size: %d" % block)
# block = 50
# n_thread = 2
# 设置子进程
for i in range(0, n_thread):
p = Process(target = score_wa.generate_index,
args=(i, i*block, (1+i)*block,))
ps.append(p)
# 启动子进程
for i in range(0, len(ps)):
time.sleep(2)
ps[i].daemon = True
ps[i].start()
log(" [ ] thread [%d] launched." % i)
# 等待所有进程结束
for i in range(0, len(ps)):
ps[i].join() # 通信 等待结束
#thread_t1 = time.time()
#log(" [*] thread [%d] finished. (%.2fh)" % (i, (float(thread_t1 - time_all0)/ 3600)))
time_all1 = time.time()
log(" index computation finished. (%.2fs)" % (time_all1-time_all0))
score_wa.concat('index_res/', 'wa_trans_amt.csv')
cmd()
3.2 score计算代码
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
基于交易记录计算特征指标
"""
import pandas as pd
import time
import pymysql
import copy
import re
import os
# 最新时间点
now_year = 2019
now_mon = 4
# 最新时间(根据不同数据的时间节点然后进行相应的修改)
# zjsj = '20181231235959'
# 根据最新的年份、月份,得出预设时间
def get_mon_set(now_year, now_mon, l):
mons = []
tp_mon = now_mon+1
tp_year = now_year
for i in range(0, l):
next_year = tp_year
next_mon = tp_mon-1
if next_mon == 0:
next_mon = 12
next_year = tp_year-1
tp_mon = next_mon
tp_year = next_year
if next_mon<10:
mons.append(str(next_year)+'-0'+str(next_mon))
else:
mons.append(str(next_year)+'-'+str(next_mon))
return mons
def time_check_mer(recent_time):
recent_stamp = time.mktime(time.strptime(recent_time, '%Y%m%d'))
return int(recent_stamp)
# 提取月份
def extract_month(time_stamp):
time_tuple = time.localtime(time_stamp)
mon_num = time_tuple.tm_mon
if mon_num<10:
mon_str = '0'+str(mon_num)
else:
mon_str = str(mon_num)
return str(time_tuple.tm_year)+'-'+mon_str
def log(rec):
with open("log", 'a') as f:
f.write(rec+'\n')
def check_mer(name, odf):
# 对指定商户的交易流水提特征
global now_year
global now_mon
# 原始数据,包含非正常交易的
ndf = copy.deepcopy(odf)
# 筛选数据,只包含正常交易的
df = copy.deepcopy(odf)
df = df[(df['TRANS_STATUS']==u'成功') & (df['TXN_AMT']>0)]
if len(df)>0:
# 转为字符型
df['AC_DT'] = df['AC_DT'].map(str)
# 转为时间戳
df['ts'] = df['AC_DT'].map(time_check_mer)
# 转为年份-月份
df['month'] = df['ts'].map(extract_month)
else:
df = pd.DataFrame({}, columns =
(list(odf.columns)+['ts', 'month']))
# 所有月份
all_mon = get_mon_set(now_year, now_mon, 6)
# 加权移动平均处理
# 设置一个空list来存储每个月交易金额
amt_m = []
for i in range(len(all_mon)):
df_m = df[df['month']==all_mon[i]]
amt_tmp = df_m['TXN_AMT'].sum()
amt_m.append(amt_tmp)
# 2019-4月总交易金额
amt_m4 = amt_m[0]
# 2019-3月总交易金额
amt_m3 = amt_m[1]
# 2019-2月总交易金额
amt_m2 = amt_m[2]
# 2019-1月总交易金额
amt_m1 = amt_m[3]
# 2018-12月总交易金额
amt_m12 = amt_m[4]
# 2018-11月总交易金额
amt_m11 = amt_m[5]
# 每个月的总交易金额组成的list amt_m
# 确定权重
w = [3/6, 2/6, 1/6]
# 空的list 存储加权移动平均值
wa = []
for i in range(len(amt_m)-2):
wa.append(w[0] * amt_m[i] + w[1] * amt_m[i+1] + w[2] * amt_m[i+2])
# 加权移动平均的最大值
wa_max = max(wa)
# 加权移动平均的最小值
wa_min = min(wa)
# 汇总结果
res = [name,
# 1-加权移动平均的最大值
wa_max,
# 2-加权移动平均的最小值
wa_min,
amt_m4, # 2019-4月总交易金额
amt_m3, # 2019-3月总交易金额
amt_m2, # 2019-2月总交易金额
amt_m1, # 2019-1月总交易金额
amt_m12, # 2018-12月总交易金额
amt_m11 # 2018-11月总交易金额
]
return res
def generate_index(tid, st, ed):
# 三个参数分别为 进程号;起始商户序号;终止商户序号
# 读取数据,计算特征指标
# 输出参数,st,ed,指定对商户号列表的[st, ed]部分进行计算,即对特定子块计算
# 记录PID
with open("pid", "a") as f:
f.write(str(os.getpid())+'\n')
all_t0 = time.time()
print("*** [score]-[%d] start %d - %d" % (tid, st, ed))
tablename = 'V_WSXD_TRANS' # 交易表
# 数据库连接
a = pymysql.connect(host='your host',port=3306,
user='XXXX',passwd='XXXX',db='XX',charset="utf8")
# [0].获取数据
# 读取需要计算的商户号列表
mid_csv = pd.read_csv('mids.csv')
mids = list(mid_csv['MERC_ID'])
real_ed = min(ed, len(mids))
# 目标:抽取指定部分
#print(st, real_ed)
mids = mids[st:real_ed]
n = len(mids)
print("*** [score]-[%d] %d - %d: %d merchants." % (tid, st, real_ed, n))
# 分块大小,每块有多少商户
block = 50
end = int( n / block ) + 1
#end = 4
print("*** [score]-[%d] Block size: %d | Block amount: %d" % (tid, block, end))
# 全局结果
global_res = []
# 上一次存储位置
last_pos = 0
for pos in range(0, end):
try:
t_0 = time.time()
#print(" [score] Block [%d]" % pos)
# 组装sql语句
merchants = "('"
# 检索商户号时的偏移量
offset = pos*block
right_pos = min(offset+block, n)
for i in range(offset, right_pos-1):
merchants += str(mids[i]) + "','"
merchants += str(mids[right_pos-1]) + "')"
#print merchants
sql = 'select * from %s where %s in %s' %\
(tablename, 'MERC_ID', merchants)
# 获取mysql数据
t0 = time.time()
d = pd.read_sql(sql, con=a)
t1 = time.time()
#print(" [score] get %d merchants. (%.2fs)" % (d['MERC_ID'].nunique(), t1-t0))
# [1].按商户分组,计算特征
groups = d.groupby(d['MERC_ID'])
res = []
# 对每一个商户计算相应的交易流水特征
for group in groups:
try:
res.append(check_mer(group[0], group[1]))
except Exception as e:
with open('error_log', 'a') as f:
f.write('[%d]-block %d local error. (sample[%s], -1) \n' % (tid, pos, group[0]))
global_res = global_res + res
t_1 = time.time()
print(" [score]-[%d] Block[%d]. %d merc. Request: %.2fs. Processing: %.2fs" % (tid, pos, d['MERC_ID'].nunique(), t1-t0, t_1-t_0))
if (pos % 40 == 0) or (pos == (end-1)):
# 每40个块输出一次数据
# 40X50 = 2000个商户
# [2].输出结果
header = ['MERC_ID',
# 1-加权移动平均的最大值
'wa_max',
# 2-加权移动平均的最小值
'wa_min',
'amt_m4', # 2019-4月总交易金额
'amt_m3', # 2019-3月总交易金额
'amt_m2', # 2019-2月总交易金额
'amt_m1', # 2019-1月总交易金额
'amt_m12', # 2018-12月总交易金额
'amt_m11' # 2018-11月总交易金额
]
rd = pd.DataFrame(global_res, columns=header)
rd = rd.fillna(0)
rd.to_csv('index_res/thread_'+str(tid)+'_'+str(st+last_pos)+'_'+str(st+right_pos)+'.csv', index=False, encoding = 'gbk')
# 记录当前进程进度
log(" thread [%d] block[%d] finished: %d - %d" % (tid, pos, (st+last_pos), (st+right_pos)))
# 清空结果缓存
global_res = []
last_pos = right_pos
except Exception as e:
print(e)
with open('error_log', 'a') as f:
f.write('[%d]-block %d error \n' % (tid, pos))
all_t1 = time.time()
a.close()
rec = "Thread over - [%d] %.2f h" % (tid, (float(all_t1-all_t0)/3600))
print(rec)
# 记录计算时间
log(rec)
def concat(res_fold, final_name):
# 合并所有计算指标文件
#res_fold = 'index_res/'
ds = []
t0 = time.time()
for filename in os.listdir(res_fold):
if 'csv' in filename:
d = pd.read_csv(res_fold+filename, encoding = 'gbk')
ds.append(d)
data = pd.concat(ds, axis=0, sort=True)
t1 = time.time()
log("*** [concat scores starting] %d merchants." % (len(data)))
# 去重
data.drop_duplicates('MERC_ID', 'first', inplace=True)
data.to_csv(final_name, index=False, encoding = 'gbk')
log("*** [concat scores finished] %d merchants (%d). %.2fs" % (data['MERC_ID'].nunique(), len(data), t1-t0))
##################################################################################################
4 知识点补充
4.1 为什么要给MySQL加索引
- 提高查询的效率!索引可以大大提高MySQL的检索速度。
- 索引分单列索引和组合索引。单列索引,即一个索引只包含单个列,一个表可以有多个单列索引,但这不是组合索引。组合索引,即一个索引包含多个列。
4.2 如何给MySQL加索引
- 创建索引时,你需要确保该索引是应用在 SQL 查询语句的条件(一般作为 WHERE 子句的条件)。
- 具体的实现有很多种方式,比如建表的时候可以加,在已有的表上可以直接添加等等。下面是在已有的表格上进行添加索引!
添加索引:
ALTER table tableName ADD INDEX indexName(columnName)
4.3 加索引的缺点
- 虽然索引大大提高了查询速度,同时却会降低更新表的速度,如对表进行INSERT、UPDATE和DELETE。因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件。
- 建立索引会占用磁盘空间的索引文件。
- 即用空间换时间!