更多优质内容
请关注公众号

Python多线程和多进程(五)  多线程管理——线程池-张柏沛IT博客

正文内容

Python多线程和多进程(五) 多线程管理——线程池

栏目:Python 系列:Python多线程和多进程系列 发布时间:2020-04-11 14:28 浏览量:4500

多线程管理 - 线程池

Q1:首先,为什么需要线程池?
因为我们希望能够保持一定数量的并发线程处于执行状态,让处于执行状态的线程数不至于太少也不至于太多,提高任务执行效率。

Q2:信号量 semaphore 也可以实现保持一定数量的并发线程,为什么还要用线程池?
1. 因为线程池可以监控每一个线程和任务的执行状态,可以获取某个任务的返回值,而信号量不行。

2. 线程池中的每个线程完成一个任务后不会销毁而是会继续执行下一个任务,达到线程复用的效果,而信号量不行。要知道线程是宝贵的资源,随意的创建和结束线程是对资源的一种浪费,频繁的创建和关闭线程也会降低效率。
所以,线程池才是多线程的最佳实践方式!

 

线程池的使用方法:

from concurrent.future import ThreadPoolExecutor

executor = ThreadPoolExecutor(max_workers)  # 创建线程池对象,max_workers指定可以同时处于运行状态的线程数

task = executor.submit(fn,args)  # 将一个任务放入任务队列,并且start()一个线程,并让这个线程从队列取出和完成fn这个任务,args是fn任务函数的参数;返回的task是一个Future对象

task.done()     # 判断任务是否完成

task.cancel()   # 取消任务,只有任务还未开始执行才可以取消

task.result()   # 获取任务的结果(返回值),这个方法是阻塞的

 

下面分析一下 ThreadPoolExecutor类和Future类 的源码Future源码:
 

class Future(object):
    """Represents the result of an asynchronous computation."""
    # 翻译:Future是一个存储着异步任务的执行结果和运行状态的容器;当然Future不只是一个容器,还负责一个任务的结果获取,取消,判断是否完成,异常设置等功能。一个Future对应着一个任务(任务函数)的结果和状态。

    def __init__(self):
        """Initializes the future. Should not be called by clients."""
        # 翻译:Future对象不能在客户端脚本实例化,只能在ThreadPoolExecutor这样的python内部代码中实例化
        
        self._condition = threading.Condition()     # 创建了一个条件变量
        self._state = PENDING                       # 任务状态默认是“正在执行”
        self._result = None                         # 任务结果默认为空,因为任务还没开始执行或者正在执行
        self._exception = None              
        self._waiters = []                          
        self._done_callbacks = []                   

    def _invoke_callbacks(self):
        for callback in self._done_callbacks:
            try:
                callback(self)
            except Exception:
                LOGGER.exception('exception calling callback for %r', self)

    # 用于取消执行某一个任务
    def cancel(self):
        """Cancel the future if possible.

        Returns True if the future was cancelled, False otherwise. A future
        cannot be cancelled if it is running or has already completed.
        """
        # 翻译: 不能取消一个正在执行或已执行完的任务。只能取消还未执行的任务。什么是未执行的任务?线程池ThreadPoolExecutor限定了能同时处于执行状态的线程数n,但往线程池中添加的任务数m如果超过了n,m个任务中只有n个可以同时执行,那么m-n那部分任务需要等待前面的任务执行完才能开始执行,这些就是还未执行的任务。
        
        with self._condition:
            if self._state in [RUNNING, FINISHED]:
                return False

            if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]:
                return True

            self._state = CANCELLED
            self._condition.notify_all()    # 取消任务的时候,会唤醒 get_result()中条件变量的wait(),目的是为了告诉get_result说:“不必等待已取消的任务的结果了”

        self._invoke_callbacks()
        return True

    # 判断这个Future任务是否处于“已被取消”状态
    def cancelled(self):
        """Return True if the future was cancelled."""
        with self._condition:
            return self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]

    # 判断这个Future任务是否处于“正在运行”状态
    def running(self):
        """Return True if the future is currently executing."""
        with self._condition:
            return self._state == RUNNING

    # 判断这个Future任务是否处于“执行完毕”状态
    def done(self):
        """Return True of the future was cancelled or finished executing."""
        with self._condition:
            return self._state in [CANCELLED, CANCELLED_AND_NOTIFIED, FINISHED]

    # 获取任务函数的返回值
    def __get_result(self):
        if self._exception:
            raise self._exception
        else:
            return self._result

    # 添加任务函数执行后要执行的函数
    def add_done_callback(self, fn):
        """Attaches a callable that will be called when the future finishes.

        Args:
            fn: A callable that will be called with this future as its only
                argument when the future completes or is cancelled. The callable
                will always be called by a thread in the same process in which
                it was added. If the future has already completed or been
                cancelled then the callable will be called immediately. These
                callables are called in the order that they were added.
        """
        # 翻译:该方法用于添加一个函数,这个函数会在任务函数完成或者被取消的时候执行。函数会被存放到_done_callbacks这个列表中。所以可以为一个任务函数添加多个这样的函数。这些_done_callbacks中的函数的执行顺序是按照存入列表的顺序执行的。
        
        with self._condition:
            if self._state not in [CANCELLED, CANCELLED_AND_NOTIFIED, FINISHED]:
                self._done_callbacks.append(fn)
                return
        fn(self)


    # 获取任务执行的结果,由于使用了_self.condition.wait()方法,所以是个阻塞的方法。
    def result(self, timeout=None):
        """Return the result of the call that the future represents.

        Args:
            timeout: The number of seconds to wait for the result if the future
                isn't done. If None, then there is no limit on the wait time.

        Returns:
            The result of the call that the future represents.

        Raises:
            CancelledError: If the future was cancelled.
            TimeoutError: If the future didn't finish executing before the given
                timeout.
            Exception: If the call raised then that exception will be raised.
        """
        # 翻译:获取Future任务函数的返回值。由于要获取任务返回值,所以肯定要等任务执行完才能获取返回值,所以result()方法会阻塞等待Future对象中的任务函数执行完才能被唤醒。
        
        with self._condition:
            if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]:
                raise CancelledError()      # 如果对已取消的任务获取返回值结果会报错
            elif self._state == FINISHED:   # 如果对已执行完毕的任务获取结果则无需等待,直接返回结果
                return self.__get_result()

            self._condition.wait(timeout)   # 如果对正在执行的任务或者还未执行的任务获取结果,则需要等待,等到任务结束了才能获取结果

            # 此时 wait() 被唤醒,但是仍要再判断一次任务的状态,因为这个任务可能是还未执行的任务被取消了,取消任务是也会notify()唤醒wait()
            if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]:
                raise CancelledError()
            elif self._state == FINISHED:
                return self.__get_result()
            else:
                raise TimeoutError()

    # 用于获取任务执行时报的异常
    def exception(self, timeout=None):
        """Return the exception raised by the call that the future represents.

        Args:
            timeout: The number of seconds to wait for the exception if the
                future isn't done. If None, then there is no limit on the wait
                time.

        Returns:
            The exception raised by the call that the future represents or None
            if the call completed without raising.

        Raises:
            CancelledError: If the future was cancelled.
            TimeoutError: If the future didn't finish executing before the given
                timeout.
        """

        with self._condition:
            if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]:
                raise CancelledError()
            elif self._state == FINISHED:
                return self._exception

            self._condition.wait(timeout)

            if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]:
                raise CancelledError()
            elif self._state == FINISHED:
                return self._exception
            else:
                raise TimeoutError()

    # 设置任务结果
    def set_result(self, result):
        """Sets the return value of work associated with the future.

        Should only be used by Executor implementations and unit tests.
        """
        # 这个方法只能在任务执行完毕时在线程池的线程中调用,而不能在主线程中调用
        
        with self._condition:
            self._result = result       # 将任务结果存到对象属性中
            self._state = FINISHED      # 将任务状态改为结束
            for waiter in self._waiters:
                waiter.add_result(self)
            self._condition.notify_all()    # 唤醒和通知 result()中的wait(),说:“任务函数的结果已经拿到了,可以把结果返回给主线程了”
        self._invoke_callbacks()    # 执行任务函数完成后要执行的函数

    def set_exception(self, exception):
        """Sets the result of the future as being the given exception.

        Should only be used by Executor implementations and unit tests.
        """
        with self._condition:
            self._exception = exception
            self._state = FINISHED
            for waiter in self._waiters:
                waiter.add_exception(self)
            self._condition.notify_all()
        self._invoke_callbacks()
        


        
PS:像cancel,done,add_done_callback,get_result,set_result等方法都是在 with self._condition 下执行的,意味着这些操作都是加锁执行的,所以Future是线程安全的。

总结:
1.Future对象是用于完成:任务的结果设置,结果获取,取消执行,监控任务状态的一个对象,并且会将任务的状态和结果存储到对象成员变量中。
2.Future对象中,如何实现在主线程获取在其他线程中异步执行的任务的结果?答:使用条件变量实现,在任务没执行完的时候阻塞主线程,在任务执行完后唤醒主线程并返回结果;当然如果任务已经完成的情况下去获取结果则不会阻塞。
3.Future不存储任务函数本身,在Future的__init__中并没有成员变量是用来保存任务函数,也没有见到有任务函数传入__init__中。
4.Future不负责执行任务函数

 

_WorkItem源码:

class _WorkItem(object):
    def __init__(self, future, fn, args, kwargs):       # 接受一个Future对象,fn 任务函数,args 任务函数的参数
        self.future = future    # 任务函数的结果和状态的存储容器
        self.fn = fn            # 任务函数
        self.args = args
        self.kwargs = kwargs

    # 执行任务函数,并将任务的结果保存到future对象中。这个run方法是在 _WorkItem 的 _worker方法中调用的
    def run(self):
        if not self.future.set_running_or_notify_cancel():
            return

        try:
            result = self.fn(*self.args, **self.kwargs)     # 执行任务函数
        except BaseException as exc:
            self.future.set_exception(exc)
            # Break a reference cycle with the exception 'exc'
            self = None
        else:   
            self.future.set_result(result)                  # 将任务的结果保存到future对象中

# 用于从任务队列中弹出任务容器 _WorkItem 对象,并且执行任务容器的 run() 方法。 该方法是在 ThreadPoolExecutor 的 _adjust_thread_count 方法中调用
def _worker(executor_reference, work_queue):
    try:
        while True:         # 这里是一个死循环,说明线程池的线程也是不停从任务队列取任务执行的
            work_item = work_queue.get(block=True)
            if work_item is not None:
                work_item.run()         # work_item就是 _WorkItem 对象本身,run()作用就是运行任务函数
                # Delete references to object. See issue16284
                del work_item           # 任务执行完之后,销毁 任务容器对象
                continue
            executor = executor_reference()
            # Exit if:
            #   - The interpreter is shutting down OR
            #   - The executor that owns the worker has been collected OR
            #   - The executor that owns the worker has been shutdown.
            if _shutdown or executor is None or executor._shutdown:
                # Notice other workers
                work_queue.put(None)
                return
            del executor
    except BaseException:
        _base.LOGGER.critical('Exception in worker', exc_info=True)

 

总结: _WorkItem存放着一个任务函数和这个任务对应的Future对象,_WorkItem负责执行任务函数,并将任务函数的结果放到Future中。
_WrokItem本质是一个容器,可以认为它就是一个任务(对象)。


ThreadPoolExecutor 线程池 源码:
 

class ThreadPoolExecutor(_base.Executor):
    def __init__(self, max_workers=None, thread_name_prefix=''):
        if max_workers is None:
            max_workers = (os.cpu_count() or 1) * 5     # 默认最大的并发线程是CPU核数的5倍
        if max_workers <= 0:
            raise ValueError("max_workers must be greater than 0")

        self._max_workers = max_workers     # 最大并发线程数
        self._work_queue = queue.Queue()    # 任务队列,用于存放任务容器 _WorkItem
        self._threads = set()               # 线程池,用于存放正在运行的线程
        self._shutdown = False              # 标记,用于标记线程池是否已关闭
        self._shutdown_lock = threading.Lock()  # 互斥锁,用于保证线程添加到 self._threads集合中是线程安全的
        self._thread_name_prefix = (thread_name_prefix or
                                    ("ThreadPoolExecutor-%d" % self._counter()))    # 用于生成线程名称的前缀

    # 提交一个任务到线程池中
    def submit(self, fn, *args, **kwargs):
        with self._shutdown_lock:
            if self._shutdown:
                raise RuntimeError('cannot schedule new futures after shutdown')

            f = _base.Future()      # 生成一个Future对象
            w = _WorkItem(f, fn, args, kwargs)  # 将任务函数和Future对象都放到 _WorkItem容器中

            self._work_queue.put(w)     # 将 _WorkItem 任务容器放到任务队列中
            self._adjust_thread_count()     # 为当前任务创建一个线程,并且启动线程运行任务
            return f
    submit.__doc__ = _base.Executor.submit.__doc__


    # 这个方法很关键
    def _adjust_thread_count(self):
        # When the executor gets lost, the weakref callback will wake up
        # the worker threads.
        def weakref_cb(_, q=self._work_queue):
            q.put(None)
        # TODO(bquinlan): Should avoid creating new threads if there are more
        # idle threads than items in the work queue.
        
        num_threads = len(self._threads)
        if num_threads < self._max_workers:     # 判断已创建的线程数是否超过限制,如果超过限制则不做任何事情
            thread_name = '%s_%d' % (self._thread_name_prefix or self,
                                     num_threads)
                                     
            # 这行很关键,创建一个线程,并且,线程的目标函数不是任务函数,而是 _WorkItem中的_worker方法
            # _worker方法做的事情是:不停的从任务队列 self._work_queue 中取出任务执行
            # 所以, ThreadPoolExecutor线程池不是为一个任务生成一个线程,而是先生成一定量的线程,让这些线程不停的从任务队列取任务执行,换句话说,这些线程不会被销毁,而是可以复用。
            t = threading.Thread(name=thread_name, target=_worker,
                                 args=(weakref.ref(self, weakref_cb),
                                       self._work_queue))
            t.daemon = True     # 将线程设为守护线程
            t.start()           # 运行线程
            self._threads.add(t)
            _threads_queues[t] = self._work_queue
    
    # 这个方法也很重要:用于关闭一个线程池。他的方法比较奇特:当生产者不再往任务队列生成产品了,就可以调用这个shutdown方法,此时会将_shutdown标志设为True,并且往任务队列中添加一个None,而添加了这个None就是关键。
    # 因为当生产者生成完毕的时候,消费者不一定消费完毕,而当消费者线程消费完毕所有任务的时候,如果任务队列为空,消费者线程会被队列阻塞。这个时候所有线程不能结束。但是如果我在任务队列最后put一个None,当消费者线程拿到这个None的时候可以做个判断,并跳出while True循环从而结束线程,让线程得到释放。
    def shutdown(self, wait=True):
        with self._shutdown_lock:
            self._shutdown = True
            self._work_queue.put(None)      # 往任务队列中添加一个None,这是关闭线程的关键
        if wait:
            for t in self._threads:
                t.join()
    shutdown.__doc__ = _base.Executor.shutdown.__doc_
    


PS:shutdown的调用时机:当生产者将所有产品放入任务队列,不再会生产新商品时调用。
调用这个方法的时候,任务可能还没消费完,但是任务一定已经生产完。
这个方法用于当所有任务消费完毕后,通知线程池结束所有线程,避免线程处于阻塞状态。
调用shutdown的时候没有通知线程池结束所有线程,而是任务消费完毕时任务队列中的None元素会通知线程池结束所有线程
    


学习多线程关键不是学怎么用python的多线程函数和类,而是了解其原理和设计模式,这样自己也能设计出比较好的多线程模型。


上面了解了线程池的设计模式和原理,接下来我们用线程池来模拟实现一个简单的爬虫:

from threading import Semaphore,Thread
import threading
from time import sleep
from concurrent.futures import ThreadPoolExecutor

# 用于获取一页列表页的url
def get_url(current_page=1,per_rows=10):
    sleep(0.5)    # 模拟爬取
    urls = []
    for i in range(1,per_rows+1):
        id = (current_page-1)*per_rows+i
        url = "http://www.zbpblog.com/blog-%d.html" % id
        urls.append(url)

    print("%s 爬取列表页 %d 成功" % (threading.current_thread().getName(),current_page))
    return urls

# 用于爬取一个详情页
def get_detail(url):
    sleep(0.2)  # 模拟爬取
    print("%s 爬取链接 %s 成功" % (threading.current_thread().getName(),url))


if __name__=="__main__":
    # 创建一个线程池
    pool = ThreadPoolExecutor()

    # 先爬取列表页
    pages = 10
    results = []
    for page in range(1,pages+1):
        future = pool.submit(get_url,page)   # 添加到任务队列中,由线程取出任务并执行任务
        results.append(future)

    # 获取结果
    for future in results:
        result_urls = future.result()   # 阻塞获取结果

        # 获取到url后执行详情页任务
        for url in result_urls:
            pool.submit(get_detail,url)  # 由于爬取详情页没有返回值所以这里不用返回future对象


结果如下:
ThreadPoolExecutor-0_0 爬取列表页 1 成功
ThreadPoolExecutor-0_2 爬取列表页 3 成功
ThreadPoolExecutor-0_1 爬取列表页 2 成功
ThreadPoolExecutor-0_3 爬取列表页 4 成功
ThreadPoolExecutor-0_7 爬取列表页 8 成功
ThreadPoolExecutor-0_5 爬取列表页 6 成功
ThreadPoolExecutor-0_9 爬取列表页 10 成功
ThreadPoolExecutor-0_6 爬取列表页 7 成功
ThreadPoolExecutor-0_8 爬取列表页 9 成功
ThreadPoolExecutor-0_4 爬取列表页 5 成功
ThreadPoolExecutor-0_0 爬取链接 http://www.zbpblog.com/blog-1.html 成功
ThreadPoolExecutor-0_2 爬取链接 http://www.zbpblog.com/blog-2.html 成功
ThreadPoolExecutor-0_1 爬取链接 http://www.zbpblog.com/blog-3.html 成功
ThreadPoolExecutor-0_3 爬取链接 http://www.zbpblog.com/blog-4.html 成功
ThreadPoolExecutor-0_7 爬取链接 http://www.zbpblog.com/blog-5.html 成功
ThreadPoolExecutor-0_5 爬取链接 http://www.zbpblog.com/blog-6.html 成功
ThreadPoolExecutor-0_9 爬取链接 http://www.zbpblog.com/blog-7.html 成功
ThreadPoolExecutor-0_6 爬取链接 http://www.zbpblog.com/blog-8.html 成功

.......

可以看出,在程序中,我submit()很多很多次任务,但是结果显示,创建的线程只有ThreadPoolExecutor-0_0~9 这10个线程。说明,线程不是执行完之后就销毁然后又生成新线程,而是可以复用线程执行多个任务,让一个线程不停地从任务队列里面取出任务执行。
 

可以使用map方法简化上面的调用:

if __name__=="__main__":
    # 创建一个线程池
    pool = ThreadPoolExecutor()

    # 先爬取列表页
    pages = 100

    for result in pool.map(get_url,list(range(pages))):
        # result就是详情页urls
        for url in result:
            pool.submit(get_detail,url)
            


这样调用和上面的效果是一样的。
map()方法是线程池的方法,第一参是任务函数,第二参是任务函数的参数,是一个列表,列表有多少个元素就表示要执行多少个任务函数。map()内部会执行一个for循环调用submit()提交和执行任务,然后会对每一个submit()返回的future对象调用result()获取结果。map内部使用了yield生成器。每当一个任务返回了结果就会赋值给result变量。

所以 for result in pool.map(get_url,list(range(pages))) 中的 result 直接就是任务函数的返回值,而不是future对象。下面贴出 map() 的源码:

    def map(self, fn, *iterables, timeout=None, chunksize=1):
        if timeout is not None:
            end_time = timeout + time.monotonic()

        fs = [self.submit(fn, *args) for args in zip(*iterables)]

        def result_iterator():
            try:
                # reverse to keep finishing order
                fs.reverse()
                while fs:
                    # Careful not to keep a reference to the popped future
                    if timeout is None:
                        yield fs.pop().result()     # 阻塞
                    else:
                        yield fs.pop().result(end_time - time.monotonic())
            finally:
                for future in fs:
                    future.cancel()
        return result_iterator()


还可以使用 as_complete() 调用: 

if __name__=="__main__":
    # 创建一个线程池
    pool = ThreadPoolExecutor()

    # 先爬取列表页
    pages = 100

    futures = [pool.submit(get_url,page) for page in range(pages)]
    for future in as_completed(futures):
        urls=future.result()
        for url in urls:
            pool.submit(get_detail,url)

和map以及第一种调用方式不同,as_completed()会将已经完成任务的future先返回,所以as_completed()会阻塞直到有某个任务完成了,就将这个future赋给for循环中,而此时future.result()不会阻塞。能进入到for代码块中的future都是已经调用过_set_result()的future,都是已经执行完毕的任务,此时future.result()是无需等待的。

举个例子:我要完成 A B C D E 五个任务,5个任务分别需要5,4,3,2,1秒。我submit的顺序是 A B C D E 
此时,1s后E的future先得到,for循环了一次,然后进入阻塞。又过了1s后,得到D的future,for又循环了一次。以此类推,完成这个for循环要花5秒

as_completed()会返回已执行完毕的任务的future对象。


上面这些调用方法还是有一个小缺陷:
他只能爬取完所有列表页才能开始爬详情页,如果希望爬完一个列表页后马上将该列表下的详情页任务添加到任务队列,而不是爬完10个列表页才往任务队列添加详情页任务的话,可以在get_url函数内部执行 pool.submit(get_detail),这才是效率最高的调用方式:

# 用于爬取一个详情页
def get_detail(url):
    sleep(0.2)  # 模拟爬取
    print("%s 爬取链接 %s 成功" % (threading.current_thread().getName(),url))


# 用于获取一页列表页的url
def get_url(pool,current_page=1,per_rows=10):
    sleep(uniform(0.3,0.6))    # 模拟爬取

    for i in range(1,per_rows+1):
        id = (current_page-1)*per_rows+i
        url = "http://www.zbpblog.com/blog-%d.html" % id
        pool.submit(get_detail,url)
        # urls.append(url)
    
    print("%s 爬取列表页 %d 成功" % (threading.current_thread().getName(),current_page))

if __name__=="__main__":
     # 创建一个线程池
    pool = ThreadPoolExecutor()

    # 先爬取列表页
    pages = 100
    futures=[]
    for page in range(1,pages+1):
        future = pool.submit(get_url,pool,page)
        futures.append(future)

    for future in futures:
        future.result()      # 此时get_url是没有返回值的,这里获取任务结果的目的是等待所有get_url()任务的完成,否则会由于主线程的结束而导致其他线程没执行完就结束(因为其他线程是以守护线程的形式存在的)。当然这里只保证了列表页任务的完成,不能保证详情页任务的全部完成。

 




更多内容请关注微信公众号
zbpblog微信公众号

如果您需要转载,可以点击下方按钮可以进行复制粘贴;本站博客文章为原创,请转载时注明以下信息

张柏沛IT技术博客 > Python多线程和多进程(五) 多线程管理——线程池

热门推荐
推荐新闻