python3 多进程, 多线程, 协程性能对比 以及GIL锁解释

描述

对于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上,单线程的性能比多线程还要好,那岂不是没有用了?任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。而在爬虫中经常会遇到一些网络延迟的问题,导致线程切换,其间花费的开销是很大的。尤其当线程数很多的时间,很大比例的时间都花费在切换在锁的操作上。因此,不建议使用threading'多线程进行爬取,在官方社区推出multiprocess多进程模块,通过进程来增加python程序的工作效率。从上上图中,可以看出多进程与协程之间的时间相差无几。但是进程之间的通信只能通过QUEUE, SHARE MEMORY的方式进行。但是在一些简单IO密集型操作中,如读写文件,可以通过多线程增加其工作效率。

 

测试程序见如下

    

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)

 

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值