目录
太长不看版
在11400h的测试中,多核拉满的速度为单开的6.22倍,长时间跑任务降频后仍有4.87倍,充分利用了多核的性能,加速了日常的计算。戳。
任务分配用到的是切片访问的思路,用[start:end:step]来分割任务,将任务平均分配到多个逻辑处理器上。同时用multiprocessing的Queue()来传输数据。戳。
因为多开是同时运行多个python应用,因此必须使用程序主入口,并且用右键的run的形式。戳。
———————————————————————————————————————————
首先能写这个的契机是11代的笔电i5终于yes了一回,6核12线程的11400h都支楞起来了,不过看着平时比较低的利用率,总感觉钱白花了。
碰巧有时候用python处理一些任务时,动不动十几二三十分钟,自己写的屎山经常一核拉满多核围观。所以想了想,学了multiprocessing,并且慢慢摸索了一个写作思路。
一、多进程和多线程的区分
这里有几个知识点要重点记录一下
单个CPU在任一时刻只能执行单个线程,只有多核CPU还能真正做到多个线程同时运行
一个进程包含多个线程,这些线程可以分布在多个CPU上
多核CPU同时运行的线程可以属于单个进程或不同进程
所以,在大多数编程语言中因为切换消耗的资源更少,多线程比多进程效率更高
坏消息,Python是个特例!GIL锁
python始于1991年,创立初期对运算的要求不高,为了解决多线程共享内存的数据安全问题,引入了GIL锁,全称为Global Interpreter Lock,也就是全局解释器锁。GIL规定,在一个进程中每次只能有一个线程在运行。这个GIL锁相当于是线程运行的资格证,某个线程想要运行,首先要获得GIL锁,然后遇到IO或者超时的时候释放GIL锁,给其余的线程去竞争,竞争成功的线程获得GIL锁得到下一次运行的机会。
正是因为有GIL的存在,python的多线程其实是假的,所以才有人说python的多线程非常鸡肋。但是虽然每个进程有一个GIL锁,进程和进程之前还是不受影响的。
GIL是个历史遗留问题,过去的版本迭代都是以GIL为基础来的,想要去除GIL还真不是一件容易的事,所以我们要做好和GIL长期面对的准备。
多进程 vs 多线程
那么是不是意味着python中就只能使用多进程去提高效率,多线程就要被淘汰了呢?那也不是的。
这里分两种情况来讨论,CPU密集型操作和IO密集型操作。针对前者,大多数时间花在CPU运算上,所以希望CPU利用的越充分越好,这时候使用多进程是合适的,同时运行的进程数和CPU的核数相同;针对后者,大多数时间花在IO交互的等待上,此时一个CPU和多个CPU是没有太大差别的,反而是线程切换比进程切换要轻量得多,这时候使用多线程是合适的。
所以有了结论:
CPU密集型操作使用多进程比较合适,例如海量运算
IO密集型操作使用多线程比较合适,例如爬虫,文件处理,批量ssh操作服务器等等
————————————————
版权声明:本文为CSDN博主「T型人小付」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Victor2code/article/details/109005171
二、导包
1.演示包
import multiprocessing as mp
from tqdm import tqdm
import pandas as pd
2.用途
(1)multiprocessing
多进程的包用的是python内置的multiprocessing,区别于多线程用的threadings。
导完mp之后可以看一下自己的核心数,后面有用。
multiprocessing.cpu_count()
(2)tqdm
tqdm是一个进度条的包,可以有效治疗等待焦虑症。
正常来讲只要把它包在可迭代对象外面就行,不过笔者最近发现pandas的series不是tqdm可以包装的对象。下文中笔者对series按list强转了之后才可以。
(3)pandas
在本文中用pandas作演示,主要也是考虑到pandas的数据有index,即使不同核心之间处理任务速度不一样,导致分别写进queue的时候顺序是乱的,出来的时候也是有顺序的。
三、函数设计
1.函数
(1)正常情况
一个正常的函数如下
def main(data):
tasks = ['姓名', '性别', '班级', '专业'] # 假设我们的任务是根据学号查询姓名、性别、班级等信息
op = pd.DataFrame(columns=tasks) # 在每个进程中都创建一张空表
op['学号'] = data['学号'] # 只放入任务量的学号
op.set_index('学号', inplace=True, drop=False) # 将学号设置为索引的同时将其保留在表格中
"""
在大表里面查每一个小任务
"""
for i in tqdm(op['学号']):
for j in tasks:
op.loc[[i], [j]] = data[data['学号'] == i][j].values[0]
(2)多核函数设计
给正常函数加上了q、start、end、step四个参数,构造成如下的样子
def main(data, q, start, end, step):
tasks = ['姓名', '性别', '班级', '专业'] # 假设我们的任务是根据学号查询姓名、性别、班级等信息
op = pd.DataFrame(columns=tasks) # 在每个进程中都创建一张空表
op['学号'] = data['学号'][start:end:step] # 只放入任务量的学号
op.set_index('学号', inplace=True, drop=False) # 将学号设置为索引的同时将其保留在表格中
"""
在大表里面查每一个小任务
"""
for i in tqdm(op['学号']):
for j in tasks:
op.loc[[i], [j]] = data[data['学号'] == i][j].values[0]
q.put(op) # 把op丢进queue里,等下取出来
2.参数设置
(1)q
对应的是multiprocessing里的queue,.put()一端放进一个数据,.get()一端取出这个数据,先放进去的先取出来。在本文里面因为每个数据都有索引,如果多核之间速度不一样,也不会导致乱序的问题。
(2)start、end、step
没错,就是常见的切片操作,start放0,end放任务总数,step写核心数量,比如笔者的11400H就写12,任务就能被分配成12个核心平均处理的量。
四、运行
1.入口
多进程是同时开好几个python跑,所以写完函数不能直接引,得加点繁文缛节。
if __name__ == '__main__':
dt = pd.read_excel(file, dtype=object) # 用pandas读表
qq = mp.Queue() # 企鹅:? 好吧,这就是上文提到的管道,用来沟通多核的任务
df_op = pd.DataFram()
for i in range(12): # 取决于核心数量
p = mp.Process(taiget=main, args=(qq, dt, 0+i, 100, 12))
p.start()
for i in range(12)
df_op = df_op.append(qq.get())
df_op.to_excel(file1, encoding='utf_8_sig', index=False)
(1)Process函数的参数问题
对于target,这里写函数,但不写括号。
参数用args去传入!
start为0+i,end为总任务量。
(2)两个for?
先跑完p再用q
2.运行及效果
以下部分笔者用的是自己的另一个程序的数据。
(1)正常跑测试
此时单核速度9.16it/s
(2)六开测试
此时每个单核的速度是5.73it/s,多核的速度照此估算是5.73*6=34.38it/s
(3)十二核(逻辑处理器)拉满
12个,看看拉满的效果。
总速度是4.75*12=57it/s
但实际上因为温度控制的问题,时间久了之后CPU会降频,笔者用了各种办法,最后成绩稳定在3.8it/s左右
3.8*12=44.6it/s
3.可不可以把数字写得更大?
可以,不过同时开18个python还能150%不成?
再写多一点也没啥用。
五、小结
多核处理中,处理速度和核心数不是线性关系,会有边际效应,但这个效应不明显。
以单开9.16it/s,六开34.38it/s和全开降频前57it/s、降频后44.6it/s的速度来看
六开的速度达到单开的3.75倍
全开的速度降频前达到单开的6.22倍,降频后达到4.87倍
对于有多核处理器的小伙伴来说,岂不美哉?