2020.10.6 分布式进程

学习总结自网站:

https://blog.csdn.net/u011318077/article/details/88094583

https://www.liaoxuefeng.com/wiki/1016959663602400/1017631559645600#0

 

1、模块

multiprocessing.managers、queue

使用multiprocessing.managers中的BaseManager类,创建分布式管理器

使用queue.Queue()创建队列,用于多个进程间的通信

 

#master和worker进程中都要有一个QueueManager作为分布式管理器
class QueueManager(BaseManager):
    pass
#Queue只在master进程中创建,并发布到网络上
#worker进程接收网络中的Queue
#两个Queue,一个用来发布任务的task_queue,一个用来接收结果的result_queue
task_queue=queue.Queue()
result_queue=queue.Queue()

 

2、原理

managers子模块支持把多个进程分布到多台机器上

②写一个服务进程作为调度者,将任务分布到其他多个进程中,然后通过网络通信进行管理

比如:爬取图片

一个进程负责抓取地址,并将地址放在Queue(容器)队列中;

另一个进程负责从Queue队列中取出链接地址进行图片下载和存储到本地。

上述过程就可以做成分布式,一台机器负责获取地址,另一台机器负责下载存储。

 

问题的核心:将Queue队列暴露在网络中,使其他机器可以访问

 

3、实现步骤

①建立Queue队列,负责进程间的通信任务队列task_queue结果队列result_queue

 

task_queue=queue.Queue()
result_queue=queue.Queue()

 

 

 

②把第一步中的两个队列在网络中注册,注册时将队列重新命名

复制代码
def return_task_queue():
    global task_queue
    return task_queue

def return_result_queue():
    global result_queue
    return result_queue

QueueManager.register('get_task_queue',callable=return_task_queue)
QueueManager.register('get_result_queue',callable=return_result_queue)
复制代码

注意:

1、Windows下的callable参数不支持lambda匿名函数(否则会报错EOFError: Ran out of input),必须是自写函数的函数名 

 2、register的第一个参数标定了发布到网上的Queue名字,在workermaster中必须一致;第二个参数callable关联的函数必须在master已经实现,且该函数必须返回一个队列queue,这个队列就是我们发布到网上的队列

 

③创建一个Queuemanager(BaseManager)的实例manager,相当于一个服务器,给定IP端口验证码

 其中验证码authkey必须写成b'abc'的形式,不能写成'abc'

manager=QueueManager(address=('127.0.0.1',8001),authkey=b'abc')

 

 

 

④启动实例manager

manager.start()

 

访问Queue对象,即创建网络中暴露重命名后的Queue实例

task=manager.get_task_queue()
result=manager.get_result_queue()

注意,不能直接访问之前我们在①中声明的Queue ,必须通过manager.get_task_queue()获得的Queue接口添加!

 

创建任务本地队列中,自动上传任务网络队列中,分配给任务进程进行处理

 

#放入的任务数为循环的次数,通过task.put(n)放入
for i in range(10):
    n=random.randint(0,10)
    print('Put task %s ...'%n)
    task.put(n)  #将n放入task队列

 

以上从①到⑥的代码都在服务进程master中,⑦在任务进程worker

 

任务进程网络中的任务队列中取出任务,然后执行,将执行结果放入网络中的结果队列

复制代码
class QueueManager(BaseManager):
    pass

#A QueueManager.register(
'get_task_queue') QueueManager.register('get_result_queue')
#B m
=QueueManager(address=('127.0.0.1',8001),authkey=b'abc')
#C m.connect()
#D task
=m.get_task_queue() result=m.get_result_queue() #任务进程 #从task队列获取任务,并把结果放入result队列 for i in range(10): try: #获取任务 n=task.get(timeout=1)#每次等待1s后取出任务 #接下来几行是具体的任务执行 print('run task %d *%d ...'%(n,n)) r='%d * %d =%d...'%(n,n,n*n) time.sleep(1) #放入result队列 result.put(r) except queue.Empty: print('task queue is empty')
复制代码

有四处需要注意的地方,分别用A,B,C,D标注在了上文代码中

A,任务进程worker中,调用register将Queue暴露在网络上时,不需要写callable参数,但第一个参数(即发布的queue的名字)需要与master中的一致

B,worker中绑定端口,验证码的设置与服务进程master中的一致

C,worker中连接网络,用m.connect()方法;master中启动Queue,用m.start()方法

D,task与result的获取,和master一样,都要通过m.get_task_queue()m.get_result_queue()

 

服务进程结果队列中取出结果,直到执行完所有任务取出所有的结果任务进程关闭,然后服务进程关闭( ⑧在服务进程master中 )

for i in range(11):
    try:
        r=result.get(timeout=5)#每次等待5s,取结果队列
    
        #对结果队列的处理
        print('Result:%s'%r)
    except queue.Empty:
        print('result queue is empty')

 

#关闭任务进程和服务进程
#在master中
manager.shutdown()

 

注意:

①先创建服务进程,再创建任务进程

②执行时,先启动服务进程,再创建任务进程,启动任务进程不要超过服务进程取出结果的等待时间

 

4、分布式进程实例

Q:创建一个分布式进程,用来完成10次乘法运算 

 

服务进程

复制代码
#服务进程在Windows和Linux系统上有所不同
#创建一个分布式进程:包括服务进程和任务进程
#多进程间通信用Queue
#该代码为服务进程
#注意:运行时先运行服务进程,再运行任务进程
#服务进程与任务进程都创建了相同的两个队列:
# 一个用来放任务task_queue
# 另一个用来放结果result_queue
#任务执行顺序:
'''
1、服务进程运行,比如将数字2放入任务队列,任务进程从任务队列中取出数字2
2、取出数字2后,执行任务,即2*2=4,任务结束后,放入结果队列
3、服务进程从结果队列中取出结果
4、所有任务执行完毕,所有结果都已经取出,最终任务队列和结果队列都是空的了
'''

#coding:utf-8
import queue,random
from multiprocessing.managers import BaseManager
from multiprocessing import freeze_support

#第一步 定义两个队列,一个用于发送任务,另一个用于接收结果
task_queue=queue.Queue()
result_queue=queue.Queue()

#创建类似的QueueManager,继承自BaseManager,用于后面创建管理器
class QueueManager(BaseManager):
    pass

#定义两个函数,返回结果就是Queue队列
def return_task_queue():
    global task_queue
    return task_queue
def return_result_queue():
    global result_queue
    return result_queue

#第二步,把上边创建的两个队列注册到网络上,用register方法
#callable参数关联了Queue对象,将Queue对象在网络中暴露
#第一个参数是注册在网络中的队列的名称
def test():
    QueueManager.register('get_task_queue',callable=return_task_queue)
    QueueManager.register('get_result_queue',callable=return_result_queue)
    #第三步 绑定端口8001,设置验证口令,这个相当于对象的初始化
    #绑定端口并填写验证口令,Windows下需填写IP地址,Linux下默认为本地,地址为空
    manager=QueueManager(address=('127.0.0.1',8001),authkey=b'abc') #口令必须写成类似b'abc'形式,只写'abc'运行错误

    #第四步 启动管理器,启动Queue队列,监听信息通道
    manager.start()

    #第五步 通过管理实例的方法访问网络中的Queue对象
    #即通过网络访问获取任务队列和结果队列,创建了两个Queue实例
    task=manager.get_task_queue()
    result=manager.get_result_queue()

    #第六步 添加任务,获取返回的结果
    #将任务放到Queue队列中
    for i in range(10):
        n=random.randint(0,10) #返回0~10间的随机数
        print('Put task %s ...'%n)
        task.put(n) #将n放入任务队列中

    #从结果队列中取出结果
    print('Try get results...')
    for i in range(11): #之所以循环11次,是用最后一次的结果来判断队列是否已经空了
        #总共循环10次,上面放入了10个数字作为任务
        #加载一个异常捕获
        try:
            r=result.get(timeout=5)#每次等待5s,取结果队列中的值
            print('Result:%s'%r)
        except queue.Empty:
            print('result queue is empty.')

    #最后一定要关闭服务,不然会报 管道未关闭 的错误
    manager.shutdown()
    print('master exit.')

if __name__=='__main__':
    #Windows下多进程可能出现问题,添加以下代码可缓解
    freeze_support()
    print('Start!')
    #运行服务进程
    test()
复制代码

任务进程

复制代码
#coding:utf-8
#定义具体的任务进程,具体的工作任务是什么

import time,sys,queue
from multiprocessing.managers import BaseManager

#创建类似的QueueManager,继承自BaseManager,用于后边创建管理器
class QueueManager(BaseManager):
    pass

#第一步:使用QueueManager注册用于获取Queue的方法名称
#前面服务进程已经将队列名称暴露于网络中
#该任务进程注册时只需要提供名称即可,与服务进程中的队列名称一致
QueueManager.register('get_task_queue')
QueueManager.register('get_result_queue')

#第二步:连接到服务器,也就是运行服务进程代码的机器
server_addr='127.0.0.1'
print ('Connect to server %s ...'%server_addr)
#创建一个管理器实例,端口和验证口令保持与服务进程中完全一致
m=QueueManager(address=(server_addr,8001),authkey=b'abc')
#连接到网络服务器
m.connect()

#第三步:从网络上获取Queue对象,并进行本地化,与服务进程是同一队列
task=m.get_task_queue()
result=m.get_result_queue()

#第四步:从task队列获取任务,并把结果写入result队列
for i in range(10):
    try:
        #前面服务进程向task队列放入了n,这里取出n
        #n和n相乘,并将相乘的算式和结果放入result队列中去
        n=task.get(timeout=1)
        print('run task %d * %d ...'%(n,n))
        r='%d * %d = %d'%(n,n,n*n)
        time.sleep(1)
        result.put(r)
    except queue.Empty:
        print('task queue is empty.')

#任务处理结束
print('Worker exit.')
复制代码

 

启动分布式进程

先运行①服务进程,再运行②任务进程(不能只单独运行其中一个,必须先①后②运行)

 

服务进程运行结果

复制代码
Start!
Put task 8 ...
Put task 6 ...
Put task 3 ...
Put task 4 ...
Put task 7 ...
Put task 6 ...
Put task 10 ...
Put task 6 ...
Put task 7 ...
Put task 10 ...
Try get results...
Result:8 * 8 = 64
Result:6 * 6 = 36
Result:3 * 3 = 9
Result:4 * 4 = 16
Result:7 * 7 = 49
Result:6 * 6 = 36
Result:10 * 10 = 100
Result:6 * 6 = 36
Result:7 * 7 = 49
Result:10 * 10 = 100
result queue is empty.
master exit.
复制代码

 

任务进程运行结果

复制代码
Connect to server 127.0.0.1 ...
run task 8 * 8 ...
run task 6 * 6 ...
run task 3 * 3 ...
run task 4 * 4 ...
run task 7 * 7 ...
run task 6 * 6 ...
run task 10 * 10 ...
run task 6 * 6 ...
run task 7 * 7 ...
run task 10 * 10 ...
Worker exit.
复制代码

 

补充:

1、当我们在一台机器上写多进程程序时,创建的Queue可以直接拿来用。但是在分布式多进程环境下,添加任务到Queue不可以直接对原始的task_queue进行操作,那样就绕开了QueueManager的封装,必须通过manager.get_task_queue()获得的Queue接口进行添加(见 实现步骤⑤)。然后在另一台机器上启动任务进程(本机启动也可以)。

2、task_queue与result_queue是两个队列,分别存放任务和结果。它们用来进行进程间的通信,交换对象。

由于是分布式的环境,放入Queue中的数据需要等待任务进程Workers机器运算处理后再进行读取,

QueueManager.register('get_task_queue',callable=return_task_queue)
QueueManager.register('get_result_queue',callable=return_result_queue)

这样就需要对queue用QueueManager进行封装放到网络中,这是通过以上两行代码实现的。我们给return_task_queue的网络调用接口取了一个名get_task_queue,而return_result_queue的名字是get_result_queue,方便区分对哪个queue进行操作,task.put(n)即是对task_queue进行写入数据,相当于分配任务。而result.get()则是等待Workers机器处理数据后返回的结果。

 

3、这个简单的Master/Worker模型有什么用?其实这就是一个简单但真正的分布式计算,把代码稍加改造,启动多个Worker,就可以把任务分布到几台甚至几十台机器上,比如把计算n*n的代码换成发送邮件,就实现了邮件队列的异步发送。

 

Queue的对象储存在哪?可以看到Worker文件中并没有创建Queue的代码,所以,Queue对象储存在Master文件中:

 

 而Queue之所以能够通过网络访问,就是通过QueueManager(继承自BaseManager)实现的。由于QueueManager管理的不止一个Queue,所以要给每个Queue的网络调用接口起一个名字,比如get_task_queue(即register函数中的第一个参数)。并且task_workertask_manager中QueueManager注册的名字必须一样。从上面的例子中可以看出,Queue对象从另一个进程通过网络传递了进来,只不过这里的传递和网络通信均由QueueManager完成。

authkey的作用:为了保证两台机器的正常通信,不被其他机器恶意干扰。只有worker和mastet的authkey一致,才能相互连接。

 

posted @   ShineLe  阅读(130)  评论(0编辑  收藏  举报
编辑推荐:
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
阅读排行:
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
点击右上角即可分享
微信分享提示