Python:多进程。

参考:https://www.liaoxuefeng.com/wiki/1016959663602400/1017628290184064

 

Python程序实现多进程(multiprocessing)

  • Fork系统和Ruby相关
  • Python多进程
    • os模块
    • multiprocessing模块:Process类,Pool类
    • 子进程subprocess模块
    • multiprocessing中的Pipe()和Queue(), 以及Connection对象。

 

了解linux的Fork系统调用(wiki)

计算机领域中,尤其是Unix类Unix系统操作系统中,fork(进程复制)是一种创建自身行程副本的操作。它通常是内核实现的一种系统调用。Fork是类Unix操作系统上创建进程的一种主要方法,甚至历史上是唯一方法。

在多任务操作系统中,行程(运行的程序)需要一种方法来创建新进程,例如运行其他程序。

如果进程需要启动另一个程序的可执行文件,它需要先Fork来创建一个自身的副本。然后由该副本即“子进程”调用exec系统调用,用其他程序覆盖自身:停止执行自己之前的程序并执行其他程序。

 

当一个进程调用fork时,它被认为是父进程,新创建的进程是它的孩子(子进程)。在fork之后,两个进程不仅运行着相同的程序,并且它们恢复执行(好像它们都已被系统调用)both processes not only run the same program, but they resume execution as though both had called the system call. 。然后它们可以检查调用的返回值确定其状态:是父进程还是子进程,以及据此行事。

 


 

Ruby的多进程处理模块Process

Ruby核心模块Process提供了大量和unix对应的接口方法。

fork[{block}] -> integer or nil

创建子进程。如果提供了block则会在子进程内运行。子进程终止会返回一个状态码0

调用fork会返回两次,一次是父进程,它返回孩子进程的ID; 另一次是孩子进程,返回nil。

孩子进程使用Kernel.exit!来退出。

父亲进程需要使用Process.wait来收集孩子进程的终止状态码。

 

exec([env,] command...[,options])

子进程调用exec方法,通过运行参数的命令取代当前进程。

 

pid -> integer

返回当前进程的id. Process.pid => 37415

 

ppid -> integer

返回当前进程的父进程的id.

  • puts "#{Process.pid}" Process.fork { puts "child_id:#{Process.pid}\nfather_id:#{Process.ppid}"}
  • 第一行代码输出当前进程的id
  • 第二行代码使用fork带一个块,块在子进程内运行,输出子进程自身的id, 和父进程的id,使用ppid方法,即parent process id。

 

多线程教程:https://www.runoob.com/ruby/ruby-multithreading.html



 

 

Python多进程

 

OS模块

Python的os模块封装了常见的系统调用,其中就包括fork,可以在Python程序中轻松创建子进程。

fork函数,调用一次,返回2次:

分别在子进程返回0,在父进程返回子进程的ID,这样设计的原因是:

  1. 一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,
  2. 而子进程只需要调用getppid()就可以拿到父进程的ID

 

import os

print("current process %s" % os.getpid())  #输出当前进程的🆔

pid = os.fork()  #复制出一个子进程。pid指向返回值。每个进程各自有不同的pid。

print("》current process %s" % os.getpid())  #在每个进程内打印它的🆔。

if pid == 0:  #根据fork返回值来判断,当前在哪个进程的内部!
    print('I am child process (%s) and my parent is %s.' % (os.getpid(), os.getppid()))
else:
    print('I (%s) just created a child process (%s).' % (os.getpid(), pid))
  • os.getpid()返回当前进程id
  • os.getppid()返回当前进程的父进程的id。
#一次输出结果:
current process 36212
》》current process 36212              #这行和下一行,是在主进程内执行后,打印出的。
I (36212) just created a child process (36213).
》》current process 36213              #最后2行输出,是在子进程内执行后,打印的。
I am child process (36213) and my parent is 36212.

 

解释:

首先,打印出当前进程的id.

其次,fork当前进程,产生一个子进程。然后这个fork函数在父进程内返回子进程的🆔, 并在子进程的内部返回状态码0。

之后,在每个进程内,打印自身的🆔。

最后,一个if..else..语句的判断。这是因为是2个进程,每个进程都有if语句。根据fork函数的返回值,可以判断当前进程的位置是父进程还是子进程。然后执行不同的代码。

⚠️:Windows没有fork调用。

 

fork的实际用途

当一个进程在接到新的任务时,可以通过fork函数来复制一个子进程,用子进程来处理新任务。例子:

常见的Apache服务器,由父进程监听端口port, 每当有新的http请求时,就fork一个子进程,用来处理新的http请求。


 

multiprocessing--Process-based parallelism

这个模块支持Win和Unix系统。所以可以用它在window系统上编写多进程服务程序。

根据操作系统不同,这个模块支持3种启动进程的方法:

  • spawn(3.4版本增加的,速度比fork和forkserver慢,但自3.8后对于macOS, 这是默认方式,因为安全问题)。父进程启动一个新的Python解释器进程。子进程只继承必要的资源,运行进程对象的run()方法所必须的资源。
  • fork, 这种启动方式只存在于Unix系统内,是默认的。使用os.fork()来产生Python解释器分叉。
  • forkserver 

 

Process类

模块内的一个类,用于创建一个Process对象(代表一个进程对象)。然后调用start()方法启动子进程。

 

from multiprocessing import Process
import os

# 子进程要执行的代码
def run_proc(name):
    print('Run child process %s (%s)' %(name, os.getpid()) )

if __name__ == '__main__':
    print('Parent process %s.' % os.getpid() )
    p = Process(target=run_proc, args=('test', ))
    p.start()
    p.join()
    print('Child process end.'

注意:

创建Process类的对象时,要写2个参数:

  • target指向创建的子进程内运行的代码。
  • args是传入子进程内部的参数。

join([timeout])

方法可以等待调用他的子进程结束后,继续之后的代码运行,通常用于进程间的同步。

  • 如果可选参数 timeout 是 None (默认值),则该方法将阻塞,直到调用 join() 方法的进程终止。
  • 如果 timeout 是一个正数,它最多会阻塞 timeout 秒。

 

⚠️运行上面代码遇到一个错误,TypeError: run_proc() takes 1 positional argument but 5 were given 

原因, args是一个'test,'字符串,传入的参数args应该是tuple,即('test', )。输入错误。

p = Process(target=run_proc, args=('test,'))

 

 

Pool类:

如果要启动大量的子进程,可以用进程池的方式批量创建子进程。

Pool会赋予函数并行化处理一系列输入值的能力,可以将输入的数据分配给不同的进程处理(数据并行)。

⚠️Pool的含义是从池水引申为是备用的共用资源 

 

简单例子:

from multiprocessing import Pool

def f(x):
    return x*x

if __name__ == '__main__':
    with Pool(5) as p:
        print(p.map(f, [1,2,3]))
#[1, 4, 9]

 

解释:

map(fun, iterable[, chunksize])

内置函数map的并行版本。它是multiprocessing包内的方法。它在结果准备好前,会阻塞block。

map_async(funciterable[, chunksize[, callback[, error_callback]]])

和上面的map()方法类似,可以通过callback返回一个可调用对象。

 

with声明:(点击查看原理)

封装了启动和关闭子进程的功能,可以自动关闭文件、线程锁的自动获取和释放等。

 

Using a pool of workers(work processes)  使用备用的工作进程。ma p

附录:pool原意是水池,引申为备用的人员:a pool of cheap labour ,廉价劳动力。所以文档中a pool of workers翻译过来是备用的进程。

Pool类代表了备用的工作进程。它的实例方法可以把任务卸载offload到这些备用进程内。

from multiprocessing import Pool

import os, time, random

def long_time_task(name):
    print("Run task %s (%s)..." %(name, os.getpid()))
    start = time.time()  #获得1970年到现在的秒数。
    time.sleep(random.random()*3)
    end = time.time()
    print('Task %s runs %0.2f seconds' % (name, (end - start)))

if __name__ == '__main__':
    print("Parent process %s" % os.getpid())
    p = Pool(4)
    for i in range(5):
        p.apply_async(long_time_task, args=(i,))
    print('Waiting for all subprocesses done...')
    p.close()
    p.join()
    print('All subprocesses done.')

结果:

Run task 0 (36505)...
Run task 1 (36503)...
Run task 2 (36506)...
Run task 3 (36504)...
Task 1 runs 1.15 seconds.
Run task 4 (36503)...
Task 2 runs 1.74 seconds.
Task 0 runs 2.03 seconds.
Task 3 runs 2.50 seconds.
Task 4 runs 1.45 seconds.
All subprocesses done.

 

解释:

Pool的apply_async(func[,   args[,   kwds[, callback[, error_callback]]]])

apply方法的并行版本,返回一个结果对象。在返回结果前会block阻塞。 ⚠️类似map和map_async。

 

Pool的close()方法:

阻止后续任务提交到进程池,当所有任务执行完成后,工作进程会退出。

 

Pool的join([timeout])

必须在close()或terminate()后调用。等待工作进程结束。

 

子进程--subprocess模块

很多时候,子进程并不是自身,而是一个外部进程。我们创建了子进程后,还需要控制子进程的输入和输出。

subprocess模块可以让我们非常方便地启动一个子进程,然后控制其输入和输出。

 

进程间通信 (点击查看官方文档的例子)

Process之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。

Python的multiprocessing模块包装了底层的机制,提供了QueuePipes在进程之间交换object。

 

class multiprocessing.Queue --队列

Queue类其实就是对模块queue中的Queue类的克隆。使用方法大致一样。

get([block[, timeout]])

 

移除并返回一个item,如果block是True, timeout是None,即都是默认,那么会阻塞,直到一个item可以被使用。

如果timeout存在,则最多阻塞timeout秒,如果没有item可用,raise queue.Empty例外。

 

注意⚠️,队列其实在底层使用了管道机制。Queue()返回一个使用一个管道和少量锁和信号量实现的共享队列实例。当一个进程将一个对象放进队列中时,一个写入线程会启动并将对象从缓冲区写入管道中。 

 

例子:

from multiprocessing import Process, Queue
import os, time, random

def write(arg):
    print('Process to write: %s' % os.getpid())
    for value in ['A', 'B', 'C']:
        print('Put %s to queue...' % value)
        arg.put(value)
        time.sleep(random.random())

def read(arg):
    print('Process to read: %s' % os.getpid())
    while True:
        value = arg.get()
        print('Get %s from queue' % value) 

if __name__ == '__main__':
    q = Queue()
    pw = Process(target=write, args=(q,))
    pr = Process(target=read, args=(q,))

    pw.start() #启动子进程pw,写入
    pr.start() #启动子进程pr,读取

    pw.join() #等待pr结束

    pr.terminate() #pr是死循环,无法等待结束,强行终止。

 

 

multiprocessing.Pipes()函数

返回一对相互连接的Connection对象。表示管道的两端,默认True是双向连接。如果是False,则是单向传输数据。

class multiprocessing.connection.Connection对象

它用于发送/接收管道对象或strings。使用Pipe()创建。

  • send(obj)
  • recv()
  • close()关闭这个连接。这个方法会被垃圾收集机制自动调用

⚠️:

例子:

from multiprocessing import Process, Pipe

def f(conn):
    conn.send([42, None, 'hi'])
    conn.close()

if __name__ == '__main__':
    parent_conn, child_conn = Pipe()
    p = Process(target=f, args=(child_conn,))
    p.start()
    print(parent_conn.recv())
    p.join()

 

 

小结:

使用多进程时,一般使用“信息传递机制”实现进程之间的通信。避免使用任何原始的同步方式(同步原语)如locks。

“信息传递机制”:

  • Pipe(), 连接2个进程,
  • 一个队列(能够在多个producers和contumers之间通信)

 

 

 

 

 

posted @ 2019-11-18 11:43  Mr-chen  阅读(803)  评论(0)    收藏  举报