首先介绍并发、并行、异步和同步的概念,然后介绍 Python 中实现这些技术的主要方式,包括多线程、多线程、协程 gevent 、asyncio、及 futures 等实现方式。
并发是指在一个时间段内发生若干时间的情况。
并行是指同一时刻发生若干事件的情况。
下面用单核和多核 CPU 的工作方式来说明这两个概念。
并发:在单核 CPU 的情况下,多任务操作系统的各任务是以并发的方式运行的,因为只有一个 CPU,所以各任务会以分时的方式在一段时间内分别占用 CPU 依次执行,如果在自己分得的时间段(又称时间片)内没有运行完成,则需等待下一次得到 CPU 使用权时会继续执行,直至完成。在这种方式下,各任务的时间片很小,切换速度很快,所以给我们的感觉是多个任务在同时进行,这就是并发。
并行:在多核 CPU 的情况下,因为有两个或两个以上可以同时工作的内核,所以就有可能各核上运行的任务能够同时进行,这就是并行。
提到同步和异步的概念,一般会涉及多个任务或事件的参与。可以在并发或并行的背景下理解同步和异步。
同步指的是并发或并行发生的各任务之间不是孤立地独自运行的,一个任务的进行可能需要在获得另一个任务给出的结果之后,或者只有一个任务完成或给出一个结果之后,另一个任务在获得这个结果之后才能继续运行。总之,各任务的运行会彼此相互制约,合拍前进,节奏和步调要协调好,否则就会出现错误。
异步指的是并发或并行发生的各任务之间彼此是独立运行的,不受各自的影响。这是与同步的主要区别。
适用于同步的情况:
当需要多个任务互相配合在并发或并行环境中合作完成时,就需要以同步的方式运行。
适用于异步的情况:
当需要将一个大的任务分解为若干个小的子任务时,各子任务可以分别独自完成,彼此之间不必互相协作,这时就可以考虑使它们异步地并行或异步地并发执行。这在数据抓取时是一种常见的模式,将若干要抓取的链接分成几组,然后对每组分别使用子任务进行抓取,待各子任务抓取结束后,可以将结果进行汇总,完成任务。
下面介绍各种 Python 支持的并发或并行方式时,使用的抓取的实例就是一个异步任务。
多线程是以并发的方式执行的,由于 Python 语言自身的设计限制,因此即使是多核的机器,Python 的多线程程序也只能运行在一个单核上以并发的方式运行。代码示例如下:
import threading
from bs4 import BeautifuSoup
import requests
def get_urls_in_pages(from_page_num,to_page_num):
url = r'http://college.koolearn.com/kaoyan/s/fs-0-0-0-0-0-0/?p='
urls = []
for i in range(from_page_num,to_page_num+1):
urls.append(url + str(i))
#下面的操作,用requets模块、BeautifulSoup模块来抓取数据
def main_test():
page_ranges_lst = [
(1,10),
(11,20),
(21,30),
(31,40),
]
th_lst = []
for page_ranges in page_ranges_lst:
th = threading.Thread(target=get_urls_in_pages,args=(page_ranges[0],page_ranges[1]))
th_lst.append(th) #将生成的县城都放入 th_list 列表中,此时 th 只是线程初始化后的实例,还未开始执行。
for th in th_lst:
th.start() #该语句这些线程在系统中接受系统的调度,异步地并发地执行了
for th in th_lst:
th.join() #等待各线程执行完毕后,再退出外层函数 main_test()
if __name__ == '__main__':
main_test()
多进程方式依赖于所在机器的处理器个数。在多核机器上进行多进程编程时,各核上运行的进程之间是并行执行的。可以利用进程池,使每一个内核上运行一个进程,当池中的进程数量大于内核总数时,待运行的进程会等待,直至其他进程运行完毕让出内核。,当系统内只有一个单核 CPU 时,多进程并行不会发生,此时各进程会依次占用 CPU 运行至完成。
下面的代码给出在 8 核处理器上运行多进程的程序
获得 CPU 可用的核数,命令如下:
from multiprocessing import cpu_count
print(cpu_count())
多进程抓取代码如下:
import multiprocessing
from bs4 import BeautifuSoup
import requests
def get_urls_in_pages(from_page_num,to_page_num):
url = r'http://college.koolearn.com/kaoyan/s/fs-0-0-0-0-0-0/?p='
urls = []
for i in range(from_page_num,to_page_num+1):
urls.append(url + str(i))
#下面的操作,用requets模块、BeautifulSoup模块来抓取数据
def main_test():
page_ranges_lst = [
(1,10),
(11,20),
(21,30),
(31,40),
]
pool = multiprocessing.Pool(process=4) # 4核CPU,该语句的作用可同时并行 4 个进程的进程池
for page_ranges in page_ranges_lst:
#apply_async 方法可以使进入进程池的进程以异步的方式并行运行
pool.apply_async(get_urls_in_pages,(page_ranges[0],page_ranges[1]))
pool.close()
pool.join()
if __name__ == '__main__':
mian_test()
一般在 Python 讨论协程时,都会与生成器(generator)联系在一起。生成器是一个函数,主要特点是生成器在返回值时,不是使用 return,而是使用 yield 关键字。在定义函数时,如果函数体中包含 yield 关键字,则该函数就被认为是一个生成器。
使用 gevent 异步库可以方便地实现基于协程的并发设计,可以利用 gevent 的 “monkey patch” 能力,将标准的 I/O 函数转为异步执行的函数,而代码基本不用变化。
在 gevent 中使用 greenlet 对象实现并发,greeenlet 就是协程,可以认为是一种轻量线程。
gevent 库的安装:
sudo pip3 install gevent
利用 gevent 库抓取的代码示例如下:
from multiprocessing import cpu_count
print(cpu_count())
多进程抓取代码如下:
import gevent
from gevent import monkey
monkey.patch_all()
from bs4 import BeautifuSoup
import requests
def get_urls_in_pages(from_page_num,to_page_num):
url = r'http://college.koolearn.com/kaoyan/s/fs-0-0-0-0-0-0/?p='
urls = []
for i in range(from_page_num,to_page_num+1):
urls.append(url + str(i))
#下面的操作,用requets模块、BeautifulSoup模块来抓取数据
def main_test():
page_ranges_lst = [
(1,10),
(11,20),
(21,30),
(31,40),
]
jobs = []
for page_range in page_ranges_lst:
jobs.append(gevent.spawn(get_urls_in_pages,page_range[0],page_range[1]))
gevent.joinall(jobs)
if __name__ == '__main__':
mian_test()
注意:
若没有如下两句,则会变为依次顺序抓取,失去了并发的能力。
from gevent import monkey
monkey.patch_all()
从 Python3.4 开始,Python 自身提供了 asyncio 库用于实现异步 I/O 程序设计,利用 asyncio 可以使用协程进行并发编程。asyncio 提供了对套接字 API 的全面支持,但在使用时涉及的细节问题比较多,可以引入另一个 aiohttp 库。
asyncio 库主要提供了异步并发编程的框架
aiohttp 库用于在抓取时发出 http 请求
asyncio 的安装
pip3 install asynci
aiohttp 的安装:
sudo pip3 install aiohttp
使用 asyncio 和 aiohttp 实现异步并发抓取的代码如下:
import asyncio
import aiohttp
import requests
from bs4 import BeautifulSoup
sem = asyncio.Semaphore(4) #设置Semaphore为4,说明在抓取时最多并发发出4个请求
@asyncio.coroutine
def get_urls_in_pages(from_page_num,to_page_num):
url = r'http://college.koolearn.com/kaoyan/s/fs-0-0-0-0-0-0/?p='
urls = []
for i in range(from_page_num,to_page_num+1):
urls.append(url + str(i))
for url in urls:
with(yield from sem):
response = yield from aiohttp.request('GET',url) #该语句功能是发出http请求,并在此处等待请求的结果
html = yield from response.read_and_close()
bs = BeautifulSoup(html.decode('utf-8'))
#以下的操作是抓取数据
def main_test():
page_ranges_lst = [
(1,10),
(11,20),
(21,30),
(31,40),
]
#下面三行语句代码完成的功能就是启动事件循环机制,生成待运行的诸协程,然后调度运行
loop = asyncio.get_event_loop()
f = asyncio.wait([get_urls_in_pages(page_range[0],page_range[1]) for page_range in page_ranges_lst])
loop.run_until_complete(f)
if __name__ == '__main__':
mian_test()