前言
看到越来越多的大佬都在使用python的异步IO,协程等概念来实现高效的IO处理过程,可是我对这些概念还不太懂,就学习了一下。 因为是初学者,在理解上有很多不到位的地方,如果有错误,还希望能够有人积极帮我指出。
下面就使用一个简单的爬虫的例子,通过一步一步的改进,最后来用异步IO的方式实现。
1. 阻塞的IO
我们要实现一个爬虫,去爬百度首页n次,最简单的想法就是依次下载,从建立socket连接到发送网络请求再到读取响应数据,顺序进行。
代码如下:
1 import time 2 import socket 3 import sys 4 5 def doRequest(): 6 sock = socket.socket() 7 sock.connect(('www.baidu.com',80)) 8 sock.send("GET / HTTP/1.1\r\nHost: www.baidu.com\r\nConnection: Close\r\n\r\n".encode("utf-8")) 9 response = sock.recv(1024) 10 return response 11 12 def main(): 13 start = time.time() 14 for i in range(int(sys.argv[1])): 15 doRequest() 16 print("spend time : %s" %(time.time()-start)) 17 18 main()
因为socket是阻塞方式调用的,所以cpu执行到sock.connect()
,sock.recv()
,就会一直卡在那里直到socket的状态就绪,所以浪费了很多的CPU时间。
请求10次和20次的时间分别如下所示:
1 ? python3 1.py 10 2 spend time : 0.9282660484313965 3 ? python3 1.py 20 4 spend time : 1.732438087463379
可以看到,速度慢的跟蜗牛一样。
2. 改进1-并发
为了加快请求的速度,很容易想到我们可以使用并发的方式进行,那么最好的方式就是使用多线程了。修改后的代码如下:
1 # 多线程 2 3 import time 4 import socket 5 import sys 6 import threading 7 8 def doRequest(): 9 sock = socket.socket() 10 sock.connect(('www.baidu.com',80)) 11 sock.send("GET / HTTP/1.1\r\nHost: www.baidu.com\r\nConnection: Close\r\n\r\n".encode("utf-8")) 12 response = sock.recv(1024) 13 return response 14 15 def main(): 16 start = time.time() 17 threads = [] 18 for i in range(int(sys.argv[1])): 19 # doRequest() 20 threads.append(threading.Thread(target=doRequest,args=())) 21 for i in threads: 22 i.start() 23 for i in threads: 24 i.join() 25 print("spend time : %s" %(time.time()-start))
使用线程之后,看一下请求10次和20次的时间:
1 ? python3 2.py 10 2 spend time : 0.1124269962310791 3 ? python3 2.py 20 4 spend time : 0.15438294410705566
速度明显快了很多,几乎是刚才的10倍了。
但是python的线程是有问题的,因为一个python进程中,同一时刻只允许一个线程运行,正在执行的线程会获取到GPL。做阻塞的系统调用时,例如sock.connect()
,sock.recv()
时,当前线程会释放GIL,让别的线程有机会获取GPL,然后执行。但是这种获取GPL的调度策略是抢占式的,以保证同等优先级的线程都有均等的执行机会,那带来的问题是:并不知道下一时刻是哪个线程被运行,也不知道它正要执行的代码是什么。所以就可能存在竞态条件。这种竞争有可能使某些线程处于劣势,导致一直获取不到GPL
比如如下的情况,线程1执行的代码如下:
1 flag = True 2 while flag: 3 ..... # 这里省略一些复杂的操作,会调用多次IO操作 4 time.sleep(1)
可以看到,线程1的任务非常简单,而线程2的任务非常复杂,这就会导致CPU不停地去执行线程1,而真正做实际工作的线程2却很少被调度到,导致了浪费了大量的CPU资源。
3. 改进2-非阻塞方式
在第一个例子中,我们意识到浪费了大量的时间,是因为我们用了阻塞的IO,导致CPU在卡在那里等待IO的就绪,那使用非阻塞的IO,是不是就可以解决这个问题了。
代码如下:
1 import time 2 import socket 3 import sys 4 5 def doRequest(): 6 sock = socket.socket() 7 sock.setblocking(False) 8 try: 9 sock.connect(('www.baidu.com',80)) 10 except BlockingIOError: 11 pass 12 13 # 因为设置为非阻塞模式了,不知道何时socket就绪,需要不停的监控socket的状态 14 while True: 15 try: 16 sock.send("GET / HTTP/1.1\r\nHost: www.baidu.com\r\nConnection: Close\r\n\r\n".encode("utf-8")) 17 # 直到send 不抛出异常,就发送成功了 18 break 19 except OSError: 20 pass 21 while True: 22 try: 23 response = sock.recv(1024) 24 break 25 except OSError: 26 pass 27 return response 28 def main(): 29 start = time.time() 30 for i in range(int(sys.argv[1])): 31 doRequest() 32 print("spend time : %s" %(time.time()-start)) 33 34 main()
sock.setblocking(False)
把socket设置为非阻塞式的,也就是说执行完sock.connect()
和sock.recv()
之后,CPU不再等待IO了,会继续往下执行,来看一下执行时间:
1 ? python3 3.py 10 2 spend time : 1.0597507953643799 3 ? python3 3.py 20 4 spend t