python(进程与多进程)

今日内容概要

  • 同步与异步
  • 阻塞与非阻塞
  • 创建join方法
  • 进程间数据隔离
  • 进程间通信之IPC机制
  • 进程对象诸多方法
  • 生产者与消费者类型
  • 互斥锁

同步与异步

同步:

所谓的同步就是一个任务的完成需要依赖另一个任务,只有被依赖的那个任务完成之后,依赖的任务才能算完成,所以这是一种可靠的任务序列

举个例子:我们叫一个人去吃饭,我们告诉那个人一起去吃饭,然后就站在原地等他,啥也不做,等他完成自己的事情后再一起去吃饭。

异步:

异步就是不需要等待依赖的任务是否完成,只是通知那个被依赖的任务要完成的工作是什么,就可以离开了,做自己的事,等被通知的任务对象完成了自己的事后,再向自己反馈,这是一种不可靠的任务序列

例子:
	运用上面的那个例子,叫人吃饭,被叫的那个人不需要给予回应,我通知了他就走了,他忙完自己的事是要一起去吃饭还是不去,到时候给我一个回馈就行了。

阻塞与非阻塞

阻塞与非阻塞一共有四种形式,分别是以下:

1.同步阻塞形式:

效率最低,拿上面的例子来说,喊了人啥也不做,就干巴巴的站在原地等人。

2.异步阻塞形式:

异步操作是可以被阻塞住的,只不过它不是在处理消息的时候被阻塞,而是在等消息通知的时候被阻塞。

例子:
就像是在银行等待办理业务的人,采用的是异步的方式去等待消息被触发(通知),也就是领了一张小纸条,假如在这段时间里他不能离开银行做其它的事情,那么很显然,这个人被阻塞在了这个等待的操作上面;


3.同步非阻塞形式:

 实际上是效率低下的。

  想象一下你一边打着电话一边还需要抬头看到底队伍排到你了没有,如果把打电话和观察排队的位置看成是程序的两个操作的话,这个程序需要在这两种不同的行为之间来回的切换,效率可想而知是低下的。

4.异步阻塞形式:

效率是最高的

例子:
	假如在银行办业务,如果是采用的异步阻塞形式的方式,就是自己去领了一个号码,在等待的过程中有自己的事情要做,就告诉大堂经理让他在叫到自己的号码的时候通知你,自己就不会被阻塞在这个等待上面,这就是 异步 +阻塞 的形式

注意:

因为很多时候同步操作会以阻塞的形式表现出来,同样的,很多人也会把异步和非阻塞混淆,因为异步操作一般都不会在真正的IO操作处被阻塞

创建进程的多种方式

最简单的方式:

1.鼠标双击程序的图标

2.python代码创建进程

创建一个进程(Process类)

创建类需要用到multiprocess,multiprocess不是一个模块而是python中一个操作、管理进程的包。

Process([group [, target [, name [, args [, kwargs]]]]]),由该类实例化得到的对象,表示一个子进程中的任务(尚未启动)

强调:
1. 需要使用关键字的方式来指定参数
2. args指定的为传给target函数的位置参数,是一个元组形式,必须有逗号

参数说明:

    1. group参数未使用,值始终为None
    2 .target表示调用对象,即子进程要执行的任务  
    3 .name为子进程的名称
    4 .args表示调用对象的位置参数元组,args=(1,2,'anne',)
    5 .kwargs表示调用对象的字典,kwargs={'name':'anne','age':18}

   start() 函数:通过调用它,可以直接启动我们创建的进程。它会马上执行我们进程中传入的函数,start 函数没有任何参数,也没有返回值。

image

方式一:
	建立函数,直接创建进程
    from multiprocessing import Process
    import time


def task(name):
    print('task is runing 子进程',name) #2. 第二步打印
    time.sleep(3)
    print('task is over 子进程',name) # 3.第三步打印

if __name__ == '__main__':
    p1 = Process(target=task ,args=('momo',)) # 里面输入的位置参数
    p1.start() # 异步,告诉操作系统创建一个新的进程并在该进程中执行task
    # task()# 同步
    print('主进程')  # 1.最先打印主进程
    
    
方式二:
    通过继承类的方式创建进程
    from multiprocessing import Process
import time
class MyProcess(Process):
    def __init__(self,name):  # (修改__init__方法)
        super().__init__() # 这里需要重新调用(父类)__init__的方法
        self.name = name


    def run(self):
        print('run is runing',self.name)  # 2,执行的第二步
        time.sleep(3)
        print('run is over',self.name) # 3.执行的第三步


        
if __name__ == '__main__':
    obj =MyProcess('momo')
    obj.start()
    print('主进程')  # 1.执行的第一步
	

这里再思考一个问题,如果是每一个子进程都单独的通过 .start 去启动,那么在子进程很多的情况下,启动的确实会有一些慢了。这个时候我们就可以通过 for 循环的方式去启动子进程。方式如下:

   for sun_process in (p1,p2):
        sun_process.start()

join方法

join同样的也会存在着这样一种情况,我们希望子进程(结束后再)去执行我们的主进程,这里就用到了join函数

	 p1.join():主线程等待p1终止(强调:是主线程处于等的状态,而p是处于运行的状态)。

代码:

from multiprocessing import Process
import time

def task(name,n):
    print('%s 是开始'%name)
    time.sleep(n)
    print('%s 是结束'%name)


if __name__ == '__main__':
    p1 = Process(target=task,args=('水水',1))
    p2 = Process(target=task,args=('默默',2))
    p3 = Process(target=task,args=('胖胖',3))
    # p1.start() #异步
    """主进程等待子进程代运行结束在执行"""
    # p1.join()
    # print('主进程')
    start_time = time.time()
    p1.start()
    p2.start()
    p3.start()
    p1.join()
    p2.join()
    p3.join()
    print(time.time() - start_time) # 3秒
    # start_time = time.time()
    # p1.start()
    # p1.join()
    # p2.start()
    # p2.join()
    # p3.start()
    # p3.join()
    # print(time.time() - start_time) # 6秒

进程间数据隔离

同一台计算机上的多个进程数据是严格意义上的物理隔离(默认情况下)
from multiprocessing import Process
import time

name = '我是全局变量名'

def task():
    global name
    name = '我是局部变量名'
    print('子进程的task 函数查看了name',name)  #1. 子进程的task 函数查看了name 我是局部变量名

if __name__ == '__main__':
    p1 = Process(target=task)
    p1.start() # 创建子进程
    """如果这里不等待的话,就会先执行主进程带码的全局变量名"""
    # time.sleep(3) # 主进程代码等待3秒 让子进程先执行
    print(name)  # 主进程打印name  #2. 我是全局变量名

进程间通信之IPC机制

​ 为了进程安全起见,两个进程之间的数据是不能够互相访问的(默认情况下),进程与进程之间的数据是不可以互相访问的,而且每一个进程的内存是独立的。多进程的资源是独立的,不可以互相访问,如果想多个进程之间实现数据交互就必须通过中间件实现。

进程队列(Queue)通信:

Queue([maxsize]):建立一个共享的队列(其实并不是共享的,实际是克隆的,内部维护着数据的共享),多个进程可以向队列里存/取数据。其中,参数是队列最大项数,省略则无限制。

Queue方法:

from multiprocessing import Queue

q = Queue(3)  # 括号内可以指定存储数据的个数
q.put()  # 往消息队列中添加数据
q.full()  # 判断队列是否已满
q.get()  # 从消息队列中取数据
q.empty()  # 判断队列是否为空

# 队列为空, 使用get会等待,直到队列有数据以后再取值
q.get()
# 队列为空,取值的时候不等待,但是取不到值那么直接崩溃了
q.get_nowait()

# 建议: 获取队列的数据统一get,因为不能保证代码不会有问题
"""
full() empty() 在多进程中都不能使用!!!因为多进程中不确定性
"""

 put方法用以插入数据到队列中,put方法还有两个可选参数:blocked和timeout。如果blocked为True(默认值),并且timeout为正值,该方法会阻塞timeout指定的时间,直到该队列有剩余的空间。如果超时,会抛出Queue.Full异常。如果blocked为False,但该Queue已满,会立即抛出Queue.Full异常。

    get方法可以从队列读取并且删除一个元素。同样,get方法有两个可选参数:blocked和timeout。如果blocked为True(默认值),并且timeout为正值,那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。如果blocked为False,有两种情况存在,如果Queue有一个值可用,则立即返回该值,否则,如果队列为空,则立即抛出Queue.Empty异常。

代码:

from multiprocessing import Process, Queue


def product(q):
    q.put('子进程p添加的数据')

def consumer(q):
    print('子进程获取队列中的数据', q.get())


if __name__ == '__main__':
    q = Queue()
    # 主进程往队列中添加数据
    # q.put('我是主进程添加的数据')
    p1 = Process(target=consumer, args=(q,))
    p2 = Process(target=product, args=(q,))
    p1.start()
    p2.start()
    print('主')
# 输出结果:
# 主
# 子进程获取队列中的数据 子进程p添加的数据
这里的q.get()取决于是否是在执行父进程之前添加进来的数据

生产者消费者模型

生产者
	负责生产数据的“人”(线程)
消费者
	负责处理数据的“人”(线程)
	该模型除了要有生产者和消费者之外还必须要有消息队列,因为他们是通过一个容器来解决生产者和消费者的耦合问题
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

进程对象诸多方法

  • start ()函数:通过调用它,可以直接启动我们创建的进程。它会马上执行我们进程中传入的函数,start 函数没有任何参数,也没有返回值。

  • join ()函数:我们说过,主进程和子进程的程序会同时运行,互不影响。这样就会有一个问题,有可能是 子进程 先执行完它的业务,也有可能是 主进程 先执行完它的业务逻辑。如果有的时候我们必须要先执行完 子进程的业务 再执行 主进程的业务 。则通过调用 join 函数,在这一函数下面执行的主进程业务要等待子进程完成之后才会继续执行。我们将 join 这样的函数叫做 等待/阻塞函数。join 函数没有任何参数,也没有返回值。

  • kill ()函数:如果我们在执行子进程的过程中发现不需要这个子进程继续运行了,就可以使用 kill 函数杀死当前的这个子进程,杀死的这个子进程不会在执行子进程中函数的业务逻辑。kill 函数没有任何参数,也没有返回值。

  • is_alive ()函数:通过调用这个函数可以(判断当前的进程是否是存活状态),它返回一个 bool 值。True 表示当前进程还在,程序还在继续执行;如果是 False 则代表当前进程已经结束了

  • terminate()函数:终止进程

    PS:计算机操作系统都有相对应的命令可以直接杀死进程

  • 如何查看进程号:

    from multiprocessing import Process, current_process
     	current_process()
     	current_process().pid  
    	import os
     	os.getpid()
      	os.getppid()
    

image

守护进程:

主进程创建守护进程:

1.守护进程会在主进程代码执行结束后就终止

2.守护进程内无法在开启子进程,否则就抛出异常:AssertionError: daemonic processes are not allowed to have children。

注意:进程之间都是相互独立的,主进程代码运行结束,守护进程随即也会终止,但是当有多个子进程时,只有设置了守护进程的子进程才会随着主进程的结束而结束,其他的子进程不受到影响。

from multiprocessing import Process
import time
import random
 
 
class Run(Process):
    def __init__(self, name):
        super().__init__()
        self.name = name    # MyProcess的__init__方法会执行self.name=MyProcess-1,所以加到这里,会覆盖我们的self.name=name,因而self.name = name要写在__init__方法之后
 
    def run(self):    # run方法会在start方法调用后自动执行
        print('%s 进程开始运行。。。' % self.name)
        time.sleep(random.randrange(1, 3))
        print('%s 进程运行结束。' % self.name)
 
 
if __name__ == '__main__':
    p = Run('my')
    p.daemon = True  # 一定要在p.start()前设置,设置p为守护进程,禁止p创建子进程,并且父进程代码执行结束,p即终止运行
    p.start()
    time.sleep(1)
    print('主进程结束。')
 
# 结果是:
my 进程开始运行。。。
主进程结束。

僵尸进程与孤儿进程:

僵尸进程
	就是进程执行完毕后不会立刻销毁所有的数据,会有一些信息短暂的保留了下来
    比如进程号、进程执行时间、进程消耗功率等、、、留给父进程查看
    PS:所有的进程都会变成僵尸进程
    
孤儿进程
	子进程正常运行,父进程意外死亡,操作系统就会针对孤儿进程会派遣“福利院”前来管理,等子进程结束后进行处理子进程后事

多数据错乱问题:

from multiprocessing import Process
import time
import json
import random


# 查看票数
def search(name):
    with open(r'data.json', 'r', encoding='utf8') as f:
        data = json.load(f)
    print('%s正在查票,当前余票为:%s' % (name, data.get('ticket_num')))


# 买票
def buy(name):
    # 再次确认票
    with open(r'data.json', 'r', encoding='utf8') as f:
        data = json.load(f)
        # 模拟网络延迟
        time.sleep(random.randint(1, 3))
        # 判断是否有票,有就买
        if data.get('ticket_num') > 0:
            data['ticket_num'] -= 1
            with open(r'data.json', 'w', encoding='utf8') as f:
                json.dump(data, f)
            print('%s买票成功' % name)
        else:
            print('%s购买失败,没有抢到票' % name)


def run(name):
    search(name)
    buy(name)


if __name__ == '__main__':
    for i in range(5):
        p = Process(target=run, args=('用户%s' % i,))
        p.start()

#输出结果:
# 用户0正在查票,当前余票为:1
# 用户1正在查票,当前余票为:1
# 用户2正在查票,当前余票为:1
# 用户3正在查票,当前余票为:1
# 用户4正在查票,当前余票为:1
# 用户0买票成功
# 用户2买票成功
# 用户4买票成功
# 用户1买票成功
# 用户3买票成功

造成这样的问题句式因为多进程同时访问了(查票)系统的时候,的到的是当时那一秒中的数据信息,有多少的用户访问就是多少的用户看到,他们同时购买票是系统就会做出判断,显示就是为有票,所以就能买票成功,其实就只有一张票,却出现了都买票成功的现象,所以应该在判断的时候做出判断,如果已经被访问并选择了用户就进不去买票的界面

互斥锁

通过刚刚的学习,我们千方百计实现了程序的异步,让多个任务可以同时在几个进程中并发处理,他们之间的运行没有顺序,一旦开启也不受我们控制。尽管并发编程让我们能更加充分的利用IO资源,但是也给我们带来了新的问题。

  当多个进程使用同一份数据资源的时候,就会引发数据安全或顺序混乱问题。

#加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行的修改,没错,速度是慢了,但牺牲了速度却保证了数据安全。
虽然可以用文件共享数据实现进程间通信,但问题是:
	1.效率低(共享数据基于文件,而文件是硬盘上的数据)
	2.需要自己加锁处理

#因此我们最好找寻一种解决方案能够兼顾:
	1、效率高(多个进程共享一块内存的数据)
	2、帮我们处理好锁问题。这就是mutiprocessing模块为我们提供的基于消息的IPC通信机制:
队列和管道
	队列和管道都是将数据存放于内存中
	队列又是基于(管道+锁)实现的,可以让我们从复杂的锁问题中解脱出来,我们应该尽量避免使用共享数据,尽可能使用消息传递和队列,避免处理复杂的同步和锁问题,而且在进程数目增多时,往往可以获得更好的可获展性。

image

posted @ 2022-11-18 17:06  亓官扶苏  阅读(202)  评论(0)    收藏  举报