从底层到第三方库,全面讲解python的异步编程。这节讲述的是python的多线程实现,纯干货,无概念,代码实例讲解。
本系列有6章左右,点击头像或者专栏查看更多内容,陆续更新,欢迎关注。
部分资料来源及参考链接:
https://www.bilibili.com/video/BV1Li4y1j7RY/
现在让我们初步进入多进程,这个就是python的多进程包,是自带的,简单示例:
import multiprocessing#进程包
import time
def start():
time.sleep(2)#让程序沉睡 2 秒
print(multiprocessing.current_process().name)#打印进程名字
print(multiprocessing.current_process().pid)#打印pid
print(multiprocessing.current_process().is_alive())#打印进程是否活着
if __name__ == "__main__":
print('程序开始')
p = multiprocessing.Process(target = start)#只用写函数名 不要加括号
p.start()#开始
p.join()#堵塞
print('程序结束')
此时,并不是一个进程打开多个线程,而是多个进程,所以每次执行有不同的pid。
结果如下:
本身进程是无法通信的,借助别的数据结构,就可以实现进程通信了,一般是栈和队列,就像这样:
from multiprocessing import Process,Queue
def write(q):#放入队列
print('加入队列成功:{}'.format(Process.pid))#打印进程pid
for i in range(10):# 0~9
print('往队列放入:{}'.format(i))
q.put(i)#放入
def read(q):#读取队列
print('加入队列成功:{}'.format(Process.pid))#打印进程pid
while True:#一有东西 就马上读取
value = q.get()#读取
print('获取队列中的东西:{}'.format(value))
if __name__ == "__main__":
#由于Python的多进程默认无法进行通信 因为是并发执行的
#所以要借助别的数据结构
#一般用栈 或者 队列
q = Queue()#实例化Queue 队列
pw = Process(target = write,args =(q,))#创建写入进程
pr = Process(target = read,args = (q,))#创建读取进程
pw.start()#启动写入
pr.start()#启动读取
pw.join()#堵塞读取
python当中实现了栈和队列,非常方便,如果你运行上述代码,你会发现程序没有结束,读取进程它还在反复读取。这其实就和golang中的管道类似。此处可以先做了解。
可以使用map方法批量提交目标
import multiprocessing
def index_pool(data):
res = data * data
return res
if __name__ == "__main__":
data = list(range(100))#100个任务
pool = multiprocessing.Pool(processes = 4)#进程池大小为4
pool_out_puts = pool.map(index_pool,data)#一次性提交大量任务
# pool_out_puts = pool.apply(index_pool,args=(10,))#一个个提交
pool.close()#关闭进程 不再创建进程
pool.join()#堵塞进程
print('Pool {}'.format(pool_out_puts))
运行结果:
你会发现执行速度非常快
这就是与多线程的区别,每个进程是独立的,不会受到GIL锁的控制,速度非常快
上述的例子中,进程是同步执行的,如何写出异步的效果呢?
from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor,as_completed
import time
number_list = [1,2,3,4,5,6,7,8,9,10]
def add_number(data):#这个函数 只能消耗CPU资源 没啥意义
item = count(data)
return item
def count(number):#单纯计算 随便写
for i in range(0,5000000):
i = i + 1
return i * number
if __name__ == "__main__":
start_time = time.time()#程序启动时间
with ProcessPoolExecutor(max_workers = 5) as t:# max_workers参数为 你要开多少个进程
for item in number_list:#提交任务
t.submit(add_number,item)
# reqs = [t.submit(add_number,item) for item in number_list]#提交任务 简洁写法
# for req in as_completed(reqs):# 转成 可迭代对象
# print(req.result())#打印信息
print('程序总耗时:{}'.format(time.time() - start_time))
由于没有GIL锁的限制,执行会非常快。
在Python中,进程池(Process Pool)和异步进程池(Asyncio Process Pool)是用于并行处理任务的两种不同的机制。
进程池(Process Pool):
进程池是通过multiprocessing模块提供的一种机制,它允许你创建一组预先初始化的进程,用于执行任务。你可以将任务提交给进程池,进程池会自动分配可用的进程来执行任务。进程池可以通过Pool类来创建。
进程池适用于CPU密集型任务,可以充分利用多核处理器的并行性。它通过创建多个进程来同时执行任务,每个进程都有自己的Python解释器和GIL,因此可以实现真正的并行执行。进程池在处理大量计算密集型任务时通常具有较好的性能。
异步进程池(Asyncio Process Pool):
异步进程池是通过concurrent.futures和asyncio模块提供的一种机制,它允许在异步环境中并行处理任务。异步进程池是建立在异步编程的基础上,可以在单个线程中同时执行多个任务。
异步进程池适用于IO密集型任务,如网络请求、文件读写等。它利用异步编程的特性,通过在任务之间进行切换来提高效率,避免了线程切换的开销。异步进程池在处理大量IO密集型任务时通常具有较好的性能。
异步线程池和异步进程池的主要区别在于线程池使用的是线程,而进程池使用的是进程。
线程池:在Python中,线程是由操作系统管理的,多个线程共享同一进程的内存空间,因此线程之间的切换开销较小。线程池适用于IO密集型任务,如网络请求、文件读写等,因为在这些任务中,大部分时间都是在等待IO操作完成,线程可以在等待期间切换执行其他任务,提高效率。
进程池:进程是由操作系统管理的,每个进程都有独立的内存空间,进程之间切换的开销较大。进程池适用于CPU密集型任务,如数据处理、图像处理等,因为这些任务需要大量的计算资源,多个进程可以并行执行,提高效率。
无论是线程池还是进程池,在处理IO密集型任务时都可以提高效率。但对于CPU密集型任务,由于Python的全局解释器锁(GIL)的存在,多线程并不能真正实现并行执行,因此在这种情况下使用进程池更为合适。如果需要同时处理大量IO密集型和CPU密集型任务,可以结合使用线程池和进程池来充分利用多核资源。
进程会特别占用内存,能够使用线程池的场景,还是使用线程池更好。
以网络爬虫为例:
当使用进程池和异步进程池来实现网络爬虫项目时,它们的处理逻辑和性能表现有一些区别。
使用进程池的处理逻辑如下:
使用异步进程池的处理逻辑如下:
在性能方面,异步进程池通常比进程池更快。这是因为异步进程池可以同时执行多个任务,不需要等待一个任务完成后才能执行下一个任务,从而提高了效率。而进程池则需要按顺序逐个处理任务,无法并行执行。对于IO密集型的任务,异步进程池的性能提升更为明显,因为它可以充分利用CPU等待IO的时间来执行其他任务。