源码解密协程队列和线程队列的实现原理(二)

简介: 源码解密协程队列和线程队列的实现原理(二)

接上篇:https://developer.aliyun.com/article/1617453


线程队列



说完了协程队列,再来看看线程队列,它们的 API 是类似的,但实现细节则不同。因为操作系统感知不到协程,所以协程队列的阻塞等待是基于 Future 实现的,而线程队列的阻塞等待是基于条件变量(和互斥锁)实现的。

还是先来看看线程队列的一些 API,和协程队列是类似的。

from queue import Queue
# 可以指定一个 maxsize 参数,表示队列的容量
# 默认为 0,表示队列的容量无限
queue = Queue(maxsize=20)
# 查看容量
print(queue.maxsize)
"""
20
"""
# 查看队列的元素个数
print(queue.qsize())
"""
0
"""
# 判断队列是否已满
print(queue.full())
"""
False
"""
# 判断队列是否为空
print(queue.empty())
"""
True
"""
# 往队列中添加元素
# block 参数表示是否阻塞,默认为 True,当队列已满时,线程会阻塞
# timeout 表示超时时间,默认为 None,表示会无限等待
# 当然也可以给 timeout 传一个具体的值
# 如果在规定时间内,没有将元素放入队列,那么抛异常
queue.put(123, block=True, timeout=None)
# 也是往队列中添加元素,但是当队列已满时,会直接抛异常
# put_nowait(item) 本质上就是 put(item, block=False)
queue.put_nowait(456)
# 从队列中取出元素
# 同样可以传递 block 和 timeout 参数
# block 默认为 True,当队列为空时会陷入阻塞
# timeout 默认为 None,表示会无限等待
print(queue.get(block=True, timeout=None))
"""
123
"""
# 也是从队列中取出元素,但是当队列为空时,会直接抛异常
# get_nowait() 本质上就是 get(block=False)
print(queue.get_nowait())
"""
456
"""
# task_done(),将 unfinished_tasks 属性的值减 1
print(queue.unfinished_tasks)  
"""
2
"""
queue.task_done()
queue.task_done()
print(queue.unfinished_tasks)
"""
0
"""
# join(),当 unfinished_tasks 不为 0 时,陷入阻塞
queue.join()

API 和协程队列是相似的,我们罗列一下:

03f7ae53eb0977f1f3b43f33e10eb772.png

线程队列的具体使用我们已经知道了,下面来看看它的具体实现。

bcef9e8c25f652ed03504cdb5d77fe6e.png

线程队列的内部依旧使用双端队列进行元素存储,并且还使用了一个互斥锁和三个条件变量。

为了保证数据的一致性和线程安全,当队列在多线程环境中被修改(比如添加或删除元素)时,需要使用互斥锁。任何需要修改队列的操作都必须在获取到互斥锁之后进行,以防止多个线程同时对队列进行修改,否则会导致数据不一致或其它错误。同时,一旦对队列的修改完成,必须立即释放互斥锁,以便其它线程可以访问队列。

然后是 not_empty 条件变量,当一个新元素被添加到队列时,应该向 not_empty发送一个信号。这个动作会通知那些想从队列中获取元素,但因队列为空而陷入阻塞的线程,现在队列中已经有了新的元素,它们可以继续执行获取元素的操作。

接下来是 not_full 条件变量,当从队列中取走一个元素时,应该向 not_full 发送一个信号。这个动作通知那些想往队列添加元素,但因队列已满而陷入阻塞的线程,现在队列中已经有了可用空间,它们可以继续执行添加元素的操作。

最后是 all_tasks_done 条件变量,当处理的任务全部完成,即计数器 unfinished_task 为 0 时,应该向 all_tasks_done 发送一个信号。这个动作会通知那些执行了 join() 方法而陷入阻塞的线程,它们可以继续往下执行了。

0a9b706efcfcf6aa4932a343df4d67fc.png

因为线程队列采用了双端队列存储元素,所以双端队列的长度就是线程队列的元素个数。如果元素个数为 0,那么队列就是空;如果容量大于 0,并且小于等于元素个数,那么队列就满了。

2c4cbd033a40c7ecdf4f456c9b8ef1c9.png

前面说了,put_nowait 和 get_nowait 本质上就是调用了 put 和 get,所以我们的重点是 put 和 get 两个方法。

8cf3a4f19ee49ebc63fefa3e56f57502.png

以上就是 put 方法的底层实现,不难理解。说完了 put,再来看看 get。

f94ccfd1eebe94500dc38df73612b6d6.png

最后是 task_done 和 join 方法,看看它们的内部逻辑。

7bebdc5d957139d440e9dd155946ee20.png

调用 join 方法,当 unfinished_task 大于 0 时,会陷入阻塞。调用 task_done 方法,会将未完成任务数减 1,如果为 0,那么唤醒阻塞等待的线程。

需要注意的是,唤醒调用的方法不是 notify,而是 notify_all。对于添加元素和获取元素,每次显然只能唤醒一个线程,此时调用 notify。而 unfinished_task 为 0 时,应该要唤醒所有等待的线程,因此要调用 notify_all。

最后线程队列也有相应的 PriorityQueue 和 LifoQueue,它们的用法、实现和协程里面的这两个队列是一样的。


小结



以上便是协程队列和线程队列的具体用法和实现原理,它们本质上都是基于双端队列实现具体的元素存储,并且在队列已满和队列为空时,可以阻塞等待。

只不过协程队列是通过 Future 对象实现的,而线程队列是通过条件变量实现的。

当然,除了协程队列和线程队列,还有进程队列,但进程队列要复杂的多。因此关于进程队列的实现细节,我们以后专门花篇幅去介绍。

相关文章
|
2天前
|
存储 运维 API
源码解密协程队列和线程队列的实现原理(一)
源码解密协程队列和线程队列的实现原理(一)
10 1
|
2月前
|
存储 监控 Java
Java多线程优化:提高线程池性能的技巧与实践
Java多线程优化:提高线程池性能的技巧与实践
70 1
|
12天前
|
数据采集 负载均衡 安全
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
本文提供了多个多线程编程问题的解决方案,包括设计有限阻塞队列、多线程网页爬虫、红绿灯路口等,每个问题都给出了至少一种实现方法,涵盖了互斥锁、条件变量、信号量等线程同步机制的使用。
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
|
19天前
|
Java Spring
spring多线程实现+合理设置最大线程数和核心线程数
本文介绍了手动设置线程池时的最大线程数和核心线程数配置方法,建议根据CPU核数及程序类型(CPU密集型或IO密集型)来合理设定。对于IO密集型,核心线程数设为CPU核数的两倍;CPU密集型则设为CPU核数加一。此外,还讨论了`maxPoolSize`、`keepAliveTime`、`allowCoreThreadTimeout`和`queueCapacity`等参数的设置策略,以确保线程池高效稳定运行。
87 10
spring多线程实现+合理设置最大线程数和核心线程数
|
28天前
|
Java 数据库 Android开发
一个Android App最少有几个线程?实现多线程的方式有哪些?
本文介绍了Android多线程编程的重要性及其实现方法,涵盖了基本概念、常见线程类型(如主线程、工作线程)以及多种多线程实现方式(如`Thread`、`HandlerThread`、`Executors`、Kotlin协程等)。通过合理的多线程管理,可大幅提升应用性能和用户体验。
51 15
一个Android App最少有几个线程?实现多线程的方式有哪些?
|
10天前
|
NoSQL 网络协议 Unix
1)Redis 属于单线程还是多线程?不同版本之间有什么区别?
1)Redis 属于单线程还是多线程?不同版本之间有什么区别?
29 1
|
13天前
|
Python
5-5|python开启多线程入口必须在main,从python线程(而不是main线程)启动pyQt线程有什么坏处?...
5-5|python开启多线程入口必须在main,从python线程(而不是main线程)启动pyQt线程有什么坏处?...
|
1月前
|
Java 数据库 Android开发
一个Android App最少有几个线程?实现多线程的方式有哪些?
本文介绍了Android应用开发中的多线程编程,涵盖基本概念、常见实现方式及最佳实践。主要内容包括主线程与工作线程的作用、多线程的多种实现方法(如 `Thread`、`HandlerThread`、`Executors` 和 Kotlin 协程),以及如何避免内存泄漏和合理使用线程池。通过有效的多线程管理,可以显著提升应用性能和用户体验。
47 10
|
11天前
|
Java
COMATE插件实现使用线程池高级并发模型简化多线程编程
本文介绍了COMATE插件的使用,该插件通过线程池实现高级并发模型,简化了多线程编程的过程,并提供了生成结果和代码参考。
|
1月前
|
存储 Ubuntu Linux
C语言 多线程编程(1) 初识线程和条件变量
本文档详细介绍了多线程的概念、相关命令及线程的操作方法。首先解释了线程的定义及其与进程的关系,接着对比了线程与进程的区别。随后介绍了如何在 Linux 系统中使用 `pidstat`、`top` 和 `ps` 命令查看线程信息。文档还探讨了多进程和多线程模式各自的优缺点及适用场景,并详细讲解了如何使用 POSIX 线程库创建、退出、等待和取消线程。此外,还介绍了线程分离的概念和方法,并提供了多个示例代码帮助理解。最后,深入探讨了线程间的通讯机制、互斥锁和条件变量的使用,通过具体示例展示了如何实现生产者与消费者的同步模型。