描述
对于python来说, 多线程是python的软肋。在官方文档表明,根据程序的不同类型,如是I/O密集型,CPU密集型,分别使用多线程,多进程会使性能达到最佳。本文的主要目的是笔者在面试过程中,提及到线程,进程,协程对于爬虫来说,哪个性能会更好(笔者本人认为是进程加协程二者配合会达到更好的效果)。希望通过本文来让其他人对于爬虫的线程,进程,协程使用有更深的理解。
本文涉及的环境模块有:
ubuntu16. python3.5 cpu 1核 网络正常家庭带宽
multiprocessing , threading, gevent, requests, urllib
试验条件
访问特定url 100次,通过访问的整体耗时,来判别多线程,多进程, 协程的性能对于爬取网络I/O密集型程序的优劣。特定url选定为百度首页https://www.baidu.com。
实验前期准备
单线程/单进程 响应时间测速
单线程/单进程 响应时间测速 作为 对照组。在这里选用requests, urllib两个爬虫常用模块进行对比,也希望通过此进行比较这二者间性能差别。
上图为访问100次url,所获得的响应时间。此时间差为time模块获取,因此与实际响应时间偏大。但对于此次分析,不造成任何影响。有几次延迟时间特别高,推测该网站对于爬虫有一定的识别,不代表该模块的性能。
上图是requests,urllib 100此访问时间综合。但是值得考虑的是,笔者采用的是同一url进行100次访问,在网站有爬虫识别的技术,其访问时间会延迟。对于一个爬虫来说,访问者100次同一个网站,其实这很常见。而测试的源代码如下:
url = "http://www.baidu.com"
def LoopRequests():
count = 0
t = []
while True:
if count > 100:
break
s = time.time()
req = requests.get(url)
e = time.time()
time.sleep(0.1)
print(e-s)
red.rpush('x1', e-s)
count += 1
return t
import urllib
from urllib import request
def LoopUrlib():
count = 0
t = []
while True:
if count > 100:
break
s = time.time()
req = request.Request(url=url)
rep = request.urlopen(req)
e = time.time()
time.sleep(0.1)
print(">>", e-s)
#single process thread t.append(e - s)
red.rpush('x2', e-s)
count += 1
return t
协程, 进程,线程对比
上图为各自开启了100个协程,100个进程,100个线程进行响应时间对比图,其数值通过三次实验访问的时间均值。其数值为结束100个url请求与开启访问url请求时间之差。值得注意的是开启了100个进程,实际在操作过程中笔者是开启了进程数为10的进程池,在for循环的100次达到的。从上图可以看出,gevent开启100个协程请求访问毫无压力,其总响应时间远低于开启了100个线程。有人会问,“开启线程不应该怎样都会比原先更快吗?怎么比单线程单进程的访问请求更慢?” 其原因是由于python本身的GIL锁导致,详细原因请看文末。
文末结论
我们知道能够开启的线程数,进程数是根据电脑本身cpu核数而定的,因此其数量不能随便增加。在上面开启了100个线程,可能线程数太多造成不必要的线程开销?因此有以下实验,分别开启了10个线程循环十次,5个线程循环二十次:
从上图表明,在减少线程数量时,可以减少线程开启的开销,结果是整体访问时间减少(数据是3次实验均值)。但始终不及gevent开启的协程效率高。其原因是由于python的GIL锁。 笔者参考资料作以下解释,见下一节
python GIL锁
python 的GIL锁并不是python 的特性,而我们使用大多解释器为CPython,其中就有GIL锁的身影。他的存在是保证多线程之间的数据完整性以及状态同步性。在这里推荐读者自己前往python社区进行详细了解JPython, IronPython解释器。
测试程序见如下
url = "http://www.baidu.com"
def LoopRequests():
count = 0
t = []
while True:
if count > 1:
break
s = time.time()
req = requests.get(url)
e = time.time()
# time.sleep(0.1)
await asyncio.sleep(.1)
print(e-s)
# single process thread t.append(e - s)
es = 1 if e - s > 5 else 0
red.rpush('x1', es)
count += 1
return t
import urllib
from urllib import request
def LoopUrlib():
count = 0
t = []
while True:
if count > 1:
break
s = time.time()
req = request.Request(url=url)
rep = request.urlopen(req)
e = time.time()
# time.sleep(0.1)
await asyncio.sleep(.1)
print(">>", e-s)
#single process thread t.append(e - s)
es = 1 if e - s > 5 else 0
red.rpush('x2', es)
count += 1
return t
import multiprocessing
from multiprocessing import Pool
def multiPool(mode = ''):
if mode == 'Loopreuqests':
st = time.time()
pool = Pool(10)
for i in range(100):
pool.apply_async(LoopRequests)
pool.close()
pool.join()
print('>>>', time.time()-st)
elif mode == 'LoopUrlib':
st = time.time()
pool = Pool(10)
for i in range(100):
pool.apply_async(LoopUrlib)
pool.close()
pool.join()
print('>>>', time.time() - st)
def threadTest(mode):
import threading
if mode == 'Loopreuqests':
st = time.time()
th = []
for i in range(5):
th.append(threading.Thread(target=LoopRequests))
for i in th:
i.start()
i.join()
print('>>>', time.time() - st)
elif mode == 'LoopUrlib':
st = time.time()
th = []
for i in range(5):
th.append(threading.Thread(target=LoopUrlib))
for i in th:
i.start()
i.join()
print('>>>', time.time() - st)
def get(k):
a = []
while True:
try:
x = red.lpop(k)[1]
if x == None:raise ValueError
a.append(x)
except Exception as e:
print(e)
# break
return a
import gevent
from gevent import monkey;monkey.patch_all()
def GeventTest():
s = time.time()
gt = [gevent.spawn(LoopUrlib) for i in range(1000)]
gevent.joinall(gt)
d = time.time()
# print("gevent:", d-s)
gt = [gevent.spawn(LoopRequests) for i in range(1000)]
gevent.joinall(gt)
print("gevent:", time.time() - d)
---------------------
转载,仅作分享,侵删
作者:hea_gui_lion
原文:https://blog.csdn.net/hea_gui_lion/article/details/85884347
|
|