08-27-周三_17-09-29
161
Python/Python并发编程/协程.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# 协程
|
||||
|
||||
协程(Coroutine)是用户态的轻量级线程,由程序自身控制调度,通过协作式多任务实现并发。它能在单线程内挂起和恢复执行,无需操作系统介入,切换开销极小,尤其适合 I/O 密集型任务(如网络请求、文件读写)。
|
||||
|
||||
**与线程/进程对比**:
|
||||
|
||||
- **资源消耗**:协程内存占用更低(共享进程内存),线程需独立栈空间,进程资源消耗最大
|
||||
- **切换开销**:协程切换在用户态完成,速度极快;线程/进程切换依赖操作系统,开销较大
|
||||
- **适用场景**:协程适合高并发 I/O 操作;线程适合 CPU 密集型任务;进程适合多核并行计算
|
||||
|
||||
**核心优势**:
|
||||
|
||||
- **高并发**:单线程可处理数千级并发连接(如 Web 服务器)
|
||||
- **无锁机制**:避免多线程同步问题(如死锁、竞态条件)
|
||||
- **代码简洁**:用同步语法写异步逻辑,避免回调地狱
|
||||
|
||||
# 实现方式
|
||||
|
||||
## 生成器函数
|
||||
|
||||
通过 yield 暂停执行并传递值:需要手动管理状态,适用简单场景
|
||||
|
||||
```python
|
||||
def simple_coroutine():
|
||||
print("协程启动")
|
||||
x = yield # 暂停点,等待外部传入值
|
||||
print(f"接收值: {x}")
|
||||
|
||||
coro = simple_coroutine()
|
||||
next(coro) # 启动协程,执行到第一个 yield
|
||||
coro.send(10) # 恢复执行,x 赋值为 10
|
||||
|
||||
```
|
||||
|
||||
## async/await
|
||||
|
||||
通过 asyncio 库实现异步编程
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
|
||||
async def fetch_data(url):
|
||||
print(f"请求 {url}")
|
||||
await asyncio.sleep(1) # 挂起协程,让出控制权给事件循环,模拟异步等待
|
||||
return f"来自 {url} 的数据"
|
||||
|
||||
|
||||
async def main():
|
||||
tasks = [fetch_data("url1"), fetch_data("url2")]
|
||||
results = await asyncio.gather(*tasks) # 实现多任务并发调度:并发执行
|
||||
print(results)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
```
|
||||
|
||||
# 基本语法
|
||||
|
||||
**定义协程函数**
|
||||
|
||||
使用 `async def` 声明协程函数
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
async def my_coroutine():
|
||||
print("协程开始")
|
||||
await asyncio.sleep(1) # 模拟 I/O 操作
|
||||
print("协程结束")
|
||||
|
||||
```
|
||||
|
||||
**运行协程**
|
||||
|
||||
协程需要通过**事件循环**执行
|
||||
|
||||
```python
|
||||
async def main():
|
||||
await my_coroutine() # 等待协程完成
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
# 事件循环
|
||||
|
||||
事件循环是协程的调度核心,负责执行、切换和监控协程任务
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
|
||||
async def task1():
|
||||
print("任务1开始")
|
||||
await asyncio.sleep(2)
|
||||
print("任务1结束")
|
||||
|
||||
|
||||
async def task2():
|
||||
print("任务2开始")
|
||||
await asyncio.sleep(1)
|
||||
print("任务2结束")
|
||||
|
||||
|
||||
async def main():
|
||||
await asyncio.gather(task1(), task2()) # 并发执行多个协程
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
```
|
||||
|
||||
# 进阶用法
|
||||
|
||||
## 任务
|
||||
|
||||
将协程封装为任务,更灵活地控制执行
|
||||
|
||||
```python
|
||||
async def main():
|
||||
task = asyncio.create_task(my_coroutine()) # 创建任务
|
||||
await task # 等待任务完成
|
||||
```
|
||||
|
||||
## 超时控制
|
||||
|
||||
设置协程执行的超时时间
|
||||
|
||||
```python
|
||||
async def slow_task():
|
||||
await asyncio.sleep(10)
|
||||
|
||||
async def main():
|
||||
try:
|
||||
await asyncio.wait_for(slow_task(), timeout=3)
|
||||
except asyncio.TimeoutError:
|
||||
print("任务超时")
|
||||
|
||||
```
|
||||
|
||||
## 协程同步
|
||||
|
||||
使用锁 `Lock` 保护共享资源
|
||||
|
||||
```python
|
||||
lock = asyncio.Lock()
|
||||
|
||||
async def safe_write():
|
||||
async with lock: # 异步上下文管理器
|
||||
# 安全地操作共享资源
|
||||
pass
|
||||
```
|
||||
|
||||
# 课后作业
|
||||
|
||||
- [必须] 动手完成本章节案例
|
||||
- [扩展] 阅读官方文档相关章节
|
||||
- [扩展] 用协程实现进程章节的爬虫案例
|
169
Python/Python并发编程/操作系统发展史.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# 手工操作 - 穿孔卡片
|
||||
|
||||
1946年第一台计算机诞生--20世纪50年代中期,计算机工作还在采用手工操作方式。此时还没有操作系统的概念。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
程序员将对应于程序和数据的已穿孔的纸带(或卡片)装入输入机,然后启动输入机把程序和数据输入计算机内存,接着通过控制台开关启动程序针对数据运行;计算完毕,打印机输出计算结果;用户取走结果并卸下纸带(或卡片)后,才让下一个用户上机。
|
||||
|
||||
手工操作方式两个特点:
|
||||
|
||||
1. 用户独占全机。不会出现因资源已被其他用户占用而等待的现象,但资源的利用率低。
|
||||
2. CPU 等待手工操作。CPU的利用不充分。
|
||||
|
||||
20世纪50年代后期,出现人机矛盾:手工操作的慢速度和计算机的高速度之间形成了尖锐矛盾,手工操作方式已严重损害了系统资源的利用率(使资源利用率降为百分之几,甚至更低),不能容忍。唯一的解决办法:只有摆脱人的手工操作,实现作业的自动过渡。这样就出现了成批处理。
|
||||
|
||||
# 批处理 - 磁带存储
|
||||
|
||||
批处理系统:加载在计算机上的一个系统软件,在它的控制下,计算机能够自动地、成批地处理一个或多个用户的作业(这作业包括程序、数据和命令)。
|
||||
|
||||
## 联机批处理系统
|
||||
|
||||
首先出现的是联机批处理系统,即作业的输入/输出由CPU来处理。
|
||||
|
||||

|
||||
|
||||
主机与输入机之间增加一个存储设备——磁带,在运行于主机上的监督程序的自动控制下,计算机可自动完成:成批地把输入机上的用户作业读入磁带,依次把磁带上的用户作业读入主机内存并执行并把计算结果向输出机输出。完成了上一批作业后,监督程序又从输入机上输入另一批作业,保存在磁带上,并按上述步骤重复处理。
|
||||
监督程序不停地处理各个作业,从而实现了作业到作业的自动转接,减少了作业建立时间和手工操作时间,有效克服了人机矛盾,提高了计算机的利用率。
|
||||
但是,在作业输入和结果输出时,主机的高速CPU仍处于空闲状态,等待慢速的输入/输出设备完成工作: 主机处于“忙等”状态。
|
||||
|
||||
## 脱机批处理系统
|
||||
|
||||
为克服与缓解:高速主机与慢速外设的矛盾,提高CPU的利用率,又引入了脱机批处理系统,即输入/输出脱离主机控制。
|
||||
|
||||

|
||||
|
||||
卫星机:一台不与主机直接相连而专门用于与输入/输出设备打交道的。
|
||||
|
||||
1. 从输入机上读取用户作业并放到输入磁带上。
|
||||
2. 从输出磁带上读取执行结果并传给输出机
|
||||
|
||||
不足:每次主机内存中仅存放一道作业,每当它运行期间发出输入/输出(I/O)请求后,高速的CPU便处于等待低速的I/O完成状态,致使CPU空闲。
|
||||
|
||||
# 多道程序系统
|
||||
|
||||
## 多道程序设计技术
|
||||
|
||||
所谓多道程序设计技术,就是指允许多个程序同时进入内存并运行。即同时把多个程序放入内存,并允许它们交替在CPU中运行,它们共享系统中的各种硬、软件资源。当一道程序因I/O请求而暂停运行时,CPU便立即转去运行另一道程序。
|
||||
|
||||

|
||||
|
||||
在A程序计算时,I/O空闲, A程序I/O操作时,CPU空闲(B程序也是同样);必须A工作完成后,B才能进入内存中开始工作,两者是串行的,全部完成共需时间=T1+T2。
|
||||
|
||||

|
||||
|
||||
将A、B两道程序同时存放在内存中,它们在系统的控制下,可相互穿插、交替地在CPU上运行:当A程序因请求I/O操作而放弃CPU时,B程序就可占用CPU运行,这样 CPU不再空闲,而正进行A I/O操作的I/O设备也不空闲,显然,CPU和I/O设备都处于“忙”状态,大大提高了资源的利用率,从而也提高了系统的效率,A、B全部完成所需时间<<T1+T2。
|
||||
|
||||
单处理机系统中多道程序运行时的特点:
|
||||
|
||||
1. 多道:计算机内存中同时存放几道相互独立的程序;
|
||||
2. 宏观上并行:同时进入系统的几道程序都处于运行过程中,即它们先后开始了各自的运行,但都未运行完毕;
|
||||
3. 微观上串行:实际上,各道程序轮流地用CPU,并交替运行。
|
||||
|
||||
多道程序系统的出现,标志着操作系统渐趋成熟的阶段,先后出现了作业调度管理、处理机管理、存储器管理、外部设备管理、文件系统管理等功能。
|
||||
由于多个程序同时在计算机中运行,开始有了**空间隔离**的概念,只有内存空间的隔离,才能让数据更加安全、稳定。
|
||||
出了空间隔离之外,多道技术还第一次体现了**时空复用**的特点,遇到IO操作就切换程序,使得cpu的利用率提高了,计算机的工作效率也随之提高。
|
||||
|
||||
## 多道批处理系统
|
||||
|
||||
20世纪60年代中期,在前述的批处理系统中,引入多道程序设计技术后形成多道批处理系统(简称:批处理系统)。
|
||||
它有两个特点:
|
||||
|
||||
1. 多道:系统内可同时容纳多个作业。这些作业放在外存中,组成一个后备队列,系统按一定的调度原则每次从后备作业队列中选取一个或多个作业进入内存运行,运行作业结束、退出运行和后备作业进入运行均由系统自动实现,从而在系统中形成一个自动转接的、连续的作业流。
|
||||
2. 成批:在系统运行过程中,不允许用户与其作业发生交互作用,即:作业一旦进入系统,用户就不能直接干预其作业的运行。
|
||||
|
||||
- 批处理系统的追求目标:提高系统资源利用率和系统吞吐量,以及作业流程的自动化。
|
||||
- 批处理系统的一个重要缺点:不提供人机交互能力,给用户使用计算机带来不便。
|
||||
- 虽然用户独占全机资源,并且直接控制程序的运行,可以随时了解程序运行情况。但这种工作方式因独占全机造成资源效率极低。
|
||||
- 一种新的追求目标:既能保证计算机效率,又能方便用户使用计算机。 20世纪60年代中期,计算机技术和软件技术的发展使这种追求成为可能。
|
||||
|
||||
# 分时系统
|
||||
|
||||
由于CPU速度不断提高和采用分时技术,一台计算机可同时连接多个用户终端,而每个用户可在自己的终端上联机使用计算机,好象自己独占机器一样。
|
||||
|
||||

|
||||
|
||||
|
||||
分时技术:把处理机的运行时间分成很短的时间片,按时间片轮流把处理机分配给各联机作业使用。
|
||||
若某个作业在分配给它的时间片内不能完成其计算,则该作业暂时中断,把处理机让给另一作业使用,等待下一轮时再继续其运行。由于计算机速度很快,作业运行轮转得很快,给每个用户的印象是,好象他独占了一台计算机。而每个用户可以通过自己的终端向系统发出各种操作控制命令,在充分的人机交互情况下,完成作业的运行。
|
||||
具有上述特征的计算机系统称为分时系统,它允许多个用户同时联机使用计算机。
|
||||
|
||||
特点:
|
||||
|
||||
1. 多路性。若干个用户同时使用一台计算机。微观上看是各用户轮流使用计算机;宏观上看是各用户并行工作。
|
||||
2. 交互性。用户可根据系统对请求的响应结果,进一步向系统提出新的请求。这种能使用户与系统进行人机对话的工作方式,明显地有别于批处理系统,因而,分时系统又被称为交互式系统。
|
||||
3. 独立性。用户之间可以相互独立操作,互不干扰。系统保证各用户程序运行的完整性,不会发生相互混淆或破坏现象。
|
||||
4. 及时性。系统可对用户的输入及时作出响应。分时系统性能的主要指标之一是响应时间,它是指:从终端发出命令到系统予以应答所需的时间。
|
||||
|
||||
**分时系统的主要目标**:对用户响应的及时性,即不至于用户等待每一个命令的处理时间过长。
|
||||
分时系统可以同时接纳数十个甚至上百个用户,由于内存空间有限,往往采用对换(又称交换)方式的存储方法。即将未“轮到”的作业放入磁盘,一旦“轮到”,再将其调入内存;而时间片用完后,又将作业存回磁盘(俗称“滚进”、“滚出“法),使同一存储区域轮流为多个用户服务。
|
||||
|
||||
**注意:分时系统的分时间片工作,在没有遇到IO操作的时候就用完了自己的时间片被切走了,这样的切换工作其实并没有提高cpu的效率,反而使得计算机的效率降低了。但是我们牺牲了一点效率,却实现了多个程序共同执行的效果,这样你就可以在计算机上一边听音乐一边聊qq了。**
|
||||
|
||||
# 实时系统
|
||||
|
||||
虽然多道批处理系统和分时系统能获得较令人满意的资源利用率和系统响应时间,但却不能满足实时控制与实时信息处理两个应用领域的需求。于是就产生了实时系统,即系统能够及时响应随机发生的外部事件,并在严格的时间范围内完成对该事件的处理。
|
||||
|
||||
实时系统在一个特定的应用中常作为一种控制设备来使用。
|
||||
|
||||
**实时系统可分成两类**:
|
||||
|
||||
1. 实时控制系统。当用于飞机飞行、导弹发射等的自动控制时,要求计算机能尽快处理测量系统测得的数据,及时地对飞机或导弹进行控制,或将有关信息通过显示终端提供给决策人员。当用于轧钢、石化等工业生产过程控制时,也要求计算机能及时处理由各类传感器送来的数据,然后控制相应的执行机构。
|
||||
2. 实时信息处理系统。当用于预定飞机票、查询有关航班、航线、票价等事宜时,或当用于银行系统、情报检索系统时,都要求计算机能对终端设备发来的服务请求及时予以正确的回答。此类对响应及时性的要求稍弱于第一类。
|
||||
**实时操作系统的主要特点**:
|
||||
3. 及时响应。每一个信息接收、分析处理和发送的过程必须在严格的时间限制内完成。
|
||||
4. 高可靠性。需采取冗余措施,双机系统前后台工作,也包括必要的保密措施等。
|
||||
|
||||
分时——现在流行的PC,服务器都是采用这种运行模式,即把CPU的运行分成若干时间片分别处理不同的运算请求 linux系统
|
||||
实时——一般用于单片机上、PLC等,比如电梯的上下控制中,对于按键等动作要求进行实时处理
|
||||
|
||||
# 通用操作系统
|
||||
|
||||
操作系统的三种基本类型:多道批处理系统、分时系统、实时系统。
|
||||
|
||||
- 通用操作系统:具有多种类型操作特征的操作系统。可以同时兼有多道批处理、分时、实时处理的功能,或其中两种以上的功能。
|
||||
- 实时处理+批处理=实时批处理系统。首先保证优先处理实时任务,插空进行批处理作业。常把实时任务称为前台作业,批作业称为后台作业。
|
||||
- 分时处理+批处理=分时批处理系统。即:时间要求不强的作业放入“后台”(批处理)处理,需频繁交互的作业在“前台”(分时)处理,处理机优先运行“前台”作业。
|
||||
|
||||
从上世纪60年代中期,国际上开始研制一些大型的通用操作系统。这些系统试图达到功能齐全、可适应各种应用范围和操作方式变化多端的环境的目标。但是,这些系统过于复杂和庞大,不仅付出了巨大的代价,且在解决其可靠性、可维护性和可理解性方面都遇到很大的困难。相比之下,UNIX操作系统却是一个例外。这是一个通用的多用户分时交互型的操作系统。它首先建立的是一个精干的核心,而其功能却足以与许多大型的操作系统相媲美,在核心层以外,可以支持庞大的软件系统。它很快得到应用和推广,并不断完善,对现代操作系统有着重大的影响。至此,操作系统的基本概念、功能、基本结构和组成都已形成并渐趋完善。
|
||||
|
||||
# 操作系统的进一步发展
|
||||
|
||||
进入20世纪80年代,大规模集成电路工艺技术的飞跃发展,微处理机的出现和发展,掀起了计算机大发展大普及的浪潮。一方面迎来了个人计算机的时代,同时又向计算机网络、分布式处理、巨型计算机和智能化方向发展。于是,操作系统有了进一步的发展,如:个人计算机操作系统、网络操作系统、分布式操作系统等。
|
||||
|
||||
## 个人计算机操作系统
|
||||
|
||||
个人计算机上的操作系统是联机交互的单用户操作系统,它提供的联机交互功能与通用分时系统提供的功能很相似。
|
||||
由于是个人专用,因此一些功能会简单得多。然而,由于个人计算机的应用普及,对于提供更方便友好的用户接口和丰富功能的文件系统的要求会愈来愈迫切。
|
||||
|
||||
## 网络操作系统
|
||||
|
||||
计算机网络:通过通信设施,将地理上分散的、具有自治功能的多个计算机系统互连起来,实现信息交换、资源共享、互操作和协作处理的系统。
|
||||
网络操作系统:在原来各自计算机操作系统上,按照网络体系结构的各个协议标准增加网络管理模块,其中包括:通信、资源共享、系统安全和各种网络应用服务。
|
||||
|
||||
## 分布式操作系统
|
||||
|
||||
表面上看,分布式系统与计算机网络系统没有多大区别。分布式操作系统也是通过通信网络,将地理上分散的具有自治功能的数据处理系统或计算机系统互连起来,实现信息交换和资源共享,协作完成任务。——硬件连接相同。
|
||||
但有如下一些明显的区别:
|
||||
|
||||
1. 分布式系统要求一个统一的操作系统,实现系统操作的统一性。
|
||||
2. 分布式操作系统管理分布式系统中的所有资源,它负责全系统的资源分配和调度、任务划分、信息传输和控制协调工作,并为用户提供一个统一的界面。
|
||||
3. 用户通过这一界面,实现所需要的操作和使用系统资源,至于操作定在哪一台计算机上执行,或使用哪台计算机的资源,则是操作系统完成的,用户不必知道,此谓:系统的透明性。
|
||||
4. 分布式系统更强调分布式计算和处理,因此对于多机合作和系统重构、坚强性和容错能力有更高的要求,希望系统有:更短的响应时间、高吞吐量和高可靠性。
|
||||
|
||||
# 操作系统的作用
|
||||
|
||||
现代的计算机系统主要是由一个或者多个处理器,主存,硬盘,键盘,鼠标,显示器,打印机,网络接口及其他输入输出设备组成。
|
||||
|
||||
一般而言,现代计算机系统是一个复杂的系统。
|
||||
|
||||
其一:如果每位应用程序员都必须掌握该系统所有的细节,那就不可能再编写代码了(严重影响了程序员的开发效率:全部掌握这些细节可能需要一万年....)
|
||||
|
||||
其二:并且管理这些部件并加以优化使用,是一件极富挑战性的工作,于是,计算安装了一层软件(系统软件),称为操作系统。它的任务就是为用户程序提供一个更好、更简单、更清晰的计算机模型,并管理刚才提到的所有设备。
|
||||
|
||||
**总结:**
|
||||
|
||||
1. 程序员无法把所有的硬件操作细节都了解到,管理这些硬件并且加以优化使用是非常繁琐的工作,这个繁琐的工作就是操作系统来干的,有了他,程序员就从这些繁琐的工作中解脱了出来,只需要考虑自己的应用软件的编写就可以了,应用软件直接使用操作系统提供的功能来间接使用硬件。
|
||||
2. 精简的说的话,操作系统是一个协调、管理和控制计算机硬件资源和软件资源的控制程序。
|
BIN
Python/Python并发编程/操作系统发展史/传统PLC运行模式.png
Normal file
After Width: | Height: | Size: 203 KiB |
BIN
Python/Python并发编程/操作系统发展史/单道程序工作示例.png
Normal file
After Width: | Height: | Size: 80 KiB |
BIN
Python/Python并发编程/操作系统发展史/多道程序工作示例.png
Normal file
After Width: | Height: | Size: 71 KiB |
BIN
Python/Python并发编程/操作系统发展史/手工操作计算机.png
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
Python/Python并发编程/操作系统发展史/穿孔卡片1.png
Normal file
After Width: | Height: | Size: 162 KiB |
BIN
Python/Python并发编程/操作系统发展史/穿孔卡片2.png
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
Python/Python并发编程/操作系统发展史/脱机批处理系统.png
Normal file
After Width: | Height: | Size: 81 KiB |
481
Python/Python并发编程/线程.md
Normal file
@@ -0,0 +1,481 @@
|
||||
# 操作系统线程理论
|
||||
|
||||
## 进程
|
||||
|
||||
进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
|
||||
|
||||
进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。
|
||||
|
||||
## 线程
|
||||
|
||||
60年代,在 OS 中能拥有资源和独立运行的基本单位是进程,然而随着计算机技术的发展,进程出现了很多弊端:
|
||||
|
||||
1. 进程是资源拥有者,创建、撤消与切换存在较大的时空开销,因此需要引入轻型进程。
|
||||
2. 对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程并行开销过大。
|
||||
|
||||
因此在 80 年代,出现了能独立运行的基本单位:线程(Threads)。进程是资源分配的最小单位,线程是CPU调度的最小单位,每一个进程中至少有一个线程。
|
||||
|
||||
## 进程和线程的关系
|
||||
|
||||

|
||||
|
||||
线程与进程的区别可以归纳为以下4点:
|
||||
|
||||
1. 地址空间和其它资源共享(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
|
||||
2. 通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信,需要进程同步和互斥手段的辅助,以保证数据的一致性。
|
||||
3. 调度和切换:线程上下文切换比进程上下文切换要快得多。
|
||||
4. 多线程操作系统中,进程不是一个可执行的实体。
|
||||
|
||||
## 使用线程的实际场景
|
||||
|
||||
开启一个打字处理软件进程,该进程肯定需要办不止一件事情,比如监听键盘输入,处理文字,定时自动将文字保存到硬盘,这三个任务操作的都是同一块数据,因而不能用多进程。只能在一个进程里并发地开启三个线程,如果是单线程,那就只能是,键盘输入时,不能处理文字和自动保存,自动保存时又不能输入和处理文字。
|
||||
|
||||
## 内存中的线程
|
||||
|
||||

|
||||
|
||||
线程通常是有益的,但是带来了不小程序设计难度,线程的问题是:
|
||||
|
||||
1. 父进程有多个线程,那么开启的子线程是否需要同样多的线程
|
||||
2. 在同一个进程中,如果一个线程关闭了文件,而另外一个线程正准备往该文件内写内容呢?
|
||||
|
||||
因此,在多线程的代码中,需要更多的心思来设计程序的逻辑、保护程序的数据。
|
||||
|
||||
# python 线程使用
|
||||
|
||||
## 全局解释器锁 GIL
|
||||
|
||||
Python 代码的执行由 Python 解释器主循环控制。Python 在设计之初就考虑到要在主循环中,同时只有一个线程在执行。虽然 Python 解释器中可以“运行”多个线程,但在任意时刻只有一个线程在解释器中运行。对 Python 解释器的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。
|
||||
|
||||
在多线程环境中,Python 解释器按以下方式执行:
|
||||
|
||||

|
||||
|
||||
1. 设置 GIL
|
||||
2. 切换到一个线程去运行
|
||||
3. 运行指定数量的字节码指令或者线程主动让出控制(可以调用 time.sleep(0))
|
||||
4. 把线程设置为睡眠状态
|
||||
5. 解锁 GIL
|
||||
6. 再次重复以上所有步骤。
|
||||
|
||||
## 创建线程
|
||||
|
||||
**直接创建线程对象**:
|
||||
|
||||
```python
|
||||
from threading import Thread
|
||||
import time
|
||||
|
||||
def task(name, delay):
|
||||
print(f"{name} 开始执行")
|
||||
time.sleep(delay)
|
||||
print(f"{name} 执行完毕")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 通过 Thread 类实例化,指定目标函数(target)和参数(args/kwargs)
|
||||
t1 = Thread(target=task, args=("线程A", 2))
|
||||
t1.start() # 启动线程
|
||||
t1.join() # 等待线程结束
|
||||
```
|
||||
|
||||
**继承 Thread 类**:
|
||||
|
||||
```python
|
||||
from threading import Thread
|
||||
import time
|
||||
|
||||
|
||||
# 通过子类化 Thread 并重写 run() 方法
|
||||
class MyThread(Thread):
|
||||
def __init__(self, name):
|
||||
super().__init__()
|
||||
self.name = name
|
||||
|
||||
def run(self):
|
||||
print(f"{self.name} 运行中...")
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
t1 = MyThread("自定义线程")
|
||||
t1.start()
|
||||
|
||||
```
|
||||
|
||||
## 多线程
|
||||
|
||||
**示例代码**:多线程运行
|
||||
```python
|
||||
import threading
|
||||
import time
|
||||
import os
|
||||
|
||||
|
||||
def task(name, delay):
|
||||
print(f"当前线程 ID (Python标识符): {threading.get_ident()}")
|
||||
print(f"线程对象标识符: {threading.current_thread().ident}")
|
||||
|
||||
print(f"{name}-{os.getpid()} 开始执行")
|
||||
time.sleep(delay)
|
||||
print(f"{name}-{os.getpid()} 执行完毕")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
threads = [threading.Thread(target=task, args=(f"线程{i}", 2)) for i in range(10)]
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
print("主线程/主进程pid", os.getpid())
|
||||
|
||||
```
|
||||
|
||||
## 常用方法
|
||||
|
||||
| 方法 | 含义 |
|
||||
| :------ | :------- |
|
||||
| `Thread.isAlive()` | Thread 类中的对象方法:返回线程是否活动的 |
|
||||
| `Thread.getName()` | 返回线程名 |
|
||||
| `Thread.setName()` | 设置线程名 |
|
||||
| `threading.currentThread()` | 返回当前的线程变量 |
|
||||
| `threading.enumerate()` | 返回一个包含正在运行的线程的列表 |
|
||||
| `threading.activeCount()` | 返回正在运行的线程数量 |
|
||||
|
||||
**示例代码**:
|
||||
|
||||
```python
|
||||
from threading import Thread
|
||||
import threading
|
||||
from multiprocessing import Process
|
||||
import os
|
||||
|
||||
|
||||
def work():
|
||||
import time
|
||||
|
||||
time.sleep(3)
|
||||
print(threading.current_thread().getName())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
t = Thread(target=work)
|
||||
t.start()
|
||||
print(t.is_alive())
|
||||
print(threading.current_thread().getName())
|
||||
print(threading.current_thread())
|
||||
print(threading.enumerate())
|
||||
print(threading.active_count())
|
||||
t.join()
|
||||
print("主线程/主进程")
|
||||
print(t.is_alive())
|
||||
|
||||
```
|
||||
|
||||
## 守护线程
|
||||
|
||||
在 Python 中,守护线程(Daemon Thread) 是一种特殊的线程,它的生命周期与主线程(或程序的主进程)绑定。当所有非守护线程(即普通线程)结束时,无论守护线程是否完成任务,它都会被强制终止。这种机制常用于执行后台支持任务(如日志记录、心跳检测等),无需等待其完成。
|
||||
|
||||
**核心特性**:
|
||||
|
||||
- **依赖主线程存活**:主线程结束时,守护线程立即终止(即使任务未完成)。
|
||||
- **后台服务**:通常用于非关键性任务,即使意外终止也不会影响程序逻辑。
|
||||
- **资源释放风险**:守护线程被终止时,可能不会正常释放资源(如文件句柄、网络连接),需谨慎使用。
|
||||
|
||||
**示例代码**:
|
||||
|
||||
```python
|
||||
import threading
|
||||
import time
|
||||
|
||||
|
||||
def background_task():
|
||||
while True:
|
||||
print("守护线程运行中...")
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
# 创建线程并设置为守护线程
|
||||
daemon_thread = threading.Thread(target=background_task)
|
||||
daemon_thread.daemon = True
|
||||
daemon_thread.start()
|
||||
|
||||
# 主线程执行其他操作
|
||||
time.sleep(3)
|
||||
print("主线程结束,守护线程将被终止")
|
||||
|
||||
```
|
||||
|
||||
# 线程同步机制
|
||||
|
||||
## 互斥锁
|
||||
|
||||
保证同一时刻只有一个线程能访问共享资源,防止数据竞争。
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```python
|
||||
import threading
|
||||
import time
|
||||
|
||||
|
||||
def increment():
|
||||
global shared_counter
|
||||
with lock: # 自动获取和释放锁:lock.acquire() 和 lock.release()
|
||||
tmp = shared_counter + 1
|
||||
time.sleep(0.1)
|
||||
shared_counter = tmp
|
||||
|
||||
if __name__ == "__main__":
|
||||
shared_counter = 0
|
||||
lock = threading.Lock()
|
||||
# 启动多个线程修改共享变量
|
||||
threads = [threading.Thread(target=increment) for _ in range(100)]
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
print(shared_counter) # 输出 100(无竞争)
|
||||
|
||||
```
|
||||
|
||||
## 死锁与可重入锁
|
||||
|
||||
|
||||
**死锁**:两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为**死锁进程**
|
||||
|
||||
**示例代码**:
|
||||
|
||||
```python
|
||||
from threading import Lock as Lock
|
||||
import time
|
||||
|
||||
mutexA = Lock()
|
||||
mutexA.acquire()
|
||||
mutexA.acquire() # 上面已经拿过一次key了,这边就拿不到了,程序被阻塞到这里
|
||||
print(123)
|
||||
mutexA.release()
|
||||
mutexA.release()
|
||||
|
||||
```
|
||||
|
||||
**可重入锁**:`threading.RLock` 允许同一线程多次获取锁(避免死锁)。RLock 内部维护着一个 Lock和一个 counter 变量,counter 记录了 acquire 的次数,从而使得资源可以被多次 acquire。直到一个线程所有的 acquire 都被 release,其他的线程才能获得资源。
|
||||
|
||||
```python
|
||||
from threading import RLock as Lock
|
||||
import time
|
||||
|
||||
mutexA=Lock()
|
||||
mutexA.acquire()
|
||||
mutexA.acquire()
|
||||
print(123)
|
||||
mutexA.release()
|
||||
mutexA.release()
|
||||
```
|
||||
|
||||
## 同步锁
|
||||
|
||||
- 协调线程间的执行顺序(如生产者-消费者模型)。
|
||||
- 控制并发数量(如限制同时访问数据库的连接数)。
|
||||
|
||||
### 信号量
|
||||
|
||||
控制同时访问资源的线程数量:适用于限制并发数
|
||||
|
||||
```python
|
||||
import threading
|
||||
|
||||
semaphore = threading.Semaphore(3) # 最多允许3个线程同时运行
|
||||
|
||||
|
||||
def task():
|
||||
with semaphore:
|
||||
print(f"{threading.current_thread().name} 正在工作")
|
||||
# 模拟耗时操作
|
||||
threading.Event().wait(3)
|
||||
|
||||
|
||||
# 启动10个线程,但最多3个并发执行
|
||||
threads = [threading.Thread(target=task) for _ in range(10)]
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
|
||||
```
|
||||
|
||||
### 条件变量
|
||||
|
||||
实现线程间通知机制:适用于生产者-消费者模型
|
||||
|
||||
```python
|
||||
import threading
|
||||
|
||||
queue = []
|
||||
condition = threading.Condition()
|
||||
|
||||
|
||||
def producer():
|
||||
with condition:
|
||||
queue.append("EaglesLab")
|
||||
condition.notify() # 通知等待的消费者
|
||||
|
||||
|
||||
def consumer():
|
||||
with condition:
|
||||
while not queue:
|
||||
condition.wait() # 等待生产者通知
|
||||
data = queue.pop()
|
||||
print(f"消费数据: {data}")
|
||||
|
||||
|
||||
# 启动生产者和消费者线程
|
||||
threading.Thread(target=producer).start()
|
||||
threading.Thread(target=consumer).start()
|
||||
|
||||
```
|
||||
|
||||
### 事件
|
||||
|
||||
简单线程间状态通知:事件常用于跨线程的状态同步。
|
||||
|
||||
```python
|
||||
import threading
|
||||
|
||||
event = threading.Event()
|
||||
|
||||
|
||||
def waiter():
|
||||
print("等待事件触发...")
|
||||
event.wait() # 阻塞直到事件被设置
|
||||
print("事件已触发!")
|
||||
|
||||
|
||||
def setter():
|
||||
threading.Event().wait(2)
|
||||
event.set() # 设置事件
|
||||
|
||||
|
||||
threading.Thread(target=waiter).start()
|
||||
threading.Thread(target=setter).start()
|
||||
|
||||
```
|
||||
|
||||
# 线程池
|
||||
|
||||
线程池通过预创建并复用一组线程,减少频繁创建/销毁线程的开销,适用于 **I/O 密集型任务**(如网络请求、文件读写)
|
||||
|
||||
- 优点:资源复用、负载均衡、简化线程管理。
|
||||
- 适用场景:批量下载、Web 服务器请求处理、数据库并发查询。
|
||||
|
||||
## 基本操作
|
||||
|
||||
通过 `concurrent.futures.ThreadPoolExecutor` 实现
|
||||
|
||||
```python
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
|
||||
def task(n):
|
||||
return n * n
|
||||
|
||||
|
||||
# 创建线程池(推荐使用 with 上下文管理)
|
||||
with ThreadPoolExecutor(max_workers=5) as executor:
|
||||
# 提交任务方式1:submit 逐个提交
|
||||
future = executor.submit(task, 5)
|
||||
print(future.result()) # 输出 25
|
||||
|
||||
# 提交任务方式2:map 批量提交
|
||||
results = executor.map(task, [1, 2, 3])
|
||||
print(list(results))
|
||||
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- **线程数量**:建议设为 CPU 核心数 × 2(I/O 密集型)
|
||||
- **异常处理**:通过 `try-except` 捕获 `future.result()` 的异常
|
||||
- **资源释放**:使用 `shutdown()` 或上下文管理器自动关闭线程池
|
||||
|
||||
## 同步机制结合
|
||||
|
||||
当多个线程访问共享资源(如全局变量、文件)时,需通过同步机制避免资源竞争和数据不一致。
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```python
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from threading import Lock
|
||||
|
||||
|
||||
def task():
|
||||
global counter
|
||||
with lock: # 使用锁保护共享变量
|
||||
counter += 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
counter = 0
|
||||
lock = Lock()
|
||||
with ThreadPoolExecutor(max_workers=5) as executor:
|
||||
futures = [executor.submit(task) for _ in range(100)]
|
||||
for future in futures:
|
||||
future.result()
|
||||
print(f"最终计数:{counter}")
|
||||
|
||||
```
|
||||
|
||||
# 案例解析
|
||||
|
||||
## 案例1
|
||||
|
||||
基于条件变量同步机制,实现多线程-生产/消费者模型完整版本
|
||||
|
||||
```python
|
||||
import threading
|
||||
|
||||
def producer(i):
|
||||
with condition:
|
||||
queue.append(f"EaglesLab {i}")
|
||||
condition.notify() # 通知等待的消费者
|
||||
|
||||
|
||||
def consumer(i):
|
||||
with condition:
|
||||
# 等待直到队列不为空或生产结束
|
||||
while not queue and not producer_done:
|
||||
condition.wait() # 等待生产者通知
|
||||
if queue:
|
||||
data = queue.pop()
|
||||
elif producer_done:
|
||||
return
|
||||
print(f"消费者-{i} 消费数据: {data}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
queue = []
|
||||
condition = threading.Condition() # 初始化条件变量
|
||||
producer_done = False
|
||||
# 启动生产者和消费者线程
|
||||
pt = [threading.Thread(target=producer, args=(i,)) for i in range(3)]
|
||||
ct = [threading.Thread(target=consumer, args=(i,)) for i in range(10)]
|
||||
for t in pt + ct:
|
||||
t.start()
|
||||
with condition:
|
||||
producer_done = True
|
||||
condition.notify_all()
|
||||
for t in pt:
|
||||
t.join()
|
||||
for t in ct:
|
||||
t.join()
|
||||
print("Main Process/Thread Done...")
|
||||
|
||||
```
|
||||
|
||||
# 课后作业
|
||||
|
||||
- [必须] 动手完成本章节案例
|
||||
- [扩展] 阅读官方文档相关章节
|
||||
- [扩展] 用多线程实现进程章节的爬虫案例
|
BIN
Python/Python并发编程/线程/GIL.png
Normal file
After Width: | Height: | Size: 168 KiB |
BIN
Python/Python并发编程/线程/内存中的线程.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
Python/Python并发编程/线程/进程和线程关系.png
Normal file
After Width: | Height: | Size: 11 KiB |
994
Python/Python并发编程/进程.md
Normal file
@@ -0,0 +1,994 @@
|
||||
# 进程
|
||||
|
||||
进程(Process)是计算机中**的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。**在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
|
||||
|
||||
狭义定义:**进程是正在运行的程序的实例**(an instance of a computer program that is being executed)。
|
||||
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
|
||||
|
||||
1. 进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。
|
||||
2. 进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。进程是操作系统中最基本、重要的概念。是多道程序系统出现后,为了刻画系统内部出现的动态情况,描述系统内部各道程序的活动规律引进的一个概念,所有多道程序设计操作系统都建立在进程的基础上。
|
||||
|
||||
# 进程调度方式
|
||||
|
||||
**扩展阅读**
|
||||
|
||||
要想多个进程交替运行,操作系统必须对这些进程进行调度,这个调度也不是随即进行的,而是需要遵循一定的法则,由此就有了进程的调度算法。
|
||||
|
||||
- **先来先服务调度(FCFS, First-Come, First-Served)**
|
||||
|
||||
先来先服务(FCFS)调度算法是一种最简单的调度算法,该算法既可用于作业调度,也可用于进程调度。FCFS算法比较有利于长作业(进程),而不利于短作业(进程)。由此可知,本算法适合于CPU繁忙型作业,而不利于I/O繁忙型的作业(进程)。
|
||||
|
||||
- **短作业优先调度(SJF, Shortest Job First)**
|
||||
|
||||
短作业(进程)优先调度算法(SJ/PF)是指对短作业或短进程优先调度的算法,该算法既可用于作业调度,也可用于进程调度。但其对长作业不利;不能保证紧迫性作业(进程)被及时处理;作业的长短只是被估算出来的。
|
||||
|
||||
- **最高优先级调度(Priority Scheduling)**
|
||||
|
||||
每个进程被赋予一个优先级。系统总是选择优先级最高(数值最小或最大)的进程执行。如果两个进程有相同优先级,则可以按FCFS调度。
|
||||
|
||||
- **时间片轮转调度(Round Robin, RR)**
|
||||
|
||||
时间片轮转(Round Robin,RR)法的基本思路是让每个进程在就绪队列中的等待时间与享受服务的时间成比例。在时间片轮转法中,需要将CPU的处理时间分成固定大小的时间片,例如,几十毫秒至几百毫秒。如果一个进程在被调度选中之后用完了系统规定的时间片,但又未完成要求的任务,则它自行释放自己所占有的CPU而排到就绪队列的末尾,等待下一次调度。同时,进程调度程序又去调度当前就绪队列中的第一个进程。
|
||||
显然,轮转法只能用来调度分配一些可以抢占的资源。这些可以抢占的资源可以随时被剥夺,而且可以将它们再分配给别的进程。CPU是可抢占资源的一种。但打印机等资源是不可抢占的。由于作业调度是对除了CPU之外的所有系统硬件资源的分配,其中包含有不可抢占资源,所以作业调度不使用轮转法。
|
||||
|
||||
在轮转法中,时间片长度的选取非常重要。首先,时间片长度的选择会直接影响到系统的开销和响应时间。如果时间片长度过短,则调度程序抢占处理机的次数增多。这将使进程上下文切换次数也大大增加,从而加重系统开销。反过来,如果时间片长度选择过长,例如,一个时间片能保证就绪队列中所需执行时间最长的进程能执行完毕,则轮转法变成了先来先服务法。时间片长度的选择是根据系统对响应时间的要求和就绪队列中所允许最大的进程数来确定的。
|
||||
|
||||
|
||||
- **多级反馈队列调度(Multilevel Feedback Queue, MLFQ)**
|
||||
|
||||
前面介绍的各种用作进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程,而且如果并未指明进程的长度,则短进程优先和基于进程长度的抢占式调度算法都将无法使用。
|
||||
|
||||
而多级反馈队列调度算法则不必事先知道各种进程所需的执行时间,而且还可以满足各种类型进程的需要,因而它是目前被公认的一种较好的进程调度算法。在采用多级反馈队列调度算法的系统中,调度算法的实施过程如下所述。
|
||||
|
||||
1. 应设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列次之,其余各队列的优先权逐个降低。该算法赋予各个队列中进程执行时间片的大小也各不相同,在优先权愈高的队列中,为每个进程所规定的执行时间片就愈小。例如,第二个队列的时间片要比第一个队列的时间片长一倍,……,第i+1个队列的时间片要比第i个队列的时间片长一倍。
|
||||
|
||||
2. 当一个新进程进入内存后,首先将它放入第一队列的末尾,按FCFS原则排队等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入第二队列的末尾,再同样地按FCFS原则等待调度执行;如果它在第二队列中运行一个时间片后仍未完成,再依次将它放入第三队列,……,如此下去,当一个长作业(进程)从第一队列依次降到第n队列后,在第n 队列便采取按时间片轮转的方式运行。
|
||||
|
||||
3. 仅当第一队列空闲时,调度程序才调度第二队列中的进程运行;仅当第1~(i-1)队列均空时,才会调度第i队列中的进程运行。如果处理机正在第i队列中为某进程服务时,又有新进程进入优先权较高的队列(第1~(i-1)中的任何一个队列),则此时新进程将抢占正在运行进程的处理机,即由调度程序把正在运行的进程放回到第i队列的末尾,把处理机分配给新到的高优先权进程。
|
||||
|
||||
# 进程的并行与并发
|
||||
|
||||
**并行**:并行是指两者同时执行,比如赛跑,两个人都在不停的往前跑;(资源够用,比如三个线程,四核的CPU )
|
||||
|
||||
**并发**:并发是指资源有限的情况下,两者交替轮流使用资源,比如一段路(单核CPU资源)同时只能过一个人,A走一段后,让给B,B用完继续给A,交替使用,目的是提高效率。
|
||||
|
||||
**并行**:是从微观上,也就是在一个精确的时间片刻,有不同的程序在执行,这就要求必须有多个处理器。
|
||||
|
||||
**并发**:是从宏观上,在一个时间段上可以看出是同时执行的,比如一个服务器同时处理多个session。
|
||||
|
||||
# 同步异步阻塞非阻塞
|
||||
|
||||
## 进程状态介绍
|
||||
|
||||
<img src="进程/进程状态转换图1.png" alt="img-进程状态转换图1" />
|
||||
|
||||
在了解其他概念之前,我们首先要了解进程的几个状态。在程序运行的过程中,由于被操作系统的调度算法控制,程序会进入几个状态:就绪,运行和阻塞。
|
||||
|
||||
- 就绪(Ready)状态:当进程已分配到除CPU以外的所有必要的资源,只要获得处理机便可立即执行,这时的进程状态称为就绪状态。
|
||||
- 执行/运行(Running)状态:当进程已获得处理机,其程序正在处理机上执行,此时的进程状态称为执行状态。
|
||||
- 阻塞(Blocked)状态:正在执行的进程,由于等待某个事件发生而无法执行时,便放弃处理机而处于阻塞状态。引起进程阻塞的事件可有多种,例如,等待I/O完成、申请缓冲区不能满足、等待信件(信号)等。
|
||||
|
||||
<img src="进程/进程状态转换图2.png" alt="img-进程状态转换图2" style="zoom:80%;" />
|
||||
|
||||
## 同步和异步
|
||||
|
||||
所谓同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。
|
||||
|
||||
所谓异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。
|
||||
|
||||
## 阻塞与非阻塞
|
||||
|
||||
阻塞和非阻塞这两个概念与程序(线程)等待消息通知(无所谓同步或者异步)时的状态有关。也就是说阻塞与非阻塞主要是程序(线程)等待消息通知时的状态角度来说的
|
||||
|
||||
## 同步/异步与阻塞/非阻塞
|
||||
|
||||
- 同步阻塞形式:效率最低。就是你专心排队,什么别的事都不做。
|
||||
|
||||
- 异步阻塞形式:效率较高。如果在银行等待办理业务的人采用的是异步的方式去等待消息被触发(通知),也就是领了一张小纸条,假如在这段时间里他不能离开银行做其它的事情,那么很显然,这个人被阻塞在了这个等待的操作上面;异步操作是可以被阻塞住的,只不过它不是在处理消息时阻塞,而是在等待消息通知时被阻塞。
|
||||
|
||||
- 同步非阻塞形式:效率低下。想象一下你一边打着电话一边还需要抬头看到底队伍排到你了没有,如果把打电话和观察排队的位置看成是程序的两个操作的话,这个程序需要在这两种不同的行为之间来回的切换,效率可想而知是低下的。
|
||||
|
||||
- 异步非阻塞形式:效率更高。因为打电话是你(等待者)的事情,而通知你则是柜台(消息触发机制)的事情,程序没有在两种不同的操作中来回切换。比如说,这个人突然发觉自己烟瘾犯了,需要出去抽根烟,于是他告诉大堂经理说,排到我这个号码的时候麻烦到外面通知我一下,那么他就没有被阻塞在这个等待的操作上面,自然这个就是异步+非阻塞的方式了。
|
||||
|
||||
很多人会把同步和阻塞混淆,是因为很多时候同步操作会以阻塞的形式表现出来,同样的,很多人也会把异步和非阻塞混淆,因为异步操作一般都不会在真正的IO操作处被阻塞。
|
||||
|
||||
# 进程的创建与结束
|
||||
|
||||
## 进程的创建
|
||||
|
||||
但凡是硬件,都需要有操作系统去管理,只要有操作系统,就有进程的概念,就需要有创建进程的方式,一些操作系统只为一个应用程序设计,比如微波炉中的控制器,一旦启动微波炉,所有的进程都已经存在。
|
||||
|
||||
而对于通用系统(跑很多应用程序),需要有系统运行过程中创建或撤销进程的能力,主要分为4中形式创建新的进程:
|
||||
|
||||
1. 系统初始化(查看进程linux中用ps命令,windows中用任务管理器,前台进程负责与用户交互,后台运行的进程与用户无关,运行在后台并且只在需要时才唤醒的进程,称为守护进程,如电子邮件、web页面、新闻、打印)
|
||||
2. 一个进程在运行过程中开启了子进程(如nginx开启多进程,os.fork,subprocess.Popen等)
|
||||
3. 用户的交互式请求,而创建一个新进程(如用户双击暴风影音)
|
||||
4. 一个批处理作业的初始化(只在大型机的批处理系统中应用)
|
||||
|
||||
无论哪一种,新进程的创建都是由一个已经存在的进程执行了一个用于创建进程的系统调用而创建的。
|
||||
|
||||
## 进程的结束
|
||||
|
||||
1. 正常退出(自愿,如用户点击交互式页面的叉号,或程序执行完毕调用发起系统调用正常退出,在linux中用exit,在windows中用ExitProcess)
|
||||
2. 出错退出(自愿,python a.py中a.py不存在)
|
||||
3. 严重错误(非自愿,执行非法指令,如引用不存在的内存,1/0等,可以捕捉异常,try...except...)
|
||||
4. 被其他进程杀死(非自愿,如kill -9)
|
||||
|
||||
# Python 多进程编程
|
||||
|
||||
我们可以使用 python 中的 **multiprocess** 包来实现多进程编程。
|
||||
|
||||
由于 multiprocess 包提供的子模块非常多,为了方便大家归类记忆,将这部分大致分为四个部分:创建进程部分,进程同步部分,进程池部分,进程之间数据共享。
|
||||
|
||||
## 进程创建
|
||||
|
||||
process 模块是一个创建进程的模块,借助这个模块,就可以完成进程的创建。
|
||||
|
||||
**直接创建**:Process 类 + target 函数
|
||||
|
||||
```python
|
||||
from multiprocessing import Process
|
||||
p = Process(target=func, args=(arg1,))
|
||||
p.start()
|
||||
```
|
||||
|
||||
**继承类创建**:自定义 Process 子类,重写 run() 方法
|
||||
|
||||
```python
|
||||
from multiprocessing import Process
|
||||
class MyProcess(Process):
|
||||
def run(self):
|
||||
print('Hello World!')
|
||||
```
|
||||
|
||||
**代码示例**
|
||||
|
||||
```python
|
||||
# 直接创建
|
||||
from multiprocessing import Process
|
||||
|
||||
def func(name):
|
||||
print("hello %s" % name)
|
||||
print("子进程结束")
|
||||
|
||||
if __name__ == '__main__':
|
||||
p = Process(target=func, args=('nls',)) # 实例化对象:子进程p 传递函数名和函数的实参
|
||||
p.start() # 启动子进程
|
||||
print("主进程结束...")
|
||||
|
||||
|
||||
# 继承类创建
|
||||
import os
|
||||
from multiprocessing import Process
|
||||
|
||||
class MyProcess(Process):
|
||||
def __init__(self,name):
|
||||
super().__init__()
|
||||
self.name=name
|
||||
|
||||
def run(self):
|
||||
print(os.getpid())
|
||||
print('%s 正在和女主播聊天' %self.name)
|
||||
|
||||
if __name__ == '__main__':
|
||||
p1 = MyProcess('张三')
|
||||
p1.start()
|
||||
print('主进程结束...')
|
||||
|
||||
```
|
||||
|
||||
**方法介绍:**
|
||||
|
||||
| 方法 | 含义 |
|
||||
| :----------------- | :------------------------------------------------------------ |
|
||||
| `p.start()` | 启动进程,并调用该子进程中的 `p.run() ` |
|
||||
| `p.run()` | 进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法 |
|
||||
| `p.terminate()` | 强制终止进程 p,不会进行任何清理操作,如果 p 创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。如果 p 还保存了一个锁那么也将不会被释放,进而导致死锁 |
|
||||
| `p.is_alive()` | 如果 p 仍然运行,返回 True |
|
||||
| `p.join([timeout])` | 主线程等待p终止(强调:是主线程处于等的状态,而p是处于运行的状态)。timeout是可选的超时时间,需要强调的是,p.join只能join住start开启的进程,而不能join住run开启的进程 |
|
||||
| `p.daemon()` | 默认值为 False,如果设为 True,代表 p 为后台运行的守护进程,当p的父进程终止时,p 也随之终止,并且设定为 True 后,p 不能创建自己的新进程,必须在p.start()之前设置 |
|
||||
| `p.name()` | 进程的名称 |
|
||||
| `p.pid()` | 进程的 pid |
|
||||
| `p.exitcode()` | 进程在运行时为None、如果为–N,表示被信号N结束 |
|
||||
| `p.authkey()` | 进程的身份验证键,默认是由os.urandom()随机生成的32字符的字符串。这个键的用途是为涉及网络连接的底层进程间通信提供安全性,这类连接只有在具有相同的身份验证键时才能成功 |
|
||||
|
||||
注意:在Windows操作系统中由于没有fork(linux操作系统中创建进程的机制),在创建子进程的时候会自动 import 启动它的这个文件,而在 import 的时候又执行了整个文件。因此如果将 process() 直接写在文件中就会无限递归创建子进程报错。所以必须把创建子进程的部分使用 `if __name__ =='__main__'` 判断保护起来,import 的时候,就不会递归运行了。
|
||||
|
||||
### join
|
||||
|
||||
正常情况下,是主进程先执行结束,然后等待子进程执行结束以后,整个程序退出。如果在 start 了以后使用 join,那么将会阻塞(也可以理解为同步)主进程,等子进程结束以后,主进程才能继续执行。
|
||||
|
||||
```python
|
||||
from multiprocessing import Process
|
||||
|
||||
def func(name):
|
||||
print("hello %s" % name)
|
||||
print("子进程")
|
||||
|
||||
if __name__ == '__main__':
|
||||
p = Process(target=func,args=('nls',))
|
||||
p.start()
|
||||
p.join() # 阻塞等待完成
|
||||
print("主程序")
|
||||
|
||||
```
|
||||
|
||||
### 查看进程号
|
||||
|
||||
我们可以通过 os 模块中提供的 getpid 的方法来获取当前进程的进程号
|
||||
|
||||
```python
|
||||
import os
|
||||
from multiprocessing import Process
|
||||
|
||||
def func():
|
||||
print('子进程id:',os.getpid(),'父进程id:',os.getppid())
|
||||
print("子进程结束")
|
||||
|
||||
if __name__ == '__main__':
|
||||
p = Process(target=func,args=())
|
||||
p.start()
|
||||
print("主进程id:",os.getpid())
|
||||
print("主程序结束,等待子进程结束中...")
|
||||
```
|
||||
|
||||
由此我们可以看出,在子进程中查看他的父进程的id号等同于我们在主进程中查看到的id号,可以说明子进程确实是由我们的主进程创建的。
|
||||
|
||||
### 多进程实例
|
||||
|
||||
多个进程同时运行(注意,子进程的执行顺序不是根据启动顺序决定的)
|
||||
|
||||
```python
|
||||
from multiprocessing import Process # 从 multiprocessing 包中导入 Process 模块
|
||||
import time
|
||||
|
||||
def func(name): # 创建一个函数,当作一个任务
|
||||
print("hello %s" % name)
|
||||
time.sleep(1)
|
||||
print("子进程结束")
|
||||
|
||||
if __name__ == '__main__':
|
||||
for i in range(5):
|
||||
p = Process(target=func, args=('nls',))
|
||||
p.start()
|
||||
print("主程序结束,等待子进程....")
|
||||
```
|
||||
|
||||
使用join方法
|
||||
|
||||
```python
|
||||
from multiprocessing import Process # 从multiprocessing包中导入Process模块
|
||||
import time
|
||||
|
||||
def func(name): # 创建一个函数,当作一个任务
|
||||
print("hello %s" % name)
|
||||
time.sleep(1)
|
||||
print("子进程结束")
|
||||
|
||||
if __name__ == '__main__':
|
||||
for i in range(5):
|
||||
p = Process(target=func, args=('nls',))
|
||||
p.start()
|
||||
p.join()
|
||||
print("主程序结束,等待子进程....")
|
||||
|
||||
```
|
||||
|
||||
发现,如果使用了join方法后,子进程变成了顺序执行,每个子进程结束以后,下一个子进程才能开始。同一时刻,只能由一个子进程执行,变成了一种阻塞的方式。
|
||||
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```python
|
||||
import multiprocessing
|
||||
import time
|
||||
|
||||
# 定义子进程执行的函数
|
||||
def worker(num):
|
||||
print(f"进程 {num} 开始工作")
|
||||
time.sleep(2)
|
||||
print(f"进程 {num} 工作结束")
|
||||
|
||||
if __name__ == '__main__':
|
||||
processes = []
|
||||
|
||||
# 创建并启动 3 个进程
|
||||
for i in range(3):
|
||||
p = multiprocessing.Process(target=worker, args=(i,))
|
||||
processes.append(p)
|
||||
p.start()
|
||||
|
||||
# 等待所有进程完成
|
||||
for p in processes:
|
||||
p.join()
|
||||
|
||||
print("所有进程完成,主进程退出...")
|
||||
|
||||
```
|
||||
|
||||
|
||||
### 守护进程
|
||||
|
||||
随着主进程的结束而结束,主进程创建守护进程,进程之间是互相独立的,主进程代码运行结束,守护进程随即终止。
|
||||
|
||||
1. 守护进程会在主进程代码执行结束后就终止
|
||||
2. 守护进程内无法再开启子进程,否则抛出异常
|
||||
|
||||
**示例代码**
|
||||
|
||||
```python
|
||||
from multiprocessing import Process
|
||||
import time
|
||||
|
||||
def foo():
|
||||
print(123)
|
||||
time.sleep(1)
|
||||
print("end123") # 父进程代码执行结束,所以这里不会输出
|
||||
|
||||
def bar():
|
||||
print(456)
|
||||
time.sleep(3)
|
||||
print("end456")
|
||||
|
||||
if __name__ == '__main__':
|
||||
p1=Process(target=foo)
|
||||
p2=Process(target=bar)
|
||||
|
||||
p1.daemon=True # 设置为守护进程
|
||||
p1.start()
|
||||
p2.start()
|
||||
time.sleep(0.1)
|
||||
print("main-------")
|
||||
```
|
||||
|
||||
### socket 聊天并发实例
|
||||
|
||||
**示例代码**
|
||||
|
||||
```python
|
||||
# 服务端
|
||||
import socket
|
||||
import multiprocessing
|
||||
|
||||
def handle_client(conn, addr):
|
||||
"""
|
||||
子进程处理客户端连接的函数
|
||||
[优化点]:添加异常处理防止僵尸进程
|
||||
"""
|
||||
print(f"客户端 {addr} 已连接")
|
||||
try:
|
||||
while True:
|
||||
data = conn.recv(1024)
|
||||
if not data: # 客户端主动断开连接
|
||||
break
|
||||
print(f"接收自 {addr} 的数据: {data.decode()}")
|
||||
conn.sendall(f"服务端响应: {data.decode().upper()}".encode())
|
||||
except ConnectionResetError:
|
||||
print(f"客户端 {addr} 异常断开")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 端口复用
|
||||
server.bind(("127.0.0.1", 8888))
|
||||
server.listen(5)
|
||||
print("服务器已启动,等待连接...")
|
||||
|
||||
try:
|
||||
while True:
|
||||
conn, addr = server.accept()
|
||||
# 创建子进程处理连接
|
||||
process = multiprocessing.Process(
|
||||
target=handle_client,
|
||||
args=(conn, addr),
|
||||
daemon=True, # 设置守护进程防止僵尸进程
|
||||
)
|
||||
process.start()
|
||||
conn.close() # 主进程关闭连接副本
|
||||
except KeyboardInterrupt:
|
||||
print("\n服务器正在关闭...")
|
||||
finally:
|
||||
server.close()
|
||||
|
||||
# 客户端 - 超简版
|
||||
import socket
|
||||
|
||||
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
client.connect(("127.0.0.1", 8888))
|
||||
|
||||
while True:
|
||||
msg = input(">>: ").strip()
|
||||
if not msg:
|
||||
continue
|
||||
|
||||
client.send(msg.encode("utf-8"))
|
||||
msg = client.recv(1024)
|
||||
print(msg.decode("utf-8"))
|
||||
|
||||
```
|
||||
|
||||
## 进程间数据共享
|
||||
|
||||
在 Python 中,由于进程间内存空间相互独立,直接共享数据需借助特定机制。
|
||||
|
||||
**示例代码**:
|
||||
|
||||
```python
|
||||
from multiprocessing import Process
|
||||
import os
|
||||
|
||||
# 全局变量
|
||||
count = 0
|
||||
|
||||
def increment():
|
||||
global count
|
||||
count += 1
|
||||
print(f"子进程 {os.getpid()} 修改后的 count: {count}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 创建两个子进程
|
||||
p1 = Process(target=increment)
|
||||
p2 = Process(target=increment)
|
||||
|
||||
p1.start()
|
||||
p2.start()
|
||||
p1.join()
|
||||
p2.join()
|
||||
|
||||
print(f"主进程中的 count: {count}")
|
||||
|
||||
```
|
||||
|
||||
### 原生共享内存方案(Value/Array)
|
||||
|
||||
适合共享简单数据类型(如整数、浮点数)或数组。通过底层共享内存实现,无需复制数据,性能较高。
|
||||
|
||||
**代码示例**:
|
||||
|
||||
```python
|
||||
from multiprocessing import Process, Value, Array
|
||||
|
||||
def increment(num):
|
||||
num.value += 1
|
||||
|
||||
if __name__ == '__main__':
|
||||
counter = Value('i', 0) # 'i' 表示整数类型
|
||||
arr = Array('d', [0.0, 1.0, 2.0]) # 'd' 表示双精度浮点数
|
||||
processes = [Process(target=increment, args=(counter,)) for _ in range(5)]
|
||||
for p in processes:
|
||||
p.start()
|
||||
for p in processes:
|
||||
p.join()
|
||||
print(counter.value) # 数据共享,但数据不一致
|
||||
```
|
||||
|
||||
### 进程同步机制
|
||||
|
||||
共享数据时需通过锁(Lock)、信号量(Semaphore)等保证数据一致性。
|
||||
|
||||
**示例代码**:
|
||||
|
||||
```python
|
||||
from multiprocessing import Process, Value, Lock
|
||||
|
||||
# 同步可避免多进程同时修改数据导致的错误
|
||||
def safe_increment(num, lock):
|
||||
with lock: # 自动获取和释放锁: lock.acquire() 和 lock.release()
|
||||
num.value += 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
counter = Value("i", 0)
|
||||
lock = Lock() # 创建对象
|
||||
|
||||
processes = [Process(target=safe_increment, args=(counter, lock)) for _ in range(5)]
|
||||
for p in processes:
|
||||
p.start()
|
||||
for p in processes:
|
||||
p.join()
|
||||
print(counter.value)
|
||||
|
||||
```
|
||||
|
||||
### Manager 代理对象方案
|
||||
|
||||
适合共享复杂数据结构(如字典、列表)。通过代理模式,由 Manager 服务进程管理数据,子进程通过代理访问。
|
||||
|
||||
`Manager` 提供了一种方式来创建可以在多个进程之间共享的对象。`Manager` 允许不同的进程通过代理对象来共享数据结构,包括列表、字典、命名空间等,而无需显式的进程间通信机制(如队列或管道)。`Manager` 实现了进程间的同步机制,确保多个进程可以安全地读写共享数据。
|
||||
|
||||
`multiprocessing.Manager` 提供了一个管理器对象,这个管理器可以生成各种共享对象,如列表、字典、队列、锁等。所有这些对象都可以被不同的进程安全地访问和修改。
|
||||
|
||||
**共享对象类型**:
|
||||
|
||||
- `list`:共享的列表。
|
||||
- `dict`:共享的字典。
|
||||
- `Namespace`:共享的命名空间,允许存储任意属性。
|
||||
- `Queue`:共享的队列,用于进程间通信。
|
||||
- `Lock`:锁,用于进程同步,防止数据竞争。
|
||||
|
||||
**基本使用流程**
|
||||
|
||||
1. 从 `multiprocessing.Manager()` 创建管理器对象。
|
||||
2. 使用管理器对象来创建共享数据结构(如 `list`、`dict` 等)。
|
||||
3. 在多个进程中共享这些数据结构。
|
||||
4. 进程完成后,关闭管理器对象。
|
||||
|
||||
**示例代码**:
|
||||
|
||||
```python
|
||||
from multiprocessing import Manager, Process
|
||||
|
||||
def update_dict(shared_dict, key):
|
||||
shared_dict[key] = key * 2
|
||||
|
||||
if __name__ == '__main__':
|
||||
with Manager() as manager:
|
||||
shared_dict = manager.dict()
|
||||
processes = [Process(target=update_dict, args=(shared_dict, i)) for i in range(3)]
|
||||
for p in processes:
|
||||
p.start()
|
||||
for p in processes:
|
||||
p.join()
|
||||
print(shared_dict)
|
||||
|
||||
```
|
||||
|
||||
### 共享内存高级方案
|
||||
|
||||
|
||||
## 进程间通信
|
||||
|
||||
IPC(Inter-Process Communication)
|
||||
|
||||
在计算机系统中,进程是操作系统分配资源的基本单位,每个进程拥有独立的内存空间和资源。由于进程间的内存隔离,进程间通信成为实现多进程协作的关键技术。
|
||||
|
||||
队列和管道都是将数据存放于内存中,队列又是基于(管道+锁)实现的,可以让我们从复杂的锁问题中解脱出来,我们应该尽量避免使用共享数据,尽可能使用消息传递和队列,避免处理复杂的同步和锁问题,而且在进程数目增多时,往往可以获得更好的可扩展性。
|
||||
|
||||
### 管道
|
||||
|
||||
点对点通信,返回两个连接对象。
|
||||
|
||||
**使用方式**
|
||||
|
||||
```python
|
||||
from multiprocessing import Pipe
|
||||
|
||||
conn1, conn2 = Pipe()
|
||||
conn1.send("Hello")
|
||||
print(conn2.recv())
|
||||
```
|
||||
|
||||
### 队列
|
||||
|
||||
安全传递数据,支持多生产者和消费者。
|
||||
|
||||
**使用方式**
|
||||
|
||||
```python
|
||||
from multiprocessing import Queue
|
||||
|
||||
q = Queue([maxsize]) # 创建共享的进程队列,maxsize 是队列中允许的最大项数,默认为大小限制。
|
||||
|
||||
```
|
||||
|
||||
**常见方法**
|
||||
|
||||
| 方法 | 含义 |
|
||||
| :------- | :--------|
|
||||
| `q.get(block=True, timeout=None)`| 返回q中的一个项目。如果q为空,此方法将阻塞,直到队列中有项目可用为止。block用于控制阻塞行为,默认为True. 如果设置为False,将引发Queue.Empty异常(定义在Queue模块中)。timeout是可选超时时间,用在阻塞模式中。如果在制定的时间间隔内没有项目变为可用,将引发Queue.Empty异常。|
|
||||
| `q.get_nowait()`| 同q.get(False)方法。|
|
||||
| `q.put(obj, block=True, timeout=None)`| 将obj放入队列。如果队列已满,此方法将阻塞至有空间可用为止。block控制阻塞行为,默认为True。如果设置为False,将引发Queue.Empty异常(定义在Queue库模块中)。timeout指定在阻塞模式中等待可用空间的时间长短。超时后将引发Queue.Full异常。|
|
||||
| `q.qsize()` | 返回队列中目前项目的正确数量。此函数的结果并不可靠,因为在返回结果和在稍后程序中使用结果之间,队列中可能添加或删除了项目。在某些系统上,此方法可能引发NotImplementedError异常。|
|
||||
| `q.empty()` | 如果调用此方法时 q为空,返回True。如果其他进程或线程正在往队列中添加项目,结果是不可靠的。也就是说,在返回和使用结果之间,队列中可能已经加入新的项目。 |
|
||||
| `q.full()` | 如果q已满,返回为True. 由于线程的存在,结果也可能是不可靠的(参考q.empty()方法)。|
|
||||
| `q.close()` | 关闭队列,防止队列中加入更多数据。调用此方法时,后台线程将继续写入那些已入队列但尚未写入的数据,但将在此方法完成时马上关闭。如果q被垃圾收集,将自动调用此方法。关闭队列不会在队列使用者中生成任何类型的数据结束信号或异常。例如,如果某个使用者正被阻塞在get()操作上,关闭生产者中的队列不会导致get()方法返回错误。|
|
||||
| `q.cancel_join_thread()` | 不会再进程退出时自动连接后台线程。这可以防止join_thread()方法阻塞。|
|
||||
| `q.join_thread()` | 连接队列的后台线程。此方法用于在调用q.close()方法后,等待所有队列项被消耗。默认情况下,此方法由不是q的原始创建者的所有进程调用。调用q.cancel_join_thread()方法可以禁止这种行为。|
|
||||
|
||||
|
||||
**代码示例**
|
||||
|
||||
```python
|
||||
from multiprocessing import Queue
|
||||
q=Queue(3)
|
||||
|
||||
q.put('1')
|
||||
q.put('2')
|
||||
q.put('3')
|
||||
# q.put(3) # 队列已满,阻塞方式:等待队列中 get('旧数据')
|
||||
|
||||
try:
|
||||
q.put_nowait('4') # 非阻塞方式:但会抛出异常
|
||||
except:
|
||||
print('队列已经满了')
|
||||
|
||||
print(q.full())
|
||||
|
||||
print(q.get())
|
||||
print(q.get())
|
||||
print(q.get())
|
||||
# print(q.get()) # 队列已空,阻塞方式:等待队列中 put('新数据')
|
||||
|
||||
try:
|
||||
q.get_nowait() # 非阻塞方式:但会抛出异常
|
||||
except:
|
||||
print('队列已经空了')
|
||||
|
||||
print(q.empty())
|
||||
```
|
||||
|
||||
我们可以使用队列,是的进程和进程之间的数据能够交换,比如某个进程用于产生数据,某个进程用于拿去数据。这样,进程和进程之间就可以通信了。
|
||||
|
||||
### 案例分析1
|
||||
|
||||
定义了两个进程,一个用于产生数据,一个用于消费数据,使用队列进行数据交换。
|
||||
|
||||
```python
|
||||
from multiprocessing import Process, Queue
|
||||
import time
|
||||
|
||||
|
||||
def func_put(q):
|
||||
for i in range(3):
|
||||
q.put(f"数据{i+1}")
|
||||
|
||||
|
||||
def func_get(q):
|
||||
time.sleep(1)
|
||||
while True:
|
||||
try:
|
||||
print(f"GET到数据:{q.get_nowait()}")
|
||||
except Exception:
|
||||
print("数据已经全部拿走")
|
||||
break
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
q = Queue()
|
||||
p_put = Process(target=func_put, args=(q,))
|
||||
p_get = Process(target=func_get, args=(q,))
|
||||
p_put.start()
|
||||
p_put.join()
|
||||
|
||||
p_get.start()
|
||||
p_get.join()
|
||||
|
||||
```
|
||||
|
||||
### 案例分析2
|
||||
|
||||
多个进程计算并通过队列返回结果
|
||||
|
||||
```python
|
||||
import multiprocessing
|
||||
import time
|
||||
|
||||
|
||||
def calculate_square(num, queue):
|
||||
result = num * num
|
||||
print(
|
||||
f"进程 {multiprocessing.current_process().name} 计算 {num} 的平方,结果是: {result}"
|
||||
)
|
||||
# multiprocessing.current_process().name 获取当前进程的名称,便于调试和输出。
|
||||
time.sleep(1)
|
||||
queue.put(result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
numbers = [1, 2, 3, 4, 5]
|
||||
queue = multiprocessing.Queue()
|
||||
processes = []
|
||||
|
||||
for num in numbers:
|
||||
p = multiprocessing.Process(target=calculate_square, args=(num, queue))
|
||||
processes.append(p)
|
||||
p.start()
|
||||
|
||||
for p in processes:
|
||||
p.join()
|
||||
|
||||
results = []
|
||||
|
||||
while not queue.empty():
|
||||
results.append(queue.get())
|
||||
|
||||
print(f"所有进程计算结果: {results}")
|
||||
print("主进程结束...")
|
||||
|
||||
```
|
||||
|
||||
### 生产者消费者模型
|
||||
|
||||
在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。
|
||||
|
||||
**为什么要使用生产者和消费者模式**
|
||||
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。
|
||||
|
||||
**什么是生产者消费者模式**
|
||||
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
|
||||
|
||||
|
||||
**示例代码**:
|
||||
|
||||
```python
|
||||
from multiprocessing import Process, Queue, current_process
|
||||
import time
|
||||
import random
|
||||
import os
|
||||
|
||||
def consumer(q):
|
||||
while True:
|
||||
res = q.get()
|
||||
if res is None: break # 接收结束信号
|
||||
time.sleep(random.randint(1, 3))
|
||||
print(f"进程 {current_process().name} 吃 {res}")
|
||||
|
||||
def producer(q):
|
||||
for i in range(10):
|
||||
time.sleep(random.randint(1, 3)) # 恢复生产者延时
|
||||
res = f"包子{i}"
|
||||
q.put(res)
|
||||
print(f"进程 {current_process().name} 生产了 {res}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
q = Queue()
|
||||
|
||||
# 生产者进程
|
||||
producers = [Process(target=producer, args=(q,)) for _ in range(1)]
|
||||
# 消费者进程
|
||||
consumers = [Process(target=consumer, args=(q,)) for _ in range(10)]
|
||||
|
||||
# 启动所有进程
|
||||
for p in producers + consumers:
|
||||
p.start()
|
||||
|
||||
# 等待生产者完成
|
||||
for p in producers:
|
||||
p.join()
|
||||
|
||||
# 发送毒丸信号(每个消费者一个)
|
||||
for _ in range(len(consumers)):
|
||||
q.put(None)
|
||||
|
||||
# 等待消费者完成
|
||||
for c in consumers:
|
||||
c.join()
|
||||
|
||||
```
|
||||
|
||||
**JoinableQueue([maxsize])**
|
||||
|
||||
创建可连接的共享进程队列。这就像是一个 Queue 对象,但队列允许项目的使用者通知生产者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。
|
||||
|
||||
| 方法 | 含义 |
|
||||
| :----- | :----- |
|
||||
| `q.task_done()` | 消费者使用此方法发出信号,表示 q.get() 返回的结果已经被处理。如果调用此方法的次数大于从队列中删除的结果数量,将引发 ValueError 异常。|
|
||||
| `q.join() ` | 生产者将使用此方法进行阻塞,直到队列中所有项目均被处理。阻塞将持续到为队列中的每个项目均调用 q.task_done() 方法为止。|
|
||||
|
||||
**示例代码**:
|
||||
|
||||
```python
|
||||
from multiprocessing import Process, JoinableQueue, current_process
|
||||
import random
|
||||
import time
|
||||
|
||||
def consumer(q):
|
||||
while True:
|
||||
res = q.get() # 阻塞
|
||||
time.sleep(random.randint(1, 3))
|
||||
print(f"进程 {current_process().name} 吃 {res}")
|
||||
q.task_done() # 每调用一次,队列内部计数器减
|
||||
|
||||
def producer(q):
|
||||
for i in range(10):
|
||||
time.sleep(random.randint(1, 3))
|
||||
res = f"包子{i}"
|
||||
q.put(res)
|
||||
print(f"进程 {current_process().name} 生产了 {res}")
|
||||
q.join() # 阻塞直到计数器归零,确保所有任务被处理
|
||||
|
||||
if __name__ == "__main__":
|
||||
q = JoinableQueue()
|
||||
|
||||
# 生产者进程
|
||||
producers = [Process(target=producer, args=(q,)) for _ in range(1)]
|
||||
# 消费者进程:主进程结束后,守护进程自动终止,避免无限阻塞
|
||||
consumers = [Process(target=consumer, args=(q,), daemon=True) for _ in range(10)]
|
||||
|
||||
# 启动所有进程
|
||||
for p in producers + consumers:
|
||||
p.start()
|
||||
|
||||
# 等待生产者完成
|
||||
for p in producers:
|
||||
p.join()
|
||||
|
||||
# 等待队列任务处理完毕
|
||||
print("所有任务已完成,程序正常退出")
|
||||
|
||||
```
|
||||
|
||||
|
||||
## 进程池
|
||||
|
||||
进程池(multiprocessing.Pool)是预先创建并管理一组子进程的技术,用于高效处理批量任务。通过复用固定数量的进程,避免频繁创建/销毁进程的开销,提升 CPU 密集型任务的性能。
|
||||
|
||||
- 那么在成千上万个任务需要被执行的时候,我们就需要去创建成千上万个进程么?首先,创建进程需要消耗时间,销毁进程也需要消耗时间。第二即便开启了成千上万的进程,操作系统也不能让他们同时执行,这样反而会影响程序的效率。因此我们不能无限制的根据任务开启或者结束进程。
|
||||
|
||||
- 定义一个池子,在里面放上固定数量的进程,有需求来了,就拿一个池中的进程来处理任务,等到处理完毕,进程并不关闭,而是将进程再放回进程池中继续等待任务。如果有很多任务需要执行,池中的进程数量不够,任务就要等待之前的进程执行任务完毕归来,拿到空闲进程才能继续执行。也就是说,池中进程的数量是固定的,那么同一时间最多有固定数量的进程在运行。这样不会增加操作系统的调度难度,还节省了开闭进程的时间,也一定程度上能够实现并发效果。
|
||||
|
||||
- `multiprocessing.Pool` 是 `multiprocessing` 模块中的一个非常有用的工具,用于管理进程池(Pool of Processes)。它允许你并行地执行函数,并且可以轻松地分配多个任务到多个进程中执行,从而提高程序的执行效率。`Pool` 使得多进程编程的管理变得更加容易,尤其是在需要并行处理大量数据时。
|
||||
|
||||
### 基本概念
|
||||
|
||||
`Pool` 是进程的集合,用于执行并行任务。它提供了一种简化的接口来并行执行多个任务,将任务分配给多个进程并管理它们的执行。
|
||||
|
||||
**进程池的好处**:
|
||||
- 通过限制并发进程的数量,可以有效地管理资源消耗。
|
||||
- 可以自动调度和分配任务到多个进程。
|
||||
- 提供了多种方法(如 `apply`、`map`、`apply_async`、`map_async`)来调度任务并收集结果。
|
||||
|
||||
**创建进程池**:
|
||||
|
||||
```python
|
||||
from multiprocessing import Pool
|
||||
|
||||
# 创建包含4个子进程的进程池(默认值为CPU核心数)
|
||||
pool = Pool(processes=4)
|
||||
```
|
||||
|
||||
**关闭进程池**:
|
||||
|
||||
```python
|
||||
pool.close() # 停止接收新任务
|
||||
pool.join() # 阻塞主进程,等待所有子进程结束
|
||||
```
|
||||
|
||||
### 提交任务
|
||||
|
||||
**代码示例**:同步阻塞方式
|
||||
|
||||
```python
|
||||
from multiprocessing import Pool
|
||||
import time
|
||||
|
||||
|
||||
def task(x):
|
||||
time.sleep(1) # 模拟耗时操作
|
||||
return x * x
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
start = time.time()
|
||||
with Pool(4) as pool:
|
||||
results = []
|
||||
for i in range(4):
|
||||
res = pool.apply(task, (i,)) # 同步提交,逐个执行
|
||||
results.append(res)
|
||||
print(f"结果:{results},耗时:{time.time()-start:.2f}秒")
|
||||
```
|
||||
|
||||
**代码示例**:异步非阻塞方式
|
||||
|
||||
```python
|
||||
from multiprocessing import Pool
|
||||
import time
|
||||
|
||||
def task(x):
|
||||
time.sleep(1)
|
||||
return x * x
|
||||
|
||||
if __name__ == '__main__':
|
||||
start = time.time()
|
||||
with Pool(4) as pool: # 自动调用 pool.close() 和 pool.join()
|
||||
async_results = [pool.apply_async(task, (i,)) for i in range(4)] # 异步提交任务
|
||||
results = [res.get() for res in async_results] # 阻塞直到所有结果返回
|
||||
print(f"结果:{results},耗时:{time.time()-start:.2f}秒")
|
||||
|
||||
```
|
||||
|
||||
**示例代码**:批量处理
|
||||
|
||||
```python
|
||||
from multiprocessing import Pool
|
||||
import time
|
||||
|
||||
|
||||
def task(x):
|
||||
time.sleep(1)
|
||||
return x * x
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
start = time.time()
|
||||
with Pool(4) as pool:
|
||||
results = pool.map(task, range(4)) # 批量提交
|
||||
print(f"结果:{results},耗时:{time.time()-start:.2f}秒")
|
||||
|
||||
```
|
||||
|
||||
### 回调函数
|
||||
|
||||
需要回调函数的场景:进程池中任何一个任务一旦处理完了,就立即告知主进程:我好了,你可以处理我的结果了。主进程则调用一个函数去处理该结果,该函数即回调函数。
|
||||
|
||||
我们可以把耗时间(阻塞)的任务放到进程池中,然后指定回调函数(主进程负责执行),这样主进程在执行回调函数时就省去了I/O的过程,直接拿到的是任务的结果。
|
||||
|
||||
在 Python 的多进程编程中,apply_async 的回调函数(callback)是一种异步处理任务结果的机制,它允许在子进程完成任务后自动触发特定逻辑,而无需阻塞主进程。
|
||||
|
||||
**回调函数的执行机制**:
|
||||
|
||||
- 运行环境:回调函数在**主进程**中执行,而非子进程。这意味着:回调函数内无法直接操作子进程的变量或资源。回调中应避免耗时操作,否则会阻塞主进程。
|
||||
|
||||
- 参数传递:回调函数默认接收**任务的返回值**作为唯一参数。若需传递额外参数,可通过闭包或全局变量实现。
|
||||
|
||||
**示例代码**:
|
||||
|
||||
```python
|
||||
from multiprocessing import Pool
|
||||
|
||||
def square(x):
|
||||
return x * x
|
||||
|
||||
def collect_result(result, result_list):
|
||||
result_list.append(result)
|
||||
|
||||
if __name__ == '__main__':
|
||||
with Pool(4) as pool:
|
||||
results = []
|
||||
# 提交10个任务并绑定回调
|
||||
for i in range(10):
|
||||
pool.apply_async(square, (i,), callback=lambda r: collect_result(r, results))
|
||||
pool.close()
|
||||
pool.join()
|
||||
print("最终结果:", sorted(results))
|
||||
```
|
||||
|
||||
### 案例分析
|
||||
|
||||
实时爬取网页内容并存储至本地文件。
|
||||
|
||||
```python
|
||||
from multiprocessing import Pool
|
||||
import requests
|
||||
import os
|
||||
import time
|
||||
|
||||
|
||||
def get_page(url):
|
||||
print("<进程%s> get %s" % (os.getpid(), url))
|
||||
respone = requests.get(url)
|
||||
if respone.status_code == 200:
|
||||
return {"url": url, "text": respone.text}
|
||||
|
||||
|
||||
def pasrse_page(res):
|
||||
print("<进程%s> parse %s" % (os.getpid(), res["url"]))
|
||||
parse_res = "url:<%s> size:[%s]\n" % (res["url"], len(res["text"]))
|
||||
with open("db.txt", "a") as f:
|
||||
f.write(parse_res)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
start = time.time()
|
||||
urls = [
|
||||
"https://www.baidu.com",
|
||||
"https://www.python.org",
|
||||
"https://www.openstack.org",
|
||||
"http://www.sina.com.cn/",
|
||||
]
|
||||
|
||||
with Pool(4) as pool:
|
||||
# 提交任务并绑定回调
|
||||
async_results = [pool.apply_async(get_page, (i,), callback=pasrse_page) for i in urls]
|
||||
results = [res.get() for res in async_results] # 获取结果
|
||||
print(f"耗时:{time.time()-start:.2f}秒")
|
||||
|
||||
```
|
||||
|
||||
# 课后作业
|
||||
|
||||
- [必须] 动手完成本章节案例
|
||||
- [扩展] 阅读官方文档相关章节
|
||||
|
BIN
Python/Python并发编程/进程/进程状态转换图1.png
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
Python/Python并发编程/进程/进程状态转换图2.png
Normal file
After Width: | Height: | Size: 187 KiB |