day20-多并发编程基础(一)

重新写一下吧,系统奔溃了,以前写的完全没了,悲催,今日主要写进程

1. 进程的理论知识

2. python中的进程操作

开始今日份整理,加油,你是最胖的!!!

 

1. 进程的理论知识

1.1 操作系统的背景知识

  顾名思义,进程即正在执行的一个过程。进程是对正在运行程序的一个抽象。

  进程的概念起源于操作系统,是操作系统最核心的概念,也是操作系统提供的最古老也是最重要的抽象概念之一。操作系统的其他所有内容都是围绕进程的概念展开的。

PS:即使可以利用的cpu只有一个(早期的计算机确实如此),也能保证支持(伪)并发的能力。将一个单独的cpu变成多个虚拟的cpu(多道技术:时间多路复用和空间多路复用+硬件上支持隔离),没有进程的抽象,现代计算机将不复存在。

必备的理论基础:

#一 操作系统的作用:
    1:隐藏丑陋复杂的硬件接口,提供良好的抽象接口
    2:管理、调度进程,并且将多个进程对硬件的竞争变得有序

#二 多道技术:
    1.产生背景:针对单核,实现并发
    ps:
    现在的主机一般是多核,那么每个核都会利用多道技术
    有4个cpu,运行于cpu1的某个程序遇到io阻塞,会等到io结束再重新调度,会被调度到4个
    cpu中的任意一个,具体由操作系统调度算法决定。
    
    2.空间上的复用:如内存中同时有多道程序
    3.时间上的复用:复用一个cpu的时间片
       强调:遇到io切,占用cpu时间过长也切,核心在于切之前将进程的状态保存下来,这样
            才能保证下次切换回来时,能基于上次切走的位置继续运行

#三 I/O操作
    #i = input 输入
    从任何其他地方到--->内存
    读文件,json.load,从网络上接收信息
    #o = output 输出
    内存--->放到任何其他地方
    写文件,json.dump,向网络上发送数据 send sendto

1.2 进程的概念以及特性

  进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

  • 狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。
  • 广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

进程的概念

第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。
第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。[3] 
进程是操作系统中最基本、重要的概念。是多道程序系统出现后,为了刻画系统内部出现的动态情况,描述系统内部各道程序的活动规律引进的一个概念,所有多道程序设计操作系统都建立在进程的基础上。

操作系统引入进程的概念的原因

从理论角度看,是对正在运行的程序过程的抽象;
从实现角度看,是一种数据结构,目的在于清晰地刻画动态系统的内在规律,有效管理和调度进入计算机系统主存储器运行的程序。

进程的特征

动态性:进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的。
并发性:任何进程都可以同其他进程一起并发执行
独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位;
异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进
结构特征:进程由程序、数据和进程控制块三部分组成。
多个不同的进程可以包含相同的程序:一个程序在不同的数据集里就构成不同的进程,能得到不同的结果;但是执行过程中,程序不能发生改变。

进程与程序的区别

程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
而进程是程序在处理机上的一次执行过程,它是一个动态的概念。
程序可以作为一种软件资料长期存在,而进程是有一定生命期的。
程序是永久的,进程是暂时的。

注意:同一个程序执行两次,就会在操作系统中出现两个进程,所以我们可以同时运行一个软件,分别做不同的事情也不会混乱。

1.3 进程的调用方法

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

  • 先来先服务调度方法
  • 短作业优先调度方法
  • 时间片轮转法,现代操作系统常用
  • 多级反馈队列,多种调度方法的结合体

1.4 进程的并发与并行

并行 : 并行是指两者同时执行,比如赛跑,两个人都在不停的往前跑;(资源够用,比如三个线程,四核的CPU )

并发 : 并发是指资源有限的情况下,两者交替轮流使用资源,比如一段路(单核CPU资源)同时只能过一个人,A走一段后,让给B,B用完继续给A ,交替使用,目的是提高效率。

区别:

并行是从微观上,也就是在一个精确的时间片刻,有不同的程序在执行,这就要求必须有多个处理器。
并发是从宏观上,在一个时间段上可以看出是同时执行的,比如一个服务器同时处理多个session。

1.5 同步\异步\阻塞\非阻塞

  1.5.1 进程的状态

在了解其他概念之前,我们首先要了解进程的几个状态。在程序运行的过程中,由于被操作系统的调度算法控制,程序会进入几个状态:就绪,运行和阻塞。

  (1)就绪(Ready)状态

  当进程已分配到除CPU以外的所有必要的资源,只要获得处理机便可立即执行,这时的进程状态称为就绪状态。

  (2)执行/运行(Running)状态当进程已获得处理机,其程序正在处理机上执行,此时的进程状态称为执行状态。

  (3)阻塞(Blocked)状态正在执行的进程,由于等待某个事件发生而无法执行时,便放弃处理机而处于阻塞状态。引起进程阻塞的事件可有多种,例如,等待I/O完成、申请缓冲区不能满足、等待信件(信号)等。

下面这些就是我最蒙圈的一部分内容

  1.5.2 同步与异步

  • 所谓同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。
  • 所谓异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列

  1.5.3 阻塞与非阻塞

  阻塞和非阻塞这两个概念与程序(线程)等待消息通知(无所谓同步或者异步)时的状态有关。也就是说阻塞与非阻塞主要是程序(线程)等待消息通知时的状态角度来说的

  1.5.4 同步/异步与阻塞/非阻塞

  1. 同步阻塞形式

  效率最低。拿上面的例子来说,就是你专心排队,什么别的事都不做。

  1. 异步阻塞形式

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

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

  1. 同步非阻塞形式

  实际上是效率低下的。

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

  1. 异步非阻塞形式

  效率更高,

  因为打电话是你(等待者)的事情,而通知你则是柜台(消息触发机制)的事情,程序没有在两种不同的操作中来回切换

  比如说,这个人突然发觉自己烟瘾犯了,需要出去抽根烟,于是他告诉大堂经理说,排到我这个号码的时候麻烦到外面通知我一下,那么他就没有被阻塞在这个等待的操作上面,自然这个就是异步+非阻塞的方式了。

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

1.6 进程的创建与结束

  1.6.1 进程的创建

  但凡是硬件,都需要有操作系统去管理,只要有操作系统,就有进程的概念,就需要有创建进程的方式,一些操作系统只为一个应用程序设计,比如微波炉中的控制器,一旦启动微波炉,所有的进程都已经存在。

  而对于通用系统(跑很多应用程序),需要有系统运行过程中创建或撤销进程的能力,主要分为4中形式创建新的进程:

  1. 系统初始化(查看进程linux中用ps命令,windows中用任务管理器,前台进程负责与用户交互,后台运行的进程与用户无关,运行在后台并且只在需要时才唤醒的进程,称为守护进程,如电子邮件、web页面、新闻、打印)

  2. 一个进程在运行过程中开启了子进程(如nginx开启多进程,os.fork,subprocess.Popen等)

  3. 用户的交互式请求,而创建一个新进程(如用户双击暴风影音)

  4. 一个批处理作业的初始化(只在大型机的批处理系统中应用)

  无论哪一种,新进程的创建都是由一个已经存在的进程执行了一个用于创建进程的系统调用而创建的。

  1.6.2 进程的结束

       1. 正常退出(自愿,如用户点击交互式页面的叉号,或程序执行完毕调用发起系统调用正常退出,在linux中用exit,在windows中用ExitProcess)

  2. 出错退出(自愿,python a.py中a.py不存在)

  3. 严重错误(非自愿,执行非法指令,如引用不存在的内存,1/0等,可以捕捉异常,try...except...)

  4. 被其他进程杀死(非自愿,如kill -9)

2. python中的进程操作

  2.1 进程创建的俩种方式

进程的实例化

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

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

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

方式一:调用直接模块实现

import time
from multiprocessing import Process

def funk(name):
    print('%s is running!'%name)
    time.sleep(2)
    print('%s is done!'%name)


if __name__ == '__main__':
    p1 = Process(target= funk,args=('子进程1',))
    p1.start()
    print('')

方式二:自建类,

import time
from multiprocessing import Process

class MyProcess(Process):
    def __init__(self,name):
        super().__init__()
        self.name = name

    def run(self):#这里必须是RUN函数,否则无法调用
        print('%s is running!'%self.name)
        time.sleep(2)
        print('%s is done!'%self.name)

if __name__ == '__main__':
    p1 = MyProcess('子进程1')

    p1.start()
    print('主进程!')

  2.2 进程id的查询

第一种方式

from multiprocessing import Process

def func1(name):
    print('%s is running'%name)
    print('%s is done'%name)

if __name__ =='__main__':
    p1 = Process(target= func1,args=('子进程1',))
    p1.start()
    print(p1.pid)
    print('')

第二种方式

from multiprocessing import Process
import os

def func2(name):
    print('%s is running'%name)
    print('子进程为%s,父进程为%s'%(os.getpid(),os.getppid()))

if __name__ == '__main__':
    p2 = Process(target= func2,args=('子进程1',))
    p2.start()
    print('')
    print('主进程为',os.getpid())

注意:补充内容,暂时没想到调整放在哪里

1.几个概念:
    父进程:父进程执行的过程中创建了子进程
    子进程:由父进程创建
    主进程:一般我们直接执行的那个程序就是主进程,在pycharm中主进程的父进程就是pycharm的进程pid

2.windows为什么要有 if __name__ == '__main__':
    windows在开子进程的时候,会从父进程中引入文件,并执行一遍,会变成嵌套
    linux macos中只是复制一份内存空间,并没有执行

3.如何 开启多个子进程

4.如何给子进程传参

from multiprocessing import Process
import os

def func(name):
    print('%s is start,id is %s,father id is %s'%(name,os.getpid(),os.getppid()))

if __name__ == '__main__':
    for i in range(10):
        p = Process(target= func,args= ('子进程%s'%i,))#这个时候传参里面必须是集合
        p.start()#非阻塞,只要告诉系统开启就好,开启并不是真正的开启

5.子进程有没有返回值
不能有返回值
因为子进程函数中的函数返回值不能传递给父进程,因为内存隔离的问题

  2.3 孤儿进程与僵尸进程

僵尸进程

  • 有害
  • 父进程对子进程收尸,而子进程一直未关闭,父进程一直在等待

孤儿进程

  • 无害
  • 父进程挂了,子进程未挂,子进程会被__init__接管,一段时候后将子进程关闭

  2.4 进程对象的其他属性

p.join():主进程等待子进程结束,这个时候其他子进程也是可以运行,是并发操作的
主进程会默认等待子进程结束后才结束,父进程会回收子进程占用的资源,join是单阻塞,n个进程用join控制
from multiprocessing import Process

def func(n):
print('发送邮件%s'%n)

if __name__ == '__main__':
p_list =[]
for i in range(10):
p = Process(target= func,args=(i,))
p_list.append(p)
p.start()
for p in p_list:
p.join()
print('全部邮件已经发送成功!')

p.pid():查看进程的id号

p.is_alive():查看进程是否活着

p.terminate(): 进程回收,强制结束一个进程,也是非阻塞的

p.name :进程名,在开进程的时候也可以加name参数,对子进程重命名

  2.5 守护进程

主进程创建进程,有三个特点

  • 守护进程会在主进程代码结束后终止
  • 守护进程无法再开启子进程
  • 守护进程只守护主进程的代码结束,不守护子进程,如果真要守护子进程,则对子进程join

守护进程需要在进程开启前设置

#守护进程小测试,设定守护进程对主函数以及子进程都守护!
from multiprocessing import Process
import time

def eye():
    while True:
        print('SERVER,我很好!')
        time.sleep(1)

def func2():
    print('进程2要做的事情!')
    time.sleep(8)
    print('进程2已经结束!')

def main():
    print('做我主要的事情')
    time.sleep(5)
    print('done')

if __name__ == '__main__':
    p = Process(target= eye,)
    p2 = Process(target=func2)
    p.daemon = True
    p.start()
    p2.start()
    main()
    p2.join()

  2.6 进程的同步控制--互斥锁

将原本共享的内容进行加锁,保证这个资源一次性只有一个在运行。加锁后将原本并发的进程变为串行,父进程建立锁,子进程调用锁。

首先看未加锁的情况。

from multiprocessing import Process,Lock
import time

def func(name):
    print('1,%s is running'%name)
    time.sleep(1)
    print('2,%s is runing!'%name)
    time.sleep(1)
    print('3,%s is running'%name)


if __name__ =='__main__':
    for i in range(3):
        ret = Process(target= func,args=('%s子进程'%i,))
        ret.start()

加锁后

from multiprocessing import Process,Lock
import time

def func(name,mutex):
    mutex.acquire()#进程加锁
    print('1,%s is running'%name)
    time.sleep(1)
    print('2,%s is runing!'%name)
    time.sleep(1)
    print('3,%s is running'%name)
    mutex.release()#进程解锁


if __name__ =='__main__':
    mutex = Lock()
    for i in range(3):
        ret = Process(target= func,args=('%s子进程'%i,mutex))
        ret.start()

join与互斥锁的区别

  • join是主进程整体堵塞,
  • 互斥锁是对需要修改的地方做修改,保证数据的安全

模拟一次抢票的过程

import json
import time
from multiprocessing import Process,Lock

def show_ticket(name):
    with open('test.txt','r')as f1:
        file =json.load(f1)
    if file['number'] >=0:
        print('%s 查询余票数量,余票数量为%s'%(name,file['number']))
    else:
        print('%s 查询余票数量,余票为0')

def get_ticket(name):
    with open('test.txt','r')as f1:
        file = json.load(f1)
    time.sleep(0.2)
    if file['number'] >0:
        file['number'] -= 1
        print('%s 购票成功!'%name)
    else:
        print('余票不足,%s 购票失败'%name)
    time.sleep(0.2)
    with open('test.txt', 'w')as f2:
        json.dump(file, f2)

def func(name,lock):
    show_ticket(name)
    lock.acquire()
    get_ticket(name)
    lock.release()


if __name__ == '__main__':
    lock = Lock()
    p_list = []
    for i in range(10):
        p = Process(target=func,args= ('购票者%s'%i,lock))
        p.start()
  • 多个进程抢占同一个数据资源会造成数据不安全
  • 我们必须牺牲效率来保证数据的安全性

  2.7 进程间的通行--队列与管道

   2.7.1 队列就是基于(管道+锁)实现的,队列中内存通信

q =Queue():实例化一个管道

q.full():判断队列中是否满,队列是先进先进的体现

q.empty():清空队列

q.put():添加到队列中

q.get():从队列中获得一个对象

队列维护了一个秩序,先进先出(FIFO)

进程之间的通信

IPC:Iter Process Communication

Pipe:管道,没有锁是进程之间数据不安全的机制,而管道+锁==队列,

队列:是一个黑箱模块,而队列就是进程之间数据安全机制

第三方工具(消息中间件):memcache、redis、kafka、rabbitm

   2.7.2 队列的模型---生产者消费者模型

   2.7.2.1 为什么要使用生产者消费者模型

生产者指的是生产数据的任务,消费者指的是处理数据的任务,在并发编程中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。

   2.7.2.2 什么是生产者消费者模型

生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

这个阻塞队列就是用来给生产者和消费者解耦的

生产者:是进程

消费者:是进程

生产者和消费者之间,传递数据,是需要一个盘子(IPC)

以爬虫为例,使用生产者消费者模型是为了平衡供需之间的关系

TIM图片20190210095950

以吃包子为例,看以下代码

from multiprocessing import Queue,Process
import time

def consumer(name,q):
    while True:
        msg = q.get()#get 会阻塞,直到队列中有一个数据
        if not msg :break
        time.sleep(0.7)
        print('%s 抢着吃了%s'%(name,msg))


def producer(name,produce,q):
    for i in range(20):
        msg = '%s %s'%(produce,i)
        q.put(msg)#由于整个队列大小只有5,在最大数量时,阻塞
        print('%s 生产了%s'%(name,msg))
        time.sleep(0.2)


if __name__ == '__main__':
    q = Queue(5)
    p1 = Process(target=consumer,args=('饭桶1',q))
    p2 = Process(target=consumer,args=('饭桶2',q))
    p3 = Process(target=producer,args=('生产者','包子',q))
    p3.start()
    p1.start()
    p2.start()
    p3.join()#确认所有的生产者已经全部生产全部数据
    q.put(None)#发送最终标志位,确认消费者收到明确的标志
    q.put(None)

在队列中还有一个JoinaLequeue的使用

这就像是一个Queue对象,但队列允许项目的使用者通知生成者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。

一些常用方法

q =JoinableQueue()#实例化一个对象

q.join()#生产者调用此方法进行阻塞,直到队列中所有的项目均被处理。阻塞将持续到队列中的每个项目均调用q.task_done()方法为止

q.task_done() #使用者使用此方法发出信号,表示q.get()的返回项目已经被处理。如果调用此方法的次数大于从队列中删除项目的数量,将引发ValueError异常

针对这一个可以可以对上面的吃包子进行一个多生产者进行改进

#2.0版本,使用JoinableQueue()
from multiprocessing import JoinableQueue,Process
import time

def consumer(name,q):
    while True:
        msg = q.get()#get 会阻塞,直到队列中有一个数据
        if not msg :break
        time.sleep(0.7)
        print('%s 抢着吃了%s'%(name,msg))
        q.task_done()#消费者告诉生产者他已经吃了一个


def producer(name,produce,q):
    for i in range(20):
        msg = '%s %s'%(produce,i)
        q.put(msg)#由于整个队列大小只有5,在最大数量时,阻塞
        print('%s 生产了%s'%(name,msg))
        time.sleep(0.2)
    q.join()#确保生产者生产的东西全部被吃完


if __name__ == '__main__':
    q = JoinableQueue(5)
    c1 = Process(target=consumer,args=('饭桶1',q))
    c1.daemon = True#生产者生产完之后,消费者也没有存在的必要
    c2 = Process(target=consumer,args=('饭桶2',q))
    c2.daemon = True#生产者生产完之后,消费者也没有存在的必要
    p1 = Process(target=producer,args=('生产者','包子',q))
    p2 = Process(target=producer,args=('生产者','叉烧包',q))
    p_list =[c1,c2,p1,p2]
    for p in p_list:
        p.start()
    p1.join()#确认所有的生产者已经全部生产全部数据
    p2.join()#确认所有的生产者已经全部生产全部数据

  2.8 进程间的数据共享(了解)

进程间应该尽量避免通信,即便需要通信,也应该选择进程安全的工具来避免加锁带来的问题。

以后我们会尝试使用数据库来解决现在进程之间的数据共享问题。

进程间数据是独立的,可以借助于队列或管道实现通信,二者都是基于消息传递的虽然进程间数据独立,但可以通过Manager实现数据共享,事实上Manager的功能远不止于此

下面代码示例图

TIM图片20190210113833

#目的:开启100个进程,分别对共享数据减一操作
#方式一(失败):
from multiprocessing import Manager,Process

def work(dict):
    dict['count']-=1

if __name__ == '__main__':
    m = Manager()
    dic = m.dict({'count':100})
    p_list =[]
    for i in range(100):
        p = Process(target=work,args=(dic,))
        p_list.append(p)
        p.start()
    for p in p_list:
        p.join()

    print(dic['count'])
#结果会发现数据不是我们预期的0,pc的内核越多,结果会越大


#方式二
from multiprocessing import Manager,Process,Lock
#Manager会提供很多数据共享的机制,但是对于一些基础数据类型,他是数据不安全的
#需要我们自己提供加锁操作

def work(dict,lock):
    # lock.acquire()#加锁操作
    # dict['count']-=1
    # lock.release()#解锁操作
    with lock:#也可以用这个,上下文管理机制
        dict['count']-=1#上文就是lock.acquire(),下文就是lock.release()

if __name__ == '__main__':
    lock = Lock()
    m = Manager()
    dic = m.dict({'count':100})
    p_list =[]
    for i in range(100):
        p = Process(target=work,args=(dic,lock))
        p_list.append(p)
        p.start()
    for p in p_list:
        p.join()

    print(dic['count'])

虽说有manager这个管理机制,但是我们日常中还是不会使用这个,一般是使用第三方工具库,来操作数据共享,毕竟不加锁的数据共享是数据不安全的。

最后的结论就是日常工作中就不要使用manager这个机制,纯粹就是给自己找麻烦。

注意:在多进程中或者是多线程中,数据共享,如果没有加锁,+=,-=,*=,/=都是数据不安全的,必须加锁!

  2.9 进程池的概念以及使用

   2.9.1 进程池

进程池的概念:

  在程序实际处理问题过程中,忙时会有成千上万的任务需要被执行,闲时可能只有零星任务。那么在成千上万个任务需要被执行的时候,我们就需要去创建成千上万个进程么?首先,创建进程需要消耗时间,销毁进程也需要消耗时间。第二即便开启了成千上万的进程,操作系统也不能让他们同时执行,这样反而会影响程序的效率。因此我们不能无限制的根据任务开启或者结束进程。那么我们要怎么做呢?

  在这里,要给大家介绍一个进程池的概念,定义一个池子,在里面放上固定数量的进程,有需求来了,就拿一个池中的进程来处理任务,等到处理完毕,进程并不关闭,而是将进程再放回进程池中继续等待任务。如果有很多任务需要执行,池中的进程数量不够,任务就要等待之前的进程执行任务完毕归来,拿到空闲进程才能继续执行。也就是说,池中进程的数量是固定的,那么同一时间最多有固定数量的进程在运行。这样不会增加操作系统的调度难度,还节省了开闭进程的时间,也一定程度上能够实现并发效果。

multiprocess.Pool模块

#1.实例化进程池的方法
Pool([numprocess  [,initializer [, initargs]]]):创建进程池

#2.参数介绍
1 numprocess:要创建的进程数,如果省略,将默认使用cpu_count()的值
2 initializer:是每个工作进程启动时要执行的可调用对象,默认为None
3 initargs:是要传给initializer的参数组

#3.主要方法
1 p.apply(func [, args [, kwargs]]):在一个池工作进程中执行func(*args,**kwargs),然后返回结果。
2 '''需要强调的是:此操作并不会在所有池工作进程中并执行func函数。如果要通过不同参数并发地执行func函数,必须从不同线程调用p.apply()函数或者使用p.apply_async()'''
3 
4 p.apply_async(func [, args [, kwargs]]):在一个池工作进程中执行func(*args,**kwargs),然后返回结果。
5 '''此方法的结果是AsyncResult类的实例,callback是可调用对象,接收输入参数。当func的结果变为可用时,将理解传递给callback。callback禁止执行任何阻塞操作,否则将接收其他异步操作中的结果。'''
6    
7 p.close():关闭进程池,防止进一步操作。如果所有操作持续挂起,它们将在工作进程终止前完成
8 
9 P.jion():等待所有工作进程退出。此方法只能在close()或teminate()之后调用

注意:开启进程池的时候,如果在开启时没有填写数字,默认都是cpu的核心数量!

比较常用的就是p.map(funcation,iterable)、p.apply_async(funcation,args=()),不同的是p.apply_async需要用close和join来保证进程池的结束!

实际开启一个进程池,并且看一下每个进程的id

from multiprocessing import Pool
import os
import time

def func(i):
    print(i,os.getpid())
    time.sleep(0.2)

if __name__ == '__main__':
    p =Pool(4)#设定池子最大进程数,一般为cpu+1/cpu数量
    for i in range(20):
        p.apply_async(func,args=(i,))#异步的提交任务
    p.close()#关闭池子,不是阻止进程池中已经有的进程,而是阻止再向池中异步的提交任务
    p.join()#阻塞池子

   2.9.2 进程池以及多进程的性能测试

  以上面的例子在加上开启多个进程我们可以明显看出多进程与进程池之间有明显的效率

#进程池以及多进程的效率测试
from multiprocessing import Pool,Process
import os
import time

def func(i):
    print(i,os.getpid())


if __name__ == '__main__':
    start_time = time.time()
    p_list =[]
    for i in range(100):
        p = Process(target= func,args=(i,))
        p_list .append(p)
        p.start()
    for p in p_list:
        p.join()
    end_time = time.time()
    process_time = end_time-start_time

    start_time = time.time()
    p =Pool(4)#设定池子最大进程数,一般为cpu+1/cpu数量
    for i in range(20):
        p.apply_async(func,args=(i,))#异步的提交任务
    p.close()#关闭池子,不是阻止进程池中已经有的进程,而是阻止再向池中异步的提交任务
    p.join()#阻塞池子
    end_time = time.time()
    pool_time = end_time-start_time

    print(process_time,pool_time)

#结果

7.379263877868652 0.4368298053741455,我们可以发现效率是有明显的差别

如果我们有多个任务,就开启多少个进程,实际上对于我们来说,是非常不划算的,由于计算机的cpu数量时有限的,所以我们起的进程数量是完全和cpu的个数成比例的。

起多进程的意义

  • 为了更好的利用cpu,所以如果我们的程序中都是网络i/o以及文件i/o,就不适合起多进程
  • 为了数据隔离,如果我们的程序中总是要用到数据共享,那么就不适用使用多进程
  • 超过了cpu个数的任务数,都应该用进程池来解决问题,不能无限的开启子进程

    2.9.3 进程池的其他机制

对于开启进程池还有其他方法,方法如下

#直接用map函数,俩步搞定,哈哈哈
from multiprocessing import Pool
import os
import time

def func(i):
    print(i,os.getpid())
    time.sleep(0.2)

if __name__ == '__main__':
    p =Pool(4)#设定池子最大进程数,一般为cpu+1/cpu数量
    p.map(func,range(20))
    # for i in range(20):
    #     p.apply_async(func,args=(i,))#异步的提交任务
    # p.close()#关闭池子,不是阻止进程池中已经有的进程,而是阻止再向池中异步的提交任务
    # p.join()#阻塞池子

2.9.4 进程池的返回值

2.9.4.1 普通函数实现进程的返回值

#普通函数方法
from multiprocessing import Pool

def func(i):
    print('爬取的网页内容%s'%i)
    return len('网页大小!')*i

if __name__ == '__main__':
    p = Pool(5)
    ret_l =[]
    for i in range(10):
        ret =p.apply_async(func,args =(i,))
        ret_l.append(ret)
    for ret in ret_l:
        print(ret.get())
    p.close()
    p.join()

2.9.4.2 map函数实现进程的返回值

#map函数方法
from multiprocessing import Pool

def func(i):
    print('爬取网页的大小%s'%i)
    return len('网页大小!')*i

if __name__ == '__main__':
    p = Pool(5)
    ret =p.map(func,range(10))
    for i in ret:
        print(i)

2.9.4.3 回调函数

模拟一个爬虫爬网页的情况,

正常获取网页,一个一个获取后进行统计

#正常版
from multiprocessing import Pool
import time
import random


def func(i):
    print('爬取的网页的内容%s'%i)
    time.sleep(random.random())
    return len('网页大小!')*i

if __name__ == '__main__':
    p = Pool(5)
    ret_l = []
    for i in range(10):
        ret =p.apply_async(func,args =(i,))
        ret_l.append(ret)
    for ret in ret_l:
        print(ret.get())
    p.close()
    p.join()

第二个版本

#采用回调函数的版本
from multiprocessing import Pool
import time
import random


def func(i):#子进程调用
    time.sleep(random.random())
    print('爬取的网页的内容%s'%i)
    return len('网页大小!')*i

def call_back(contact):#主进程调用
    print(contact)

if __name__ == '__main__':
    p = Pool(5)
    for i in range(20):
        ret =p.apply_async(func,args =(i,),callback=call_back)
    p.close()
    p.join()
#结果
爬取的网页的内容2
10
爬取的网页的内容3
15
爬取的网页的内容1
5
爬取的网页的内容0
0
爬取的网页的内容6
30
爬取的网页的内容4
20
爬取的网页的内容5
25
爬取的网页的内容7
35
爬取的网页的内容9
45
爬取的网页的内容8
40

将n个任务交给n个进程去执行

每一个进程在执行完毕后会返回一个值,这个返回值会直接交给callback参数指定的那个函数去进行处理,这样的话,所有的进程,哪一个执行的最快,哪一个就可以最先进行统计或者其他工作,可以在最短时间内获取到结果!

posted @ 2019-02-11 16:52  柴犬砍柴  阅读(186)  评论(0编辑  收藏  举报