Python 内置队列 Queue 源码解析上篇

「这是我参与2022首次更文挑战的第18天,活动详情查看:2022首次更文挑战」。

正式的Python专栏第67篇,同学站住,别错过这个从0开始的文章!

队列这个库的展示和代码解析更新的七七八八了。

今天我们看看核心队列Queue这个类,这是队列模块一个很重要的类,务必熟读。

队列queue.Queue(前面没有Simple的队列)

下面是队列库核心类的源码,学委删除了一些注释。

一看代码,隐约感觉不简单。(所以按照在,专栏最后一个探讨的类)

class Queue:

    def __init__(self, maxsize=0):
        self.maxsize = maxsize
        self._init(maxsize)
        self.mutex = threading.Lock()
        self.not_empty = threading.Condition(self.mutex)
        self.not_full = threading.Condition(self.mutex)
        self.all_tasks_done = threading.Condition(self.mutex)
        self.unfinished_tasks = 0

    def task_done(self):

        with self.all_tasks_done:
            unfinished = self.unfinished_tasks - 1
            if unfinished <= 0:
                if unfinished < 0:
                    raise ValueError('task_done() called too many times')
                self.all_tasks_done.notify_all()
            self.unfinished_tasks = unfinished

    def join(self):
        with self.all_tasks_done:
            while self.unfinished_tasks:
                self.all_tasks_done.wait()

    def qsize(self):
        with self.mutex:
            return self._qsize()

    def empty(self):
        with self.mutex:
            return not self._qsize()

    def full(self):
        with self.mutex:
            return 0 < self.maxsize <= self._qsize()

    def put(self, item, block=True, timeout=None):
        with self.not_full:
            if self.maxsize > 0:
                if not block:
                    if self._qsize() >= self.maxsize:
                        raise Full
                elif timeout is None:
                    while self._qsize() >= self.maxsize:
                        self.not_full.wait()
                elif timeout < 0:
                    raise ValueError("'timeout' must be a non-negative number")
                else:
                    endtime = time() + timeout
                    while self._qsize() >= self.maxsize:
                        remaining = endtime - time()
                        if remaining <= 0.0:
                            raise Full
                        self.not_full.wait(remaining)
            self._put(item)
            self.unfinished_tasks += 1
            self.not_empty.notify()

    def get(self, block=True, timeout=None):
        with self.not_empty:
            if not block:
                if not self._qsize():
                    raise Empty
            elif timeout is None:
                while not self._qsize():
                    self.not_empty.wait()
            elif timeout < 0:
                raise ValueError("'timeout' must be a non-negative number")
            else:
                endtime = time() + timeout
                while not self._qsize():
                    remaining = endtime - time()
                    if remaining <= 0.0:
                        raise Empty
                    self.not_empty.wait(remaining)
            item = self._get()
            self.not_full.notify()
            return item

    def put_nowait(self, item):
        return self.put(item, block=False)

    def get_nowait(self):
        return self.get(block=False)


    def _init(self, maxsize):
        self.queue = deque()

    def _qsize(self):
        return len(self.queue)

    def _put(self, item):
        self.queue.append(item)

    def _get(self):
        return self.queue.popleft()
复制代码

读源码的时候千万要耐心,这个删简版的代码也就百来行。

结合前面的SimpleQueue解析,读这篇就容易多了。

首先一眼看去,那些一个下划线开头的函数,明显是为了让子类重写覆盖的,这个之前分享优先队列和先进后出,我们也看到了,有对应的_init/_qsize/_put/_get等方法。

内部实现上使用了deque(双端队列)

构造方法如下,用了双端队列作为元素容器,存放元素。

也用了互斥锁,还有3个Condition条件对象,同时它们共用了同一个锁: mutex互斥锁。

这样使用with 这些锁,都是互斥了。

下面是构造方法源码,学委把__init__和_init两个方法合并一起了,看起来方便点:

    def __init__(self, maxsize=0):
        self.maxsize = maxsize
        self.queue = deque()#self._init(maxsize)
        self.mutex = threading.Lock()
        self.not_empty = threading.Condition(self.mutex)
        self.not_full = threading.Condition(self.mutex)
        self.all_tasks_done = threading.Condition(self.mutex)
        self.unfinished_tasks = 0
复制代码

好,下面继续看get方法:

def get(self, block=True, timeout=None):
    with self.not_empty:
        if not block:
            if not self._qsize():
                raise Empty
        elif timeout is None:
            while not self._qsize():
                self.not_empty.wait()
        elif timeout < 0:
            raise ValueError("'timeout' must be a non-negative number")
        else:
            endtime = time() + timeout
            while not self._qsize():
                remaining = endtime - time()
                if remaining <= 0.0:
                    raise Empty
                self.not_empty.wait(remaining)
        item = self.queue.popleft()
        self.not_full.notify()
        return item
复制代码

上面的get源码读者可以再看一次。

get方法 解析

首先先尝试获取非空锁,如果有其他线程在get。那么当前线程将会等待,因为是条件变量背后是同一个互斥锁。

其次,获得了非空条件锁,进去有很多判断,4个分支。依次判断。

最后,取出元素, 同时,释放not_full条件锁(这个被put方法调用了,如果设置了maxsize,not_full锁会限制超额put操作,后面继续说)。

上面三步就是get方法的大致逻辑。下面看看4层分支:

  • 第一层判断,看看是否block方式,如果没有判断队列是否空,否则跳出判断执行上面说到的最后一步。

  • 第二层判断,看看是否给了timeout,这里潜台词是block=True了,也就是没有超时。这时候如果queue内部数据容器空了,则当前线程会一直等待。如果非空跳出判断,执行上面说到的最后一步。

  • 第三层判断,判断timeout是否有效,小于0,直接抛出数值异常,就没有后续了。

  • 第四层其他情况,这时候的上下文中block=True,timeout=有效时间。这种情况下,会进入一个限时循环,如果队列此时有数据,跳出循环,执行上面说的最后一步。 否则执行循环,在给定的timeout内,队列仍然为空,那么抛出空队异常。如果在时限内,有一个或者以上元素放入队列,那么跳出循环。执行上面说的最后一步。

接下来是put方法

def put(self, item, block=True, timeout=None):

    with self.not_full:
        if self.maxsize > 0:
            if not block:
                if self._qsize() >= self.maxsize:
                    raise Full
            elif timeout is None:
                while self._qsize() >= self.maxsize:
                    self.not_full.wait()
            elif timeout < 0:
                raise ValueError("'timeout' must be a non-negative number")
            else:
                endtime = time() + timeout
                while self._qsize() >= self.maxsize:
                    remaining = endtime - time()
                    if remaining <= 0.0:
                        raise Full
                    self.not_full.wait(remaining)
        self.queue.append(item)
        self.unfinished_tasks += 1
        self.not_empty.notify()
复制代码

这个代码也是逻辑比较复杂的。

首先获取not_full条件锁,如果此时队列满了,那么put会进入等待。知道not_full被notify,也就是上面get最后取完元素后通知not_full的一个持有线程。(注意不是notify_all, 因为没get一次就获取一个元素,如果是notify_all 则会出现超额存入的情况,这绝对是一个严重漏洞)。

接着进入一个maxsize判断,如果没有设置>0的数值,也就是默认方式创建队列,那么事情就简单了。直接进入最后一步,否则又是4次判断。

最后,往队列中追加元素,同时设置unfinished_tasks的值加一个。也就是每放入新元素,unfinished_task 自增1. 另外就是通知一个在尝试get的线程,此时队列非空了,可以取元素了。

上面三步就是put方法的大致逻辑。下面看看4层分支:

这里其实更get方法很对称。

  • 第一层判断,看看是否block方式,如果队列满,抛出异常。

  • 第二层判断,看看是否给了timeout,这里潜台词是block=True了,也就是没有超时。这时候如果queue内部数据容器满了,则当前线程会一直等待。如果没有放满则跳出判断,执行上面说到的最后一步。

  • 第三层判断,判断timeout是否有效,小于0,直接抛出数值异常,就没有后续了。

  • 第四层其他情况,这时候的上下文中block=True,timeout=有效时间。这种情况下,会进入一个限时循环,如果队列此时不完全放满,跳出循环,执行上面说的最后一步。 否则执行循环,在给定的timeout内,队列仍然放满的状态,那么抛出满队异常。如果在时限内,有一个或者以上元素空间可以放入,那么跳出循环。执行上面说的最后一步。

所以get和put看懂了一个,另一个其实很好理解.

篇幅有点长了,先到这。

总结

queue.Queue这个类承受了很多,它设计上比SimpleQueue更加复杂,本文只深度探讨了它的get和put还有几个条件锁,但是里面还有几个方法和unfinished_tasks这个属性,学委还没有谈到,下篇继续分解。

而且它是优先队列,先进后出队列的父类,算是整个内置队列的顶梁柱了,希望读者们有空花点时间研究阅读一下。

喜欢Python的朋友,请关注学委的 Python基础专栏 or Python入门到精通大专栏

持续学习持续开发,我是雷学委!
编程很有趣,关键是把技术搞透彻讲明白。
欢迎关注微信,点赞支持收藏!

猜你喜欢

转载自juejin.im/post/7066079200326713351
今日推荐