[python] ThreadPoolExecutor线程池和ProcessPoolExecutor进程池

引言

Python标准库为我们提供了threading和multiprocessing模块编写相应的多线程/多进程代码,但是当项目达到一定的规模,频繁创建/销毁进程或者线程是非常消耗资源的,这个时候我们就要编写自己的线程池/进程池,以空间换时间。但从Python3.2开始,标准库为我们提供了concurrent.futures模块,它提供了ThreadPoolExecutor和ProcessPoolExecutor两个类,实现了对threading和multiprocessing的进一步抽象,对编写线程池/进程池提供了直接的支持。

Executor和Future

concurrent.futures模块的基础是Exectuor,Executor是一个抽象类,它不能被直接使用。但是它提供的两个子类ThreadPoolExecutor和ProcessPoolExecutor却是非常有用,顾名思义两者分别被用来创建线程池和进程池的代码。我们可以将相应的tasks直接放入线程池/进程池,不需要维护Queue来操心死锁的问题,线程池/进程池会自动帮我们调度。

Future这个概念相信有java和nodejs下编程经验的朋友肯定不陌生了,你可以把它理解为一个在未来完成的操作,这是异步编程的基础,传统编程模式下比如我们操作queue.get的时候,在等待返回结果之前会产生阻塞,cpu不能让出来做其他事情,而Future的引入帮助我们在等待的这段时间可以完成其他的操作。关于在Python中进行异步IO可以阅读完本文之后参考我的Python并发编程之协程/异步IO。

p.s: 如果你依然在坚守Python2.x,请先安装futures模块。

pip install futures
ProcessPoolExecutor(n):n表示池里面存放多少个进程,之后的连接最大就是n的值

submit(fn,*args,**kwargs)  异步提交任务

map(func, *iterables, timeout=None, chunksize=1) 取代for循环submit的操作

shutdown(wait=True) 相当于进程池的pool.close()+pool.join()操作
wait=True,等待池内所有任务执行完毕回收完资源后才继续,--------》默认
wait=False,立即返回,并不会等待池内的任务执行完毕
但不管wait参数为何值,整个程序都会等到所有任务执行完毕
submit和map必须在shutdown之前

result(timeout=None)  #取得结果

add_done_callback(fn)  #回调函数

使用submit来操作线程池/进程池

from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor,as_completed
import time

#模拟网络请求的网络延迟
def get_html(times):
    time.sleep(times)
    print("get page {}s finished".format(times))
    return times


#创建一个大小为2的线程池
pool = ThreadPoolExecutor(max_workers=2)

#将上个任务提交到线程池,因为线程池的大小是2,所以必须等task1和task2中有一个完成之后才会将第三个任务提交到线程池
task1 = pool.submit(get_html,3)
task2 = pool.submit(get_html,2)
task3 = pool.submit(get_html,4)

#打印该任务是否执行完毕
print(task1.done())
#只有未被提交的到线程池(在等待提交的队列中)的任务才能够取消
print(task3.cancel())
time.sleep(4)#休眠4秒钟之后,线程池中的任务全部执行完毕,可以打印状态
print(task1.done())

print(task1.result())#该任务的return 返回值  该方法是阻塞的。
  1. ThreadPoolExecutor构造实例的时候,传入max_workers参数来设置线程池中最多能同时运行的线程数目。
  2. 使用submit函数来提交线程需要执行的任务(函数名和参数)到线程池中,并返回该任务的句柄(类似于文件、画图),注意submit()不是阻塞的,而是立即返回。
  3. 通过submit函数返回的任务句柄,能够使用done()方法判断该任务是否结束。上面的例子可以看出,由于任务有2s的延时,在task1提交后立刻判断,task1还未完成,而在延时4s之后判断,task1就完成了。
  4. 使用cancel()方法可以取消提交的任务,如果任务已经在线程池中运行了,就取消不了。这个例子中,线程池的大小设置为2,任务已经在运行了,所以取消失败。如果改变线程池的大小为1,那么先提交的是task1,task2还在排队等候,这是时候就可以成功取消。
  5. 使用result()方法可以获取任务的返回值。查看内部代码,发现这个方法是阻塞的。

as_completed

上面虽然提供了判断任务是否结束的方法,但是不能在主线程中一直判断啊。有时候我们是得知某个任务结束了,就去获取结果,而不是一直判断每个任务有没有结束。这是就可以使用as_completed方法一次取出所有任务的结果。

pool = ThreadPoolExecutor(max_workers=2)
urls = [2,3,4]
all_task = [pool.submit(get_html,url) for url in urls]

for future in as_completed(all_task):
    data = future.result()
    print("in main: get page {}s success".format(data))
 
 #echo
 # 执行结果 
 # get page 2s finished 
 # in main: get page 2s success 
 # get page 3s finished 
 # in main: get page 3s success 
 # get page 4s finished 
 # in main: get page 4s success

as_completed()方法是一个生成器,在没有任务完成的时候,会阻塞,在有某个任务完成的时候,会yield这个任务,就能执行for循环下面的语句,然后继续阻塞住,循环到所有的任务结束。从结果也可以看出,先完成的任务会先通知主线程。

map

除了上面的as_completed方法,还可以使用executor.map方法,但是有一点不同。

from concurrent.futures import ThreadPoolExecutor 
import time 
# 参数times用来模拟网络请求的时间 
def get_html(times):
	time.sleep(times) 
	print("get page {}s finished".format(times)) 
	return times 
executor = ThreadPoolExecutor(max_workers=2) 
urls = [3, 2, 4] # 并不是真的url 
	for data in executor.map(get_html, urls): 
		print("in main: get page {}s success".format(data))

#echo
#执行结果
# get page 2s finished 
# get page 3s finished 
# in main: get page 3s success 
# in main: get page 2s success 
# get page 4s finished 
# in main: get page 4s success

wait

wait方法可以让主线程阻塞,直到满足设定的要求。

from concurrent.futures import ThreadPoolExecutor, wait, ALL_COMPLETED, FIRST_COMPLETED 
import time
# 参数times用来模拟网络请求的时间 
def get_html(times): 
	time.sleep(times) 
	print("get page {}s finished".format(times)) 
	return times 
executor = ThreadPoolExecutor(max_workers=2) 
urls = [3, 2, 4] # 并不是真的url 
all_task = [executor.submit(get_html, (url)) for url in urls] 
wait(all_task, return_when=ALL_COMPLETED) 
print("main")

#echo
# 执行结果 
# get page 2s finished
# get page 3s finished
# get page 4s finished
# main

wait方法接收3个参数,等待的任务序列、超时时间以及等待条件。等待条件return_when默认为ALL_COMPLETED,表明要等待所有的任务都结束。可以看到运行结果中,确实是所有任务都完成了,主线程才打印出main。等待条件还可以设置为FIRST_COMPLETED,表示第一个任务完成就停止等待。

ProcessPoolExecutor使用

from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
import time,random,os

def task(n):
    print('%s is running'% os.getpid())
    time.sleep(random.randint(1,3))
    return n
def handle(res):
    res=res.result()
    print("handle res %s"%res)

if __name__ == '__main__':
    #同步调用
    # pool=ProcessPoolExecutor(8)
    #
    # for i in range(13):
    #     pool.submit(task, i).result() #变成同步调用,串行了,等待结果
    # # pool.shutdown(wait=True) #关门等待所有进程完成
    # pool.shutdown(wait=False)#默认wait就等于True
    # # pool.submit(task,3333) #shutdown后不能使用submit命令
    #
    # print('主')

    #异步调用
    pool=ProcessPoolExecutor(8)
    for i in range(13):
         obj=pool.submit(task,i)
         obj.add_done_callback(handle) #这里用到了回调函数
    pool.shutdown(wait=True) #关门等待所有进程完成
    print('主')
##注意,创建进程池必须在if __name__ == '__main__':中,否则会报错
##其他的用法和创建线程池的一样
from concurrent.futures import ThreadPoolExecutor
from urllib import request
from threading import current_thread
import time

def get(url):
    print('%s get %s'%(current_thread().getName(),url))
    response=request.urlopen(url)
    time.sleep(2)
    # print(response.read().decode('utf-8'))
    return{'url':url,'content':response.read().decode('utf-8')}

def parse(res):
    res=res.result()
    print('parse:[%s] res:[%s]'%(res['url'],len(res['content'])))

# get('http://www.baidu.com')
if __name__ == '__main__':
    pool=ThreadPoolExecutor(2)

    urls=[
        'https://www.baidu.com',
        'https://www.python.org',
        'https://www.openstack.org',
        'https://www.openstack.org',
        'https://www.openstack.org',
        'https://www.openstack.org',
        'https://www.openstack.org',
        'https://www.openstack.org',

    ]

    for url in urls:
        pool.submit(get,url).add_done_callback(parse)

你可能感兴趣的:(python)