15、python并发编程

15、python并发编程

  • 多道技术
  • 进程理论
  • 开启进程的两种方式
  • 进程对象的join方法
  • 进程之间数据相互隔离
  • 进程对象的其他方法
  • 僵尸进程与孤儿进程
  • 守护进程与互斥锁
  • 进程通信IPC机制
  • 生产者消费模型

一、多道技术

多道技术的思想: 利用单核实现并发的效果

这里我们要清楚2个概念,什么是并行什么是并发

  • 并发

    ​ 看起来像同时运行的就可以称之为并发

  • 并行

    ​ 真正意义上的同时执行

现在我们知道,并行肯定是并发,但是单核的计算机是肯定不能实现并行,却可以实现并发。

15、python并发编程_第1张图片

多道技术节省了多个程序运行的总耗时

多道技术重点


空间上的复用与时间上的复用

  • 空间上的复用

    ​ 多个程序公用一套计算机硬件

  • 时间上的复用

    • 如上图,单道完成10秒,而多道在执行A的时候在读取任务B,完成只需要6秒,类似数学中的统筹规划。

多道中的CPU切换

"""
切换cpu分为两种情况:
	1.当一个程序遇到IO操作的时候,操作系统会剥夺程序的cpu执行权限
	  作用:提高cpu利用率并也不影响程序的执行效率
	2.当一个程序长时间占用cpu的时候,操作系统也会剥夺该程序的cpu执行权限
	  即各个程序轮流使用cpu实现多个程序运行
	  作用:降低了程序的执行效率(原时间+切换时间)
"""

二、进程理论

程序与进程的区别

"""
程序就是一堆躺在硬盘上的代码,是“死”的
进程是表示程序正在运行的过程,是“活”的
"""

进程调度

要想要多个进程交替运行,操作系统必须对这些进程进行调度,这个调度也不是随机进行的,而是需要遵循一定的法则,由此就有了进度的调度算法。

  • 先来先服务调度算法

    """对长作业有利,对短作业无益"""
    
  • 短作业优先调度算法

    """对短作业有利,对长作业无益"""
    
  • 时间片轮转法+多级反馈队列

    15、python并发编程_第2张图片

进程运行的三状态图

15、python并发编程_第3张图片

两对重要概念

  • 同步与异步
"""描述的是任务的提交方式"""
同步:任务提交后,原地等待任务的返回结果,任务完成后返回结果再去做其他事情。
    
异步:任务提交后,不原地等待任务的返回结果,直接去做其他事情,任务的返回结果会有一个异步回调机制自动处理
  • 阻塞与非阻塞
"""描述程序的运行状态"""
阻塞:程序三状态中的阻塞态
非阻塞:程序三状态中的就绪态与运行态

理想状态:我们应该让我们的程序永远处于就绪态和运行态之间切换

最高效的组合就是异步非阻塞。

三、开启进程的两种方式

第一种方式(常用)

#!/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方法

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

15、python并发编程_第4张图片

六、进程对象的其他方法

pid

计算机上运行着很多进程,计算机会给每一个运行的进程分配一个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机制

我们知道进程与进程之间的数据是相互隔离的无法互相调用,但实际生产环境中我们却常需要在进程见进行相互通信,这里我们引入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

15、python并发编程_第5张图片

你可能感兴趣的:(python,python,多线程,队列)