初识
Python
中已经有了threading
模块,为什么还需要线程池呢,线程池又是什么东西呢?在介绍线程同步的信号量机制的时候,举得例子是爬虫的例子,需要控制同时爬取的线程数,例子中创建了20个线程,而同时只允许3个线程在运行,但是20个线程都需要创建和销毁,线程的创建是需要消耗系统资源的,有没有更好的方案呢?其实只需要三个线程就行了,每个线程各分配一个任务,剩下的任务排队等待,当某个线程完成了任务的时候,排队任务就可以安排给这个线程继续执行。
这就是线程池的思想(当然没这么简单),但是自己编写线程池很难写的比较完美,还需要考虑复杂情况下的线程同步,很容易发生死锁。从Python3.2
开始,标准库为我们提供了concurrent.futures
模块,它提供了ThreadPoolExecutor
和ProcessPoolExecutor
两个类,实现了对threading
和multiprocessing
的进一步抽象(这里主要关注线程池),不仅可以帮我们自动调度线程,还可以做到:
- 主线程可以获取某一个线程(或者任务的)的状态,以及返回值。
- 当一个线程完成的时候,主线程能够立即知道。
- 让多线程和多进程的编码接口一致。
实例
简单使用
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)
# 通过submit函数提交执行的函数到线程池中,submit函数立即返回,不阻塞
task1 = executor.submit(get_html, (3))
task2 = executor.submit(get_html, (2))
# done方法用于判定某个任务是否完成
print(task1.done())
# cancel方法用于取消某个任务,该任务没有放入线程池中才能取消成功
print(task2.cancel())
time.sleep(4)
print(task1.done())
# result方法可以获取task的执行结果
print(task1.result())
# 执行结果
# False # 表明task1未执行完成
# False # 表明task2取消失败,因为已经放入了线程池中
# get page 2s finished
# get page 3s finished
# True # 由于在get page 3s finished之后才打印,所以此时task1必然完成了
# 3 # 得到task1的任务返回值
-
ThreadPoolExecutor
构造实例的时候,传入max_workers
参数来设置线程池中最多能同时运行的线程数目。 - 使用
submit
函数来提交线程需要执行的任务(函数名和参数)到线程池中,并返回该任务的句柄(类似于文件、画图),注意submit()
不是阻塞的,而是立即返回。 - 通过
submit
函数返回的任务句柄,能够使用done()
方法判断该任务是否结束。上面的例子可以看出,由于任务有2s的延时,在task1
提交后立刻判断,task1
还未完成,而在延时4s之后判断,task1
就完成了。 - 使用
cancel()
方法可以取消提交的任务,如果任务已经在线程池中运行了,就取消不了。这个例子中,线程池的大小设置为2,任务已经在运行了,所以取消失败。如果改变线程池的大小为1,那么先提交的是task1
,task2
还在排队等候,这是时候就可以成功取消。 - 使用
result()
方法可以获取任务的返回值。查看内部代码,发现这个方法是阻塞的。
as_completed
上面虽然提供了判断任务是否结束的方法,但是不能在主线程中一直判断啊。有时候我们是得知某个任务结束了,就去获取结果,而不是一直判断每个任务有没有结束。这是就可以使用as_completed
方法一次取出所有任务的结果。
from concurrent.futures import ThreadPoolExecutor, as_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]
for future in as_completed(all_task):
data = future.result()
print("in main: get page {}s success".format(data))
# 执行结果
# 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))
# 执行结果
# 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
使用map
方法,无需提前使用submit
方法,map
方法与python
标准库中的map
含义相同,都是将序列中的每个元素都执行同一个函数。上面的代码就是对urls
的每个元素都执行get_html
函数,并分配各线程池。可以看到执行结果与上面的as_completed
方法的结果不同,输出顺序和urls
列表的顺序相同,就算2s的任务先执行完成,也会先打印出3s的任务先完成,再打印2s的任务完成。
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")
# 执行结果
# get page 2s finished
# get page 3s finished
# get page 4s finished
# main
wait
方法接收3个参数,等待的任务序列、超时时间以及等待条件。等待条件return_when
默认为ALL_COMPLETED
,表明要等待所有的任务都结束。可以看到运行结果中,确实是所有任务都完成了,主线程才打印出main
。等待条件还可以设置为FIRST_COMPLETED
,表示第一个任务完成就停止等待。
源码分析
cocurrent.future
模块中的future
的意思是未来对象,可以把它理解为一个在未来完成的操作,这是异步编程的基础 。在线程池submit()
之后,返回的就是这个future
对象,返回的时候任务并没有完成,但会在将来完成。也可以称之为task的返回容器,这个里面会存储task的结果和状态。那ThreadPoolExecutor
内部是如何操作这个对象的呢?
下面简单介绍ThreadPoolExecutor
的部分代码:
-
init方法
扫描二维码关注公众号,回复: 12026320 查看本文章
init
方法中主要重要的就是任务队列和线程集合,在其他方法中需要使用到。
-
submit方法
-
submit
中有两个重要的对象,_base.Future()
和_WorkItem()
对象,_WorkItem()
对象负责运行任务和对future
对象进行设置,最后会将future
对象返回,可以看到整个过程是立即返回的,没有阻塞。 -
adjust_thread_count方法
这个方法的含义很好理解,主要是创建指定的线程数。但是实现上有点难以理解,比如线程执行函数中的weakref.ref,涉及到了弱引用等概念,留待以后理解。
-
_WorkItem对象
_WorkItem
对象的职责就是执行任务和设置结果。这里面主要复杂的还是self.future.set_result(result)
。 -
线程执行函数--_worker
这是线程池创建线程时指定的函数入口,主要是从队列中依次取出task执行,但是函数的第一个参数还不是很明白。留待以后。
总结
- future的设计理念很棒,在线程池/进程池和携程中都存在future对象,是异步编程的核心。
- ThreadPoolExecutor 让线程的使用更加方便,减小了线程创建/销毁的资源损耗,无需考虑线程间的复杂同步,方便主线程与子线程的交互。
- 线程池的抽象程度很高,多线程和多进程的编码接口一致。
未完成
- 对future模块的理解。
- weakref.ref是什么?
- 线程执行函数入口_worker的第一个参数的意思。
系统启动一个新线程的成本是比较高的,因为它涉及与操作系统的交互。在这种情形下,使用线程池可以很好地提升性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。
线程池在系统启动时即创建大量空闲的线程,程序只要将一个函数提交给线程池,线程池就会启动一个空闲的线程来执行它。当该函数执行结束后,该线程并不会死亡,而是再次返回到线程池中变成空闲状态,等待执行下一个函数。
此外,使用线程池可以有效地控制系统中并发线程的数量。当系统中包含有大量的并发线程时,会导致系统性能急剧下降,甚至导致 Python 解释器崩溃,而线程池的最大线程数参数可以控制系统中并发线程的数量不超过此数。
线程池的使用
线程池的基类是 concurrent.futures 模块中的 Executor,Executor 提供了两个子类,即 ThreadPoolExecutor 和 ProcessPoolExecutor,其中 ThreadPoolExecutor 用于创建线程池,而 ProcessPoolExecutor 用于创建进程池。
如果使用线程池/进程池来管理并发编程,那么只要将相应的 task 函数提交给线程池/进程池,剩下的事情就由线程池/进程池来搞定。
Exectuor 提供了如下常用方法:- submit(fn, *args, **kwargs):将 fn 函数提交给线程池。*args 代表传给 fn 函数的参数,*kwargs 代表以关键字参数的形式为 fn 函数传入参数。
- map(func, *iterables, timeout=None, chunksize=1):该函数类似于全局函数 map(func, *iterables),只是该函数将会启动多个线程,以异步方式立即对 iterables 执行 map 处理。
- shutdown(wait=True):关闭线程池。
程序将 task 函数提交(submit)给线程池后,submit 方法会返回一个 Future 对象,Future 类主要用于获取线程任务函数的返回值。由于线程任务会在新线程中以异步方式执行,因此,线程执行的函数相当于一个“将来完成”的任务,所以 Python 使用 Future 来代表。
实际上,在 Java 的多线程编程中同样有 Future,此处的 Future 与 Java 的 Future 大同小异。
Future 提供了如下方法:- cancel():取消该 Future 代表的线程任务。如果该任务正在执行,不可取消,则该方法返回 False;否则,程序会取消该任务,并返回 True。
- cancelled():返回 Future 代表的线程任务是否被成功取消。
- running():如果该 Future 代表的线程任务正在执行、不可被取消,该方法返回 True。
- done():如果该 Funture 代表的线程任务被成功取消或执行完成,则该方法返回 True。
- result(timeout=None):获取该 Future 代表的线程任务最后返回的结果。如果 Future 代表的线程任务还未完成,该方法将会阻塞当前线程,其中 timeout 参数指定最多阻塞多少秒。
- exception(timeout=None):获取该 Future 代表的线程任务所引发的异常。如果该任务成功完成,没有异常,则该方法返回 None。
- add_done_callback(fn):为该 Future 代表的线程任务注册一个“回调函数”,当该任务成功完成时,程序会自动触发该 fn 函数。
在用完一个线程池后,应该调用该线程池的 shutdown() 方法,该方法将启动线程池的关闭序列。调用 shutdown() 方法后的线程池不再接收新任务,但会将以前所有的已提交任务执行完成。当线程池中的所有任务都执行完成后,该线程池中的所有线程都会死亡。
使用线程池来执行线程任务的步骤如下:- 调用 ThreadPoolExecutor 类的构造器创建一个线程池。
- 定义一个普通函数作为线程任务。
- 调用 ThreadPoolExecutor 对象的 submit() 方法来提交线程任务。
- 当不想提交任何任务时,调用 ThreadPoolExecutor 对象的 shutdown() 方法来关闭线程池。
下面程序示范了如何使用线程池来执行线程任务:
- from concurrent.futures import ThreadPoolExecutor
- import threading
- import time
- # 定义一个准备作为线程任务的函数
- def action(max):
- my_sum = 0
- for i in range(max):
- print(threading.current_thread().name + ' ' + str(i))
- my_sum += i
- return my_sum
- # 创建一个包含2条线程的线程池
- pool = ThreadPoolExecutor(max_workers=2)
- # 向线程池提交一个task, 50会作为action()函数的参数
- future1 = pool.submit(action, 50)
- # 向线程池再提交一个task, 100会作为action()函数的参数
- future2 = pool.submit(action, 100)
- # 判断future1代表的任务是否结束
- print(future1.done())
- time.sleep(3)
- # 判断future2代表的任务是否结束
- print(future2.done())
- # 查看future1代表的任务返回的结果
- print(future1.result())
- # 查看future2代表的任务返回的结果
- print(future2.result())
- # 关闭线程池
- pool.shutdown()
上面程序中,第 13 行代码创建了一个包含两个线程的线程池,接下来的两行代码只要将 action() 函数提交(submit)给线程池,该线程池就会负责启动线程来执行 action() 函数。这种启动线程的方法既优雅,又具有更高的效率。
当程序把 action() 函数提交给线程池时,submit() 方法会返回该任务所对应的 Future 对象,程序立即判断 futurel 的 done() 方法,该方法将会返回 False(表明此时该任务还未完成)。接下来主程序暂停 3 秒,然后判断 future2 的 done() 方法,如果此时该任务已经完成,那么该方法将会返回 True。
程序最后通过 Future 的 result() 方法来获取两个异步任务返回的结果。读者可以自己运行此代码查看运行结果,这里不再演示。
当程序使用 Future 的 result() 方法来获取结果时,该方法会阻塞当前线程,如果没有指定 timeout 参数,当前线程将一直处于阻塞状态,直到 Future 代表的任务返回。
获取执行结果
前面程序调用了 Future 的 result() 方法来获取线程任务的运回值,但该方法会阻塞当前主线程,只有等到钱程任务完成后,result() 方法的阻塞才会被解除。
如果程序不希望直接调用 result() 方法阻塞线程,则可通过 Future 的 add_done_callback() 方法来添加回调函数,该回调函数形如 fn(future)。当线程任务完成后,程序会自动触发该回调函数,并将对应的 Future 对象作为参数传给该回调函数。
下面程序使用 add_done_callback() 方法来获取线程任务的返回值:- from concurrent.futures import ThreadPoolExecutor
- import threading
- import time
- # 定义一个准备作为线程任务的函数
- def action(max):
- my_sum = 0
- for i in range(max):
- print(threading.current_thread().name + ' ' + str(i))
- my_sum += i
- return my_sum
- # 创建一个包含2条线程的线程池
- with ThreadPoolExecutor(max_workers=2) as pool:
- # 向线程池提交一个task, 50会作为action()函数的参数
- future1 = pool.submit(action, 50)
- # 向线程池再提交一个task, 100会作为action()函数的参数
- future2 = pool.submit(action, 100)
- def get_result(future):
- print(future.result())
- # 为future1添加线程完成的回调函数
- future1.add_done_callback(get_result)
- # 为future2添加线程完成的回调函数
- future2.add_done_callback(get_result)
- print('--------------')
主程序的最后一行代码打印了一条横线。由于程序并未直接调用 future1、future2 的 result() 方法,因此主线程不会被阻塞,可以立即看到输出主线程打印出的横线。接下来将会看到两个新线程并发执行,当线程任务执行完成后,get_result() 函数被触发,输出线程任务的返回值。
另外,由于线程池实现了 上下文管理协议(Context Manage Protocol),因此,程序可以使用 with 语句来管理线程池,这样即可避免手动关闭线程池,如上面的程序所示。
此外,Exectuor 还提供了一个map(func, *iterables, timeout=None, chunksize=1)
方法,该方法的功能类似于全局函数 map(),区别在于线程池的 map() 方法会为 iterables 的每个元素启动一个线程,以并发方式来执行 func 函数。这种方式相当于启动 len(iterables) 个线程,井收集每个线程的执行结果。
例如,如下程序使用 Executor 的 map() 方法来启动线程,并收集线程任务的返回值:
- from concurrent.futures import ThreadPoolExecutor
- import threading
- import time
- # 定义一个准备作为线程任务的函数
- def action(max):
- my_sum = 0
- for i in range(max):
- print(threading.current_thread().name + ' ' + str(i))
- my_sum += i
- return my_sum
- # 创建一个包含4条线程的线程池
- with ThreadPoolExecutor(max_workers=4) as pool:
- # 使用线程执行map计算
- # 后面元组有3个元素,因此程序启动3条线程来执行action函数
- results = pool.map(action, (50, 100, 150))
- print('--------------')
- for r in results:
- print(r)
运行上面程序,同样可以看到 3 个线程并发执行的结果,最后通过 results 可以看到 3 个线程任务的返回结果。
通过上面程序可以看出,使用 map() 方法来启动线程,并收集线程的执行结果,不仅具有代码简单的优点,而且虽然程序会以并发方式来执行 action() 函数,但最后收集的 action() 函数的执行结果,依然与传入参数的结果保持一致。也就是说,上面 results 的第一个元素是 action(50) 的结果,第二个元素是 action(100) 的结果,第三个元素是 action(150) 的结果。这里要考虑一个问题,以上线程池的实现都是封装好的,任务只能在线程池初始化的时候添加一次,那么,假设我现在有这样一个需求,需要在线程池运行时,再往里面添加新的任务(注意,是新任务,不是新线程),那么要怎么办?
其实有两种方式:
3.1、重写threadpool或者future的函数:
这个方法需要阅读源模块的源码,必须搞清楚源模块线程池的实现机制才能正确的根据自己的需要重写其中的方法。
3.2、自己构建一个线程池:
这个方法就需要对线程池的有一个清晰的了解了,附上我自己构建的一个线程池:
#! /usr/bin/env python # -*- coding: utf-8 -*- import threading import Queue import hashlib import logging from utils.progress import PrintProgress from utils.save import SaveToSqlite class ThreadPool(object): def __init__(self, thread_num, args): self.args = args self.work_queue = Queue.Queue() self.save_queue = Queue.Queue() self.threads = [] self.running = 0 self.failure = 0 self.success = 0 self.tasks = { } self.thread_name = threading.current_thread().getName() self.__init_thread_pool(thread_num) # 线程池初始化 def __init_thread_pool(self, thread_num): # 下载线程 for i in range(thread_num): self.threads.append(WorkThread(self)) # 打印进度信息线程 self.threads.append(PrintProgress(self)) # 保存线程 self.threads.append(SaveToSqlite(self, self.args.dbfile)) # 添加下载任务 def add_task(self, func, url, deep): # 记录任务,判断是否已经下载过 url_hash = hashlib.new('md5', url.encode("utf8")).hexdigest() if not url_hash in self.tasks: self.tasks[url_hash] = url self.work_queue.put((func, url, deep)) logging.info("{0} add task {1}".format(self.thread_name, url.encode("utf8"))) # 获取下载任务 def get_task(self): # 从队列里取元素,如果block=True,则一直阻塞到有可用元素为止。 task = self.work_queue.get(block=False) return task def task_done(self): # 表示队列中的某个元素已经执行完毕。 self.work_queue.task_done() # 开始任务 def start_task(self): for item in self.threads: item.start() logging.debug("Work start") def increase_success(self): self.success += 1 def increase_failure(self): self.failure += 1 def increase_running(self): self.running += 1 def decrease_running(self): self.running -= 1 def get_running(self): return self.running # 打印执行信息 def get_progress_info(self): progress_info = { } progress_info['work_queue_number'] = self.work_queue.qsize() progress_info['tasks_number'] = len(self.tasks) progress_info['save_queue_number'] = self.save_queue.qsize() progress_info['success'] = self.success progress_info['failure'] = self.failure return progress_info def add_save_task(self, url, html): self.save_queue.put((url, html)) def get_save_task(self): save_task = self.save_queue.get(block=False) return save_task def wait_all_complete(self): for item in self.threads: if item.isAlive(): # join函数的意义,只有当前执行join函数的线程结束,程序才能接着执行下去 item.join() # WorkThread 继承自threading.Thread class WorkThread(threading.Thread): # 这里的thread_pool就是上面的ThreadPool类 def __init__(self, thread_pool): threading.Thread.__init__(self) self.thread_pool = thread_pool #定义线程功能方法,即,当thread_1,...,thread_n,调用start()之后,执行的操作。 def run(self): print (threading.current_thread().getName()) while True: try: # get_task()获取从工作队列里获取当前正在下载的线程,格式为func,url,deep do, url, deep = self.thread_pool.get_task() self.thread_pool.increase_running() # 判断deep,是否获取新的链接 flag_get_new_link = True if deep >= self.thread_pool.args.deep: flag_get_new_link = False # 此处do为工作队列传过来的func,返回值为一个页面内容和这个页面上所有的新链接 html, new_link = do(url, self.thread_pool.args, flag_get_new_link) if html == '': self.thread_pool.increase_failure() else: self.thread_pool.increase_success() # html添加到待保存队列 self.thread_pool.add_save_task(url, html) # 添加新任务,即,将新页面上的不重复的链接加入工作队列。 if new_link: for url in new_link: self.thread_pool.add_task(do, url, deep + 1) self.thread_pool.decrease_running() # self.thread_pool.task_done() except Queue.Empty: if self.thread_pool.get_running() <= 0: break except Exception, e: self.thread_pool.decrease_running() # print str(e) break
https://www.jianshu.com/p/b9b3d66aa0be
http://c.biancheng.net/view/2627.html
https://www.cnblogs.com/zhang293/p/7954353.html