为了提高程序并行运行的效率,我们会采取多进程和多线程的方法。通常来说,多进程适用于计算密集型任务,多线程适用于IO密集型任务,如网络爬虫。

关于多线程和多进程的区别,请参考这个表格。

下面将使用python标准库的multiprocessing包来尝试多线程的操作,在python中调用多线程要使用multiprocessing.dummy,如果是多进程则去掉dummy即可。提醒特别注意,这里的多线程仍然会受到GIL(全局解释器锁)的限制,理论上并没有真正做到并发,只是在IO密集型中的任务中可以利用等待时间切换到其他线程。

from multiprocessing.dummy import Pool as ThreadPool

import time

import requests


# 先写一个最简单的访问网页,获取网页内容的函数geturl

def geturl(url):
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36 QIHU 360SE'}
text = requests.get(url, headers=headers).text[:100]
return text

# 需要循环访问的url列表
urls = [
'http://www.baidu.com',
'http://home.baidu.com/',
'http://tieba.baidu.com/',
'http://zhidao.baidu.com/',
'http://music.baidu.com/',
'http://image.baidu.com/',
'http://python-china.org/'
]

# 使用普通循环的方法运行
start = time.time()
results = list(map(geturl, urls))
print('Normal:', time.time() - start)

# 使用多线程的方式运行,在这里我们使用processes参数为4
start2 = time.time()
pool = ThreadPool(processes=4)
result2 = pool.map_async(geturl, urls)
pool.close()
pool.join()

print('Thread Pool:', time.time() - start2)结果非常地amazing,使用普通循环方法耗时约6s,而使用多线程方法则只要1s。

在这一部分中我们使用了map_async这个函数,此外还有一种调用多线程的方法apply_async,区别在于map_async函数只能接受单一参数,通过将迭代器分块的方式来执行,而apply函数支持传入多个参数。在这个例子中用apply_async也大约耗时1s。

start2 = time.time()
pool = ThreadPool(processes=4)
for url in urls:
pool.apply_async(geturl, (url,))
pool.close()
pool.join()
print('Thread Pool:', time.time() - start2)

此外还有两个简化方法map,apply,和上面介绍的方法在于这两个方法是非异步阻塞型的,也就是一个线程出问题会将整个进程卡住。

举个例子,我们在待访问的网页列表中加一个无效网址https://www.qwerty.com,这里稍微改动geturl函数,创建一个空列表观察哪些网址被正常运行了。当使用apply_aysnc或是map_async时程序正常运行并结束,最后的list中包含7个元素,除了那个无效网址,而当仅仅使用apply或map时我们发现,因为这个无效的网址堵塞,程序报错并退出了。

list = []
def geturl(url):
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36 QIHU 360SE'}
text = requests.get(url, headers=headers).text[:100]
# 添加一行
list.append(url)
return text


urls = [
'http://www.baidu.com',
'http://home.baidu.com/',
'http://tieba.baidu.com/',
'http://zhidao.baidu.com/',
'https://www.qwerty.com/', # 添加一个无效网站
'http://music.baidu.com/',
'http://image.baidu.com/',
'http://python-china.org/'
]

apply_async和map_async方法还有一个参数是回调函数,使用方法相同,区别在于前者是逐一返回回调函数,而后者则要再所有网址运行完后一次性返回,以apply_async为例:

def geturl(url):
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36 QIHU 360SE'}
text = requests.get(url, headers=headers).text[:100]
return text, url
# 加入一个返回运行时间的回调函数
def callback(x):
print(datetime.datetime.strftime(datetime.datetime.now(), '%F%H:%M:%S.%f') + ' : callback ' + x[1])
start2 = time.time()
pool = ThreadPool(processes=4)
for url in urls:
pool.apply_async(geturl, (url,), callback=callback)
pool.close()
pool.join()
print('Thread Pool:', time.time() - start2)
运行结果如下:
2020-10-07 19:47:11.878021 : callback http://home.baidu.com/
2020-10-07 19:47:11.886995 : callback http://www.baidu.com
2020-10-07 19:47:12.210958 : callback http://image.baidu.com/
2020-10-07 19:47:12.337566 : callback http://zhidao.baidu.com/
2020-10-07 19:47:12.386963 : callback http://tieba.baidu.com/
2020-10-07 19:47:12.568395 : callback http://music.baidu.com/
2020-10-07 19:47:12.961023 : callback http://python-china.org/
Thread Pool: 1.4119532108306885

本文简单地综述了python中多线程最基础的部分,能力有效,如有谬误请不吝指出。