首页 > 编程知识 正文

python多线程坑(python真正的多线程)

时间:2023-05-04 04:46:45 阅读:97829 作者:2350

一、前言

很多时候,我们写一个爬虫,满足要求之后,会发现很多改进,其中之一就是爬行速度。本文通过代码说明了如何利用多进程、多线程和协同进程来提高爬行速度。注意:我们不深入介绍理论和原理,一切都在代码里。

第二,同步

首先,我们编写一个简化的爬虫,细分每个功能,有意识地执行功能编程。以下代码的目的是访问百度页面300次并返回状态代码,其中parse_1函数可以设置循环数,每个循环都会将当前循环数(从0开始)和url传递给parse_2函数。

导入请求

defparse_1():

url='https://www.baidu.com '

foriirange(300):

parse_2(url)

defparse_2(url):

response=requests.get(url)

打印(响应.状态代码)

if__name__=='__main__':

parse_1()的性能消耗主要在IO请求中,在单进程、单线程模式下请求URL时必然会造成等待。

示例代码是典型的串行逻辑。parse_1将url和循环号传递给parse_2。parse_2请求并返回状态代码后,parse_1继续迭代一次,重复前面的步骤。

第三,多线程

由于CPU执行程序时每个时间尺度上只有一个线程,多线程实际上提高了进程的利用率,从而提高了CPU的利用率。

实现多线程的库有很多,concurrent.futures中的ThreadPoolExecutor演示了这一点,之所以引入ThreadPoolExecutor库,是因为它比其他库代码更简单。

为了方便说明问题,下面代码中如果是新增加的部分,代码行前会加上 符号便于观察说明问题,实际运行需要去掉

导入请求

from concurrent . futureimportthreadpoolexecutor

defparse_1():

url='https://www.baidu.com '

#建立线程池

池=线程池执行器(6)

foriirange(300):

pool.submit(parse_2,url)

关闭池(等待=真)

defparse_2(url):

response=requests.get(url)

打印(响应.状态代码)

if__name__=='__main__':

Parse_1()是异步的,而不是同步的。异步意味着彼此独立,在等待一个事件的同时继续做自己的事情,而不是在工作前等待事件结束。线程是实现异步的一种方式,也就是说多线程是异步处理,也就是说我们不知道处理结果,有时候我们需要知道处理结果,所以可以用回调。

导入请求

from concurrent . futureimportthreadpoolexecutor

#添加回调函数

defcallback(未来):

打印(future.result())

defparse_1():

url='https://www.baidu.com '

池=线程池执行器(6)

foriirange(300):

results=pool.submit(parse_2,url)

#回调的关键步骤

results.add_done_callback(回调)

关闭池(等待=真)

defparse_2(url):

response=requests.get(url)

打印(响应.状态代码)

if__name__=='__main__':

Parse_1()Python实现了多线程。有一个GIL(全局解释器锁)被无数人诟病,但是多线程还是很适合抓取网页的,大多是IO密集型的。

四.多进程

多进程有两种实现方式:ProcessPoolExecutor和多进程。

00-1010类似于实现多线程的ThreadPoolExecutor。

导入请求

> from concurrent.futures import ProcessPoolExecutor def parse_1():     url = 'https://www.baidu.com'     # 建立线程池     > pool = ProcessPoolExecutor(6)     for i in range(300):         > pool.submit(parse_2, url)     > pool.shutdown(wait=True) def parse_2(url):     response = requests.get(url)     print(response.status_code) if __name__ == '__main__':     parse_1()

可以看到改动了两次类名,代码依旧很简洁,同理也可以添加回调函数

import requests from concurrent.futures import ProcessPoolExecutor > def callback(future):     > print(future.result()) def parse_1():     url = 'https://www.baidu.com'     pool = ProcessPoolExecutor(6)     for i in range(300):         > results = pool.submit(parse_2, url)         > results.add_done_callback(callback)     pool.shutdown(wait=True) def parse_2(url):     response = requests.get(url)     print(response.status_code) if __name__ == '__main__':     parse_1()
2. multiprocessing

直接看代码,一切都在注释中。

import requests > from multiprocessing import Pool def parse_1():     url = 'https://www.baidu.com'     # 建池     > pool = Pool(processes=5)     # 存放结果     > res_lst = []     for i in range(300):         # 把任务加入池中         > res = pool.apply_async(func=parse_2, args=(url,))         # 获取完成的结果(需要取出)         > res_lst.append(res)     # 存放最终结果(也可以直接存储或者print)     > good_res_lst = []     > for res in res_lst:         # 利用get获取处理后的结果         > good_res = res.get()         # 判断结果的好坏         > if good_res:             > good_res_lst.append(good_res)     # 关闭和等待完成     > pool.close()     > pool.join() def parse_2(url):     response = requests.get(url)     print(response.status_code) if __name__ == '__main__':     parse_1()

可以看到multiprocessing库的代码稍繁琐,但支持更多的拓展。多进程和多线程确实能够达到加速的目的,但如果遇到IO阻塞会出现线程或者进程的浪费,因此有一个更好的方法……

五、异步非阻塞

协程+回调配合动态协作就可以达到异步非阻塞的目的,本质只用了一个线程,所以很大程度利用了资源

实现异步非阻塞经典是利用asyncio库+yield,为了方便利用逐渐出现了更上层的封装 aiohttp,要想更好的理解异步非阻塞最好还是深入了解asyncio库。而gevent是一个非常方便实现协程的库

import requests > from gevent import monkey # 猴子补丁是协作运行的灵魂 > monkey.patch_all() > import gevent def parse_1():     url = 'https://www.baidu.com'     # 建立任务列表     > tasks_list = []     for i in range(300):         > task = gevent.spawn(parse_2, url)         > tasks_list.append(task)     > gevent.joinall(tasks_list) def parse_2(url):     response = requests.get(url)     print(response.status_code) if __name__ == '__main__':     parse_1()

gevent能很大提速,也引入了新的问题:如果我们不想速度太快给服务器造成太大负担怎么办?如果是多进程多线程的建池方法,可以控制池内数量。如果用gevent想要控制速度也有一个不错的方法:建立队列。gevent中也提供了Quene类,下面代码改动较大

import requests from gevent import monkey monkey.patch_all() import gevent > from gevent.queue import Queue def parse_1():     url = 'https://www.baidu.com'     tasks_list = []     # 实例化队列     > quene = Queue()     for i in range(300):         # 全部url压入队列         > quene.put_nowait(url)     # 两路队列     > for _ in range(2):         > task = gevent.spawn(parse_2)         > tasks_list.append(task)     gevent.joinall(tasks_list) # 不需要传入参数,都在队列中 > def parse_2():     # 循环判断队列是否为空     > while not quene.empty():         # 弹出队列         > url = quene.get_nowait()         response = requests.get(url)         # 判断队列状态         > print(quene.qsize(), response.status_code) if __name__ == '__main__':     parse_1()

结束语

以上就是几种常用的加速方法。如果对代码测试感兴趣可以利用time模块判断运行时间。爬虫的加速是重要技能,但适当控制速度也是爬虫工作者的良好习惯,不要给服务器太大压力,拜拜~

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。