玩转python(7)python多协程,多线程的比较

前段时间在做一个项目,项目本身没什么难度,只是数据存在一个数据接口服务商那儿,这就意味着,前端获取数据需要至少两次http请求,第一次是前端到后端的请求,第二次是后端到数据接口的请求。有时,后端接收到前端的一次请求后,可能需要对多个接口进行请求,按照传统串行执行请求的方法,用户体验肯定是非常糟糕了,而且对计算资源也是极大的浪费,正好前段时间学习了协程和线程的知识,所以我花了一些时间,对几种可行方案进行了测试对比。一开始我使用真正的网络io进行测试,发现这种方法受网络环境影响比较大,为了公平起见,用sleep(0.02)来代替网络io,接下来介绍方案和测试结果。

基本方案:串行执行

import time

def wget(flag):
    time.sleep(0.02) #模拟网络io
    print(flag)

count = 100 #进行100次请求
start = time.time() #开始时刻
for i in range(count):
    wget(i)
end = time.time() #结束时刻
cost = end - start #耗时
print('cost:' + str(spend))

最终耗时超过2s,毫无疑问,这是最没效率的方案,仅仅是作为参照而已。

改进方案:多线程

多线程的计时方法显然不能照搬串行执行的测试方案,原因在于每个线程启动后,如果调用join()阻塞主线程,那么相当于串行执行,如果不调用join(),那么结束时刻end会在所有线程完成之前就返回,测试结果必然不准。所以我用了一个笨办法:在每个线程结束时打印当前时间戳,把控制台上最后一个时间戳减去线程开始执行的时间戳,就是运行耗时。

import time
import threading

mutex=threading.Lock() #初始化锁对象

def wget():
    time.sleep(0.02) #模拟网络io
    mutex.acquire() #加锁
    print('endtime:'+str(time.time())) #当前线程的结束时刻
    mutex.release() #释放锁

count = 100 #进行100次请求
start = time.time() #开始时刻
print('starttime:'+str(start))
for i in range(count):
    t = threading.Thread(target=wget)
    t.start()

最后结果约为0.08s,这个成绩显然比串行执行好得多,不过程序的运行效率不会随着线程数量的增长而线性增长,原因在于线程创建切换销毁时的开销,极端情况下会造成崩溃。如果线程数不可控制时,这种方案要慎用。

改进方案:线程池

为了解决上面提到的多线程的不足之处,这里使用线程池。

import time
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed, wait, ALL_COMPLETED

mutex=threading.Lock() #初始化锁对象

def wget():
    time.sleep(0.02) #模拟网络io
    mutex.acquire() #加锁
    print(threading.currentThread())
    mutex.release() #释放锁

size = 40 #线程池大小
count = 100 #进行100次请求
start = time.time() #开始时刻
pool = ThreadPoolExecutor(max_workers=size) #线程池对象
tasks = [pool.submit(wget) for i in range(count)]
wait(tasks, return_when=ALL_COMPLETED) #等待所有线程完成
end = time.time() #结束时刻
print('spend:' + str(end-start))

经过测试后发现,当线程池线程数量设置为40时,耗时最小,约为0.08s,与上一个方案相当,不过随着线程数量继续增加,线程池稳定的特点会显现出来。

改进方案:协程异步

import asyncio
import time

async def wget(flag): #async关键字表明这是一个异步操作
    await asyncio.sleep(0.02) #await相当于yield from
    print(flag)

count = 100 #进行100次请求
start = time.time() #开始时刻
loop = asyncio.get_event_loop() #事件循环对象
tasks = [wget(i) for i in range(count)]
loop.run_until_complete(asyncio.wait(tasks)) #等待所有协程完成
loop.close()
end = time.time() #结束时刻
print('spend:' + str(end - start))

这个方案的结果让我相当震惊,没有用到任何多线程技术,所有的操作在一个线程上完成,耗时0.06s,是所有方案中耗时最少的。协程更令人振奋的优点在于,协程创建的开销与线程相比完全可以忽略,这意味着,使用多协程可以处理更多的任务。

总结

显然在这几个方案中,协程是最具有优势的,将来如果有时间,我还会对多协程多进程的协同进行测试。

posted @ 2018-06-25 20:43  bubingy  阅读(297)  评论(0编辑  收藏  举报