多道技术的思想: 利用单核实现并发的效果
这里我们要清楚2个概念,什么是并行什么是并发
并发
看起来像同时运行的就可以称之为并发
并行
真正意义上的同时执行
现在我们知道,并行肯定是并发,但是单核的计算机是肯定不能实现并行,却可以实现并发。
多道技术节省了多个程序运行的总耗时
空间上的复用与时间上的复用
空间上的复用
多个程序公用一套计算机硬件
时间上的复用
"""
切换cpu分为两种情况:
1.当一个程序遇到IO操作的时候,操作系统会剥夺程序的cpu执行权限
作用:提高cpu利用率并也不影响程序的执行效率
2.当一个程序长时间占用cpu的时候,操作系统也会剥夺该程序的cpu执行权限
即各个程序轮流使用cpu实现多个程序运行
作用:降低了程序的执行效率(原时间+切换时间)
"""
"""
程序就是一堆躺在硬盘上的代码,是“死”的
进程是表示程序正在运行的过程,是“活”的
"""
要想要多个进程交替运行,操作系统必须对这些进程进行调度,这个调度也不是随机进行的,而是需要遵循一定的法则,由此就有了进度的调度算法。
"""描述的是任务的提交方式"""
同步:任务提交后,原地等待任务的返回结果,任务完成后返回结果再去做其他事情。
异步:任务提交后,不原地等待任务的返回结果,直接去做其他事情,任务的返回结果会有一个异步回调机制自动处理
"""描述程序的运行状态"""
阻塞:程序三状态中的阻塞态
非阻塞:程序三状态中的就绪态与运行态
理想状态:我们应该让我们的程序永远处于就绪态和运行态之间切换
最高效的组合就是异步非阻塞。
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
from multiprocessing import Process
import time
def task(name):
print(f'{name} is running')
time.sleep(3)
print(f'{name} is over')
"""
windows操作系统下一定要在main内创建
因为windows下创建进程类似于模块导入的方式
会从上往下依次执行代码
linux中则是直接将代码完整的拷贝一份
"""
if __name__ == '__main__':
# 1.创建一个进程对象
p = Process(target=task, args=('No.1',))
# 2.通过操作系统创建一个进程
p.start()
print('Master')
Master
No.1 is running
No.1 is over
# 第二种方式:类的继承
import time
from multiprocessing import Process
class MyProgress(Process):
def run(self):
print('task is running!')
time.sleep(2)
print('task is over!')
if __name__ == '__main__':
p = MyProgress()
p.start()
print('Master')
Master
task is running!
task is over!
可以看到两种创建进程的方式都实现了异步,在p进程执行的过程中并没有影响到后续master的打印。
"""
创建进程就是在内存中申请一块内存空间
一个进程对应在内存中就是一块独立的内存空间
多个进程对应在内存中就是多块独立的内存空间
多个进程之间互不影响
进程与进程之间默认情况下数据是无法直接交互,需要借助第三方工具、模块。
"""
join方法是让主进程等待子进程代码运行结束后再继续运行,不影响其他子进程的执行。
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import time
from multiprocessing import Process
def task(name, n):
print(f'{name} is running')
time.sleep(n)
print(f'{name} is over')
if __name__ == '__main__':
p = Process(target=task, args=('No.1', 1))
p.start()
# 主进程等待子进程p运行结束后再继续运行
p.join()
print('Master')
运行结果
No.1 is running
No.1 is over
Master
join方法类似于拥有vip进行插队
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
from multiprocessing import Process
data = 100
def task():
global data
data = 666
print(f'task data : {data}')
if __name__ == '__main__':
p = Process(target=task)
p.start()
p.join()
print(f'main data : {data}')
运行结果
task data : 666
main data : 100
计算机上运行着很多进程,计算机会给每一个运行的进程分配一个pid。
"""
windows
在cmd中输入tasklist查看pid
tasklist | findstr pid查看pid对应进程
linux
在终端中输入ps aux查看pid
ps aux | grep pid查看pid对应进程
"""
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
from multiprocessing import Process
import os
import time
def task():
time.sleep(3)
print(f'子进程的pid:{os.getpid()}')
print(f'子进程的父进程的pid:{os.getppid()}')
if __name__ == '__main__':
p = Process(target=task)
p.start()
p.terminate() # 杀死当前进程
print(f'当前进程是否存活:{p.is_alive()}')
print(f'主进程的pid :{os.getpid()}')
print(f'主进程的父进程的pid :{os.getppid()}')
运行结果
当前进程是否存活:True
主进程的pid :3432
主进程的父进程的pid :7024
故名思意,就是死了还没死透。
当你开设了子进程之后,该进程死后不会立刻释放占用的进程号。因为要让父进程能够看到它开设的子进程的基本信息,如pid,运行时间等。所有的进程都会步入僵尸进程。
父进程等待子进程运行结束或者父进程调用join方法会回收子进程占用的pid号
子进程存活,父进程意外死亡,此时该子进程就为孤儿进程。
操作系统此时会开设一块区域专门回收孤儿进程的相关资源。
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import time
from multiprocessing import Process
def task():
print('子进程正在存活')
time.sleep(5)
print('子进程正常死亡')
if __name__ == '__main__':
p = Process(target=task)
p.daemon = True
p.start()
time.sleep(1)
print('主进程死亡')
运行结果
子进程正在存活
主进程死亡
可以看到我们将p设为守护进程后,p会随主进程死亡而一起死亡。这样的进程就是守护进程。而设置守护进程一定要在进程启动之前对其进行设置。
多个进程操作同一份数据的时候,会出现数据错乱的问题。针对该问题,解决方式就是加锁处理:将并发变为串行,牺牲效率但是保证了数据的安全
互斥锁模拟抢票
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import json
import random
import time
from multiprocessing import Process, Lock
# 查票
def search(user):
with open('data', 'r', encoding='utf-8') as f:
dic = json.load(f)
print(f'用户{user}查询余票{dic.get("ticket")}')
# 买票
def buy(user):
with open('data', 'r', encoding='utf-8') as f:
dic = json.load(f)
# 模拟网络延迟
time.sleep(random.randint(1, 5))
if dic.get('ticket') > 0:
dic['ticket'] -= 1
with open('data', 'w', encoding='utf-8') as f:
json.dump(dic, f)
print(f'用户{user}买票成功')
else:
print('购票失败')
def run(user, lock):
search(user)
lock.acquire() # 加锁
buy(user)
lock.release() # 解锁
if __name__ == '__main__':
lock = Lock()
for i in range(1, 6):
p = Process(target=run, args=(i, lock))
p.start()
data中的数据
{
"ticket": 1}
运行结果
用户1查询余票1
用户2查询余票1
用户3查询余票1
用户5查询余票1
用户4查询余票1
用户1买票成功
购票失败
购票失败
购票失败
购票失败
在上述实验中如果不加入互斥锁时,程序运行速度很快并且用户1-用户5都能购买到票,而票总共只有一张,这显然不合理。在加入互斥锁后程序运行效率大幅降低,但只有一个用户可以买到票,这就是我们牺牲了程序的效率而保证了数据的安全性
注意:
我们知道进程与进程之间的数据是相互隔离的无法互相调用,但实际生产环境中我们却常需要在进程见进行相互通信,这里我们引入IPC机制(Intent Process Communication),意识就是进程间通信。注意IPC机制并不是只在编程语言中存在,它在操作系统中同样存在。而在python中我们可以通过管道与队列两种方式来实现进程间的通信,实现原理为在进程间建立一个中转的空间,让进程与进程之间通过队列或管道进行数据的交互。
"""
管道:subprocess模块
stdin stdout stderr
"""
"""
队列是在管道的基础上增加了锁等一系列的功能,所以我们在进程间的通信常用队列
队列:先进先出
堆栈:先进后出
"""
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
from multiprocessing import Queue
# 创建一个队列
# 参数表示生成的队列最大可以同时存放的数据量 python3.7默认参数为2147483647
q = Queue(3)
# put方法往队列中存数据
q.put('chaney')
q.put('tony')
q.put('Jerry')
# get方法从队列中取数据
q1 = q.get()
q2 = q.get()
q3 = q.get()
# 可以看到队列的中数据遵循先进先出原则
print(q1, q2, q3)
运行结果
chaney tony Jerry
在put和get方法存取数据时,如果存数据超过队列最大数据量或取数据超出所有数据量,该方法会原地阻塞,程序会卡住。这里我们可以增加if判断或者异常捕获来解决程序的健壮性。
在了解了队列的前置知识后我们来实现一个简单的IPC进程通信
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
"""
主进程与子进程借助队列通信
"""
from multiprocessing import Queue, Process
def task(q):
# 通过get方法从队列中取到主进程数据
print(f'{q.get()} is running')
if __name__ == '__main__':
q = Queue() # 创建队列
p = Process(target=task, args=(q,)) # 开子进程
p.start()
q.put('task') # 通过put方法将数据通过队列给到子进程
运行结果
task is running
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
"""
子进程与子进程借助队列通信
"""
from multiprocessing import Queue, Process
def producer(q):
# 往队列中存数据
q.put('message from producer')
def consumer(q):
# 取出队列中的数据
print(q.get())
if __name__ == '__main__':
q = Queue() # 创建队列
p1 = Process(target=producer, args=(q,))
p2 = Process(target=consumer, args=(q,))
p1.start()
p2.start()
运行结果
message from producer