并发编程之进程进阶

一、进程之间的通信

  进程之间的通信称为IPC

  多个进程之间有一些固定的通信内容,socket基于文件家族通信

  进程之间虽然内存不共享,但是是可以通信的

  Lock Semaphore Event 都是进行进程之间的通信的,但是通信的内容不能改变

  进程之间的通信可以通过进程队列和管道来控制

 

二、进程队列

队列的特点:先进先出

使用到的类 from multiprocessing import Queue

方法:put()      往队列里添加值(队列满了,会进入阻塞状态)

      get()      往队列里取值(队列为空,会进入阻塞状态)

      get_nowait()    往队列里取值(非阻塞事件,队列为空时会报queue.Empty异常)—— 用异常处理

      put_nowait()    往队列里添加值 (非阻塞事件,队列已满会报queue.Full异常,而且数据会丢失)

      empty()       判断队列是否为空(多线程下不准确)

    full()      判断队列是否满了(多线程下不准确)

 

使用put_nowait(),当队列存满时,queue.Full异常

from multiprocessing import Queue

q = Queue(2)
q.put_nowait(1)
q.put_nowait(2)
q.put_nowait(3)

'''
报错:
queue.Full
'''

# 解决方案
from multiprocessing import Queue
import queue
q = Queue(2)            # 限制队列的数量
try:
    q.put_nowait(1)
    q.put_nowait(2)
    q.put_nowait(3)
except queue.Full:
    print('Full')

'''
结果
Full
'''

 

使用put_nowait()取值,当队列为空时,抛出queue.Empty异常

from multiprocessing import Queue

q = Queue()
print(q.get_nowait())   # 使用put_nowait()取值,当队列为空时,抛出queue.Empty异常

'''
报错:
    raise Empty
queue.Empty
'''

from multiprocessing import Queue
import queue

q = Queue()
try:
    print(q.get_nowait())  # 处理异常
except queue.Empty:
    print('Empty')
    
'''
结果:
Empty
'''

 

主进程和子进程的通信

一般情况下,一个进程负责put,另一个进程负责get

① 主进程put,子进程get

② 主进程get,子进程put

③ 主进程和子进程同时put和get,需要通过join方法,不然容易阻塞

from multiprocessing import Queue
from multiprocessing import Process

def getvalue(q):
    while 1:
        ret = q.get()           # 取值
        if ret is None:break    # 值为None,证明已经取完,结束循环
        print(ret)

if __name__ == '__main__':
    q = Queue()
    Process(target=getvalue,args=(q,)).start()

    for i in range(5):
        q.put(i)                # 添加值
    q.put(None)                 # 添加完再添加None,不是添加完了
    
'''
0
1
2
3
4
'''

 

三、生产者消费者模型(基于队列)

某些模块负责生产数据,这些数据由其他模块来负责处理(此处的模块可能是:函数、线程、进程等)。产生数据的模块称为生产者,而处理数据的模块称为消费者。在生产者与消费者之间的缓冲区称之为仓库。生产者负责往仓库运输商品,而消费者负责从仓库里取出商品,这就构成了生产者消费者模式

 

并发
由于生产者与消费者是两个独立的并发体,他们之间是用缓冲区通信的,生产者只需要往缓冲区里丢数据,就可以继续生产下一个数据,而消费者只需要从缓冲区拿数据即可,这样就不会因为彼此的处理速度而发生阻塞。

 

支持忙闲不均
当生产者制造数据快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中,慢慢处理掉。而不至于因为消费者的性能造成数据丢失或影响生产者生产

 

保持生产者和消费者的平衡,使用队列,若队列为空,则增加生产者数量或者减少消费者数量,若队列为满,减少生产者数量或者增加消费者数量

from multiprocessing import Queue
from multiprocessing import Process

def consumer(q,name):       # 定义消费者
    while 1:
        fruits = q.get()
        if fruits is None:break
        print('%s 购买了 %s'% (name,fruits))

def producer(q,name,fruits):
    for i in range(1,11):
        print('%s 采摘了 %s%s' % (name,fruits,i))
        q.put(fruits + str(i))

if __name__ == '__main__':
    q = Queue()
    c1 = Process(target=consumer,args=(q,'小明'))
    c1.start()

    p1 = Process(target=producer,args=(q,'果农','西瓜'))
    p1.start()
    p1.join()                       # join阻塞当前进程,待生产者执行完毕才执行下面的代码
    q.put(None)

 

四、生产者消费者模型(基于JoinableQueue类)

from multiprocessing import JoinableQueue

方法:

put()          # 往队列添加值

get()          # 往队列取值

join()          # 队列阻塞直到计数为0,标记队列的数据处理完才结束阻塞

task_done()         # 通知队列的一个数据处理完,触发计算-1

join()和task_done() 配合使用,而且消费者要设置为守护进程

 

方法一:

from multiprocessing import JoinableQueue
from multiprocessing import Process

def consumer(q,name):       # 定义消费者
    while 1:
        fruits = q.get()
        if fruits is None:break
        print('%s 购买了 %s'% (name,fruits))

def producer(q,name,fruits):    # 定义生产者
    for i in range(1,6):
        print('%s 采摘了 %s%s' % (name,fruits,i))
        q.put(fruits + str(i))

if __name__ == '__main__':
    q = JoinableQueue()
    c1 = Process(target=consumer,args=(q,'小白'))
    c1.start()

    p1 = Process(target=producer,args=(q,'果农','苹果'))
    p1.start()
    p1.join()
    q.put(None)

 

方法二:

from multiprocessing import JoinableQueue
from multiprocessing import Process

def consumer(q,name):       # 定义消费者
    while 1:
        fruits = q.get()
        print('%s 购买了 %s'% (name,fruits))
        q.task_done()
def producer(q,name,fruits):    # 定义生产者
    for i in range(1,4):
        print('%s 采摘了 %s%s' % (name,fruits,i))
        q.put(fruits + str(i))

if __name__ == '__main__':
    q = JoinableQueue()
    c1 = Process(target=consumer,args=(q,'小白'))
    c2 = Process(target=consumer,args=(q,'小黑'))
    c1.daemon = True        # 主进程结束全部结束
    c2.daemon = True        # 主进程结束全部结束
    c1.start()
    c2.start()

    p1 = Process(target=producer,args=(q,'果农','苹果'))
    p2 = Process(target=producer,args=(q,'农民','西瓜'))
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    q.join()            # task_done()为0时结束进程

 

五、管道

队列是基于管道实现的,管道是基于socket实现的

队列中进程之间的数据安全,因为队列不会同时取同一个数据,也不会同时放一个数据,队列就是管道+锁实现的。简便的IPC机制,使得进程之间数据安全

管道中进程之间数据不安全,存储数据复杂,基于socket + pickle实现的

 

Pipe的对象是一个元组,元组里有两个元素

from multiprocessing import Pipe
p = Pipe()
print(p)

# (<multiprocessing.connection.PipeConnection object at 0x000001C6C19B8A58>, <multiprocessing.connection.PipeConnection object at 0x000001C6C19B89B0>)

 

如果不使用的端口就要手动关闭,left.close() 或者 right.close()

Pipe的端口管理不会随着某个进程的关闭就关闭。

操作系统来管理进程对这些端口的使用,每关闭一个端口,计数器-1,直到所有的端口都关闭只剩下一个端口,recv就会抛出异常,需要用异常处理。

只要关闭所有send的端口,recv会认为没有信息给它发送,recv就会抛出异常。

端口是此操作系统管理的,要关闭不使用的端口,触发recv报错,即使通过发送信号停止recv的循环,也需要手动关闭端口,归还资源给操作系统

第一种方式:

from multiprocessing import Pipe
def func(left,right):
    left.close()
    while True:
        try:
            print(right.recv())     # 只剩下一个端口时,recv会抛出异常EOFError
        except EOFError:
            break

if __name__ == '__main__':
    left,right = Pipe()
    Process(target=func,args=(left,right)).start()
    right.close()
    for i in range(5):
        left.send(i)
    left.close()

'''
0
1
2
3
4
'''

 

第二种方式:

from multiprocessing import Pipe,Process

def func(left,right):
    left.close()
    while 1:
        ret = right.recv()
        if ret is None:
            right.close()
            break
        print(ret)

if __name__ == '__main__':
    left,right = Pipe()
    Process(target=func,args=(left,right)).start()
    right.close()
    for i in range(3):
        left.send(i)
    left.send(None)         # 给子进程发送讯号结束循环
    left.close()

 

六、进程池

CPU的核心数是有限的,开启过多的进程不能提高效率,反而降低了效率

进程的启动、调度、关闭都通过操作系统分配,需要一定的时间,如果任务超过5个,使用进程池,而不是开启过多的进程

 

程序的分类:

  计算密集型:充分占用CPU,多进程可以充分利用多核,适合开启多进程,但是不适合开启过多的进程

  OS密集型:大部分时间都在阻塞队列,而不是在运行状态中,不适合开启多进程

 

开启进程池的步骤:

from multiprocessing import pool

p = pool()              可以传数量,不传默认是CPU的核心数

p.apply_async(函数,参数)    异步提交函数到一个子进程中执行

p.close()             关闭进程池,用户不能再向这个池中提交任务

p.join()             阻塞,必须先close()才能执行,join()直到线程池的所有任务结束才结束阻塞

p.apply()             同步提交任务到一个子进程中执行

不能传队列、文件句柄作为子进程的参数,可以传管道 

 

提交任务

① 同步提交 apply

子进程对应函数的返回值,一个一个顺序执行的,并没有任何的并发效果

② 异步提交 apply_async

没有返回值的情况下,要想所有任务能够顺利的执行完毕,必须使用close()和join()的方法

有返回值的i情况下,通过get()方法取值,get不能再提交任务之后立即执行,应该是先提交所有的任务再通过get获取结果

 

map() 方法

  异步提交的简化版本,自带close和join方法,直接拿到返回值的可迭代对象,循环获取返回值

 

一般情况下,多进程少拿到返回值,在回调函数用的多。

使用第三方的工具使进程池在多个进程之间通信,保证了数据安全、有良好的分发数据、redis是能够共享的大字典、kafaka、memocache等

 

多进程和进程池

多进程:

①操作系统来统一调度

②创建的进程越多,占用的操作系统资源越多,操作系统的效率就会不断下降

 

进程池:

进程中的进程是有限的,如果用户量太大,进程池的限制会导致用户的响应及时性受影响

 

apply   

同步提交任务,所有的子进程执行完毕才执行主进程

from multiprocessing import Pool
import time,os

def func(args):
    time.sleep(0.5)
    print('子进程号:%s,%s'%(os.getpid(),args))


if __name__ == '__main__':
    p = Pool(os.cpu_count()+1)          # 线程数为cpu的核心数+1
    for i in range(10):
        p.apply(func,(i,))              # 同步提交任务
    print('主进程%s'% os.getpid())       # 子进程执行完毕才执行主进程

 

同步提交任务,获取返回值

from multiprocessing import Pool
import os

def func(args):
    return args ** 2                    # 子进程返回结果


if __name__ == '__main__':
    p = Pool(os.cpu_count()+1)          # 线程数为cpu的核心数+1
    for i in range(10):
        ret = p.apply(func,(i,))        # 同步提交任务,获得返回值
        print('result:%s'% ret)         # 打印返回值
    print('主进程%s'% os.getpid())

 

apply_async

异步提交任务,使用close()和join()的方法阻塞主程序,否则主程序结束后会关闭进程池。

from multiprocessing import Pool
import os,time

def func(args):
    time.sleep(2)
    print('子进程号:%s,%s'% (os.getpid(),args))

if __name__ == '__main__':
    p = Pool(4)
    for i in range(10):
        p.apply_async(func,(i,))
    p.close()
    p.join()
    print(666)

 

获取返回值,前提是需要提交所有任务,并且将所有返回值存储到容器中,循环容器通过get()获取值

from multiprocessing import Pool
def func(args):
    return args**2

if __name__ == '__main__':
    p = Pool(2)
    ret_li = []                             # 储存返回值的列表
    for i in range(1,11):
        ret = p.apply_async(func,(i,))
        ret_li.append(ret)                  # 将结果添加至列表
    for ret in ret_li:                      # 所有任务提交后在循环

 

map() 

异步提交任务,自带close和join方法,参数:子进程函数,iterable

from multiprocessing import Pool
import time

def func(args):
    time.sleep(2)
    print(args)

if __name__ == '__main__':
    p = Pool()
    p.map(func,range(20))           # map的参数:子进程函数,iterable

 

获取返回值,返回一返回值的列表,通过循环返回值列表获取值

from multiprocessing import Pool

def func(args):
    return args ** 2

if __name__ == '__main__':
    p = Pool()
    ret = p.map(func,range(20))           # 获取一个返回值的列表
    for r in ret:                         # 循环返回值的列表即可获取值
        print(r)

 

七、数据共享

from multiprocessing import Manager

将所有实现了数据共享的比较便捷的类重新又封装了一边,并且在原有的multiprocessing增加了新的机制

count += 1 不是原子性操作,实际经历了三步:①读入count变量指向的值,② +1,③让count变量指向新的结果值

当多个线程运行时,很可能某些线程执行第一步时读入的是同一个值,这样再加1和写入就相当于重复劳动了。

 

with... 上下文管理,不只用于文件处理,在执行一大段语句之前自动做某个操作,并且执行一大段语句之后,自动做某个操作

例如 with lock 包含了lock.acquire() 和 lock.release()

 

以Manager为例,由于多进程下数据不安全,需要通过加锁来保证数据的安全

from multiprocessing import Manager,Lock,Process
import time
def func(d,lock):
    with lock:                  # 自动加锁
        d['count'] -= 1

if __name__ == '__main__':
    lock = Lock()
    with Manager() as m:                # 相当于 m = Manager()
        dic = m.dict({'count':300})     # 使用类封装的数据结构
        p_li = []
        for i in range(300):
            p = Process(target=func,args=(dic,lock))
            p.start()
            time.sleep(0.1)
            p_li.append(p)              # 将进程对象添加至列表
        for p in p_li:          
            p.join()                    # 阻塞,保证全部子进程完成
        print(dic)

 

数据共享的机制:

① 支持数据类型非常有限

② list dict等都不是数据安全的,需要加锁来保证数据安全

 

八、回调函数

子进程执行完的返回值调至主进程执行,称为回调函数

回调函数一般基于数据池的apply_async 异步提交任务 进行的

from multiprocessing import Pool
import os

def func(args):
    print('子进程:%s' % os.getpid())
    return args ** 3

def call_back(ret):
    print('回调函数:%s' % os.getpid())
    print(ret)

if __name__ == '__main__':
    p = Pool()
    print('主进程:%s' % os.getpid())
    p.apply_async(func,args=(1,),callback=call_back)        # 设置回调函数,将子进程的结果返回给回调函数
    p.close()
    p.join()
    
'''
主进程:1360
子进程:6928
回调函数:1360
1
'''

func执行完毕之后执行callback函数,func的返回值会作为callback的的参数传入,回调函数通过主进程实现

 

使用场景:

①只用主进程统一获取结果,这样的话在主进程之间没有数据隔离

②多个子进程之间所做的事情是比较长时间的,而在主进程中一般情况下回调函数的执行速度都比较快

例如:子进程中做网络访问、数据分析;主进程拿着访问结果或者数据分析的结果来做一些汇总的工作

 

回调函数和同故宫ret.get()的区别

① 代码的简洁度不同

② 效率问题,子进程执行完立即调用callback

posted @ 2018-09-20 15:57  st--st  阅读(181)  评论(0编辑  收藏  举报