「Python网络编程」再识多任务的真面目/多进程(三)

博主前言:

上篇博客我讲述了多任务的一种实现方式—多线程。这篇博客我继续讲述使用进程的方式来实现多任务。由于进程的知识实属有点抽象,特别是结合线程来讲进程确实有点难度,所以在本篇博客开写之前,我借阅了多个论坛大佬写的有关于多进程的博客,所以这篇博客如有雷同,算我抄你的。但是我保证,百分之一大半都是自己理解所得。

1. 多进程

在「Python网络编程」系列第一篇博客讲端口号时,我们了解了有关进程的概念。进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动,是操作系统分配资源的基本单元,是操作系统结构的基础。
如果你觉得上段黑体字有点难以理解,那么你可以就把进程理解为运行着的程序也可。程序与进程的区别就是:程序是静态的,进程是动态的。
我在网上看到一个公式觉得非常精辟:进程 = 程序 + 资源

1.1 进程的状态

在这里插入图片描述
1)就绪状态(Ready):
进程已获得除处理器外的所需资源,等待分配处理器资源;只要分配了处理器进程就可执行。就绪进程可以按多个优先级来划分队列。例如,当一个进程由于时间片用完而进入就绪状态时,排入低优先级队列;当进程由I/O操作完成而进入就绪状态时,排入高优先级队列。
结合上面的公式可以理解为,就绪状态就是程序运行的资源已经准备好,等待CPU执行。
2)运行状态(Running):
进程占用处理器资源;处于此状态的进程的数目小于等于处理器的数目。在没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。
结合上面的公式可以理解为,运行状态就是CPU正在执行该进程。
3)阻塞状态(Blocked):
由于进程等待某种条件(如I/O操作或进程同步),在条件满足之前无法继续执行。该事件发生前即使把处理器资源分配给该进程,也无法运行。
结合上面的公式可以理解为,阻塞状态就是进程等待运行的资源满足,(此时进程的资源还没有准备好)。

1.2 进程与线程的区别

⑴. 进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
⑵. 一个程序至少有一个进程,一个进程至少有一个线程。
⑶. 进程之间相互独立,不共享全局变量;线程之间可以共享全局变量。
⑷. 线程的划分尺度小于进程,而多个线程共享内存,从而提高了程序的运行效率。
⑸. 在调度时,线程的调度快过进程的调度。
通过下图可以充分的理解上述区别,请读者细细理解。
在这里插入图片描述

2. multiprocessing模块

通过Python的multiprocessing模块,我们可以创建多进程来实现多任务。
标识每个进程的值称为进程标识符,用PID表示。
这里的PID不是我们讲过的端口号,读者一定要注意区分。
只要运行一程序,系统会自动分配一个PID值给它。
PID值是暂时唯一的,进程中止后,这个号码就会被回收,并可能被分配给另一个新进程。
在这里插入图片描述接下来,我们使用multiprocessing模块来创建多进程。

import multiprocessing
import os
import time

def test1():
	"""测试函数1"""
    print("子进程1的PID号是:%d" %os.getpid())
    while True:
        print("This is test1")
        time.sleep(2)

def test2():
	"""测试函数2"""
    print("子进程2的PID号是:%d" %os.getpid())
    while True:
        print("This is test2")
        time.sleep(2)

def main():
	# 使用multiprocessing模块的Process类来创建子进程对象
    t1 = multiprocessing.Process(target=test1)
    t2 = multiprocessing.Process(target=test2)
    # 调用start函数开启子进程
    t1.start()
    t2.start()
    print("子进程1的PID号是:"+str(t1.pid))
    print("子进程2的PID号是:"+str(t2.pid))
    print("总进程的PID号是:%d" %os.getpid())

if __name__ == '__main__':
    main()

上述代码通过multiprocessing模块的Process类来创建两个子进程,其中target指向的参数就是子进程需要执行的代码块。其用法与线程threading模块用法一样,所以不再过多讲述。
在这里我通过两种方式来获得进程号PID值。

  1. 引入os模块,调用os模块的getpid的函数获取PID值。
  2. 通过调用子进程对象的pid属性来获取PID值。

运行上述代码,我们可得结果如下图所示。
在这里插入图片描述
在这时我们打开任务管理器,点到详细信息,可以看到这时正在运行的python程序有三个。
其中有一个就是主进程,另外两个就是我们创建的子进程。
在这里插入图片描述

3. 进程间通信

由于多进程之间是相互独立的,不能共享全局变量,所以进程之间是怎样通信的呢?
其实我们已经学过了其中一种通信的方式——套接字。
socket编程就是我们将要通信的数据发送到网络,通过网络(各种交换机、路由器)实现不同进程之间的通信的过程。
实现进程间通信有许多种方式,如图所示。
在这里插入图片描述

3.1 消息队列Queue

通过multiprocessing模块的Queue对象可以实现进程间通信。
Queue,翻译过来就是队列的意思。
在《数据结构》中我们学习过有关树、队列、栈等相关知识。什么双端队列、循环队列、线索二叉树、平衡二叉树等等。在这里我就不详细的展开,若有需求请自行CSDN。
阅读下面的内容只需要知道队列的特点是先进先出,栈的特点是后进先出即可。

import multiprocessing

def download_from_server(queue):
    """模拟从网上下载数据"""
    datas = [11,22,33,44,55]
    for data in datas:
    	# 通过put函数,将数据加入队列
        queue.put(data)	

def analysis_data(queue):
    """模拟数据处理"""
    while True:
    	# 通过get_nowait函数,取出队列中最前面的数据
        data = queue.get_nowait()
        print(data)
        # 通过empty函数检测队列的元素个数是否为0,返回值为布尔型。
        if queue.empty():
            print("当前队列为空")
            break


def main():
 	# 初始化一个Queue对象,最多可接受5条信息
    queue = multiprocessing.Queue(5) 
    t1 = multiprocessing.Process(target=download_from_server,args=(queue,))
    t2 = multiprocessing.Process(target=analysis_data,args=(queue,))
    t1.start()
    t2.start()

if __name__ == '__main__':
    main()

首先,我们使用multiprocessing模块的Queue创建一个Queue对象。
然后,在子进程1中将要通信的数据通过put函数加入队列。
最后,在子进程2中通过get_nowait函数取出数据进行处理。
将数据放进队列可用:Queue.put()、Queue.put_nowait()
将数据从队列中取出:Queue.get()、Queue.get_nowait()
判断队列是否为满:Queue.full()
判断队列是否为空:Queue.empty()
「put函数和put_nowait函数的差别就是,当队列元素满的时候,再向队列加入元素是否会报错,同理get函数和get_nowait函数也是如此」

3.2 进程池

如果有大量任务需要多进程完成,则可能需要频繁的创建删除进程,这样会带来较多的资源消耗。
这种情况,我们创建适当的进程放入进程池,用来处理待处理事件,处理完毕后进程不销毁,仍然在进程池中等待处理其他事件,直到事件全部处理完毕,进程退出。 通过进程的复用降低资源的消耗。

import multiprocessing
import os,time,random

def test(msg):
	"""测试函数"""
    t_start = time.time()
    print("%s开始执行,进程号为%d"%(msg,os.getpid()))
    time.sleep(random.random()*2)
    t_stop = time.time()
    run_time = t_stop-t_start
    print(msg,"执行完毕,耗时%0.2f" %run_time)

def main():
	# 创建进程池对象
    pool = multiprocessing.Pool(3)
    for i in range(10):
    	# 将事件异步加入进程池
        pool.apply_async(test,(i,))
    # 关闭进程池,此时不能再将事件加入进程池
    pool.close()
    # 此时主进程会形成堵塞,会等待进程池中的任务全部执行完主进程才会解堵塞
    pool.join()

if __name__ == '__main__':
    main()

通过multiprocessing模块Pool类创建进程池对象,创建时在池内放入合适数量的进程(默认为CPU的个数),
然后通过apply_async函数将事件加入进程池的等待队列,使用进程池内的进程不断的执行等待事件,
直到所有事件执行完毕,所有事件处理完毕后,关闭进程池,回收进程池。

将事件加入进程池有两种函方式:

  • apply 同步提交,直接返回结果。
  • apply_async 异步提交,返回对象,通过对象获取返回值,异步提交时要用Pool.close() 和Pool.join()。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章