Python学习系列之多进程

一、多进程

       Unix/Linux操作系统提供了一个fork()系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。每一个进程都有一个GIL,所以多线程能解决GIL问题。

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

# multiprocessing.py
import os

print 'Process (%s) start...' % os.getpid()
pid = os.fork()
if pid==0:
    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)

运行结果如下:

Process (876) start...
I (876) just created a child process (877).
I am child process (877) and my parent is 876.

缺点:
1.兼容性差,只能在类linux系统下使用,windows系统不可使用;
2.扩展性差,当需要多条进程的时候,进程管理变得很复杂;
3.会产生“孤儿”进程和“僵尸”进程,需要手动回收资源。
优点:
是系统自带的接近低层的创建方式,运行效率高。 

      由于Windows没有fork调用,上面的代码在Windows上无法运行。由于Mac系统是基于BSD(Unix的一种)内核,所以,在Mac下运行是没有问题的,Linux下更没有问题。有了fork调用,一个进程在接到新任务时就可以复制出一个子进程来处理新任务,常见的Apache服务器就是由父进程监听端口,每当有新的http请求时,就fork出子进程来处理新的http请求(平常我们利用Java写网络编程进行网络监听,一般用多线程,主线程是UI,一个子线程监听,一个子线程去处理网络请求)

threading-pic

二、multiprocessing 模块

      如果你打算编写多进程的服务程序,Unix/Linux无疑是正确的选择。由于Windows没有fork调用,难道在Windows上无法用Python编写多进程的程序?由于Python是跨平台的,自然也应该提供一个跨平台的多进程支持。

       multiprocessing模块就是跨平台版本的多进程模块,是Python的标准模块。multiprocessing包提供本地和远程两种并发,通过使用子进程而非线程有效地回避了全局解释器锁。如果你要做的是 CPU 密集型操作,那么你需要使用 Python 的 multiprocessing 模块。原因是,Python 有一个全局解释器锁 (GIL),使得所有子线程都必须运行在同一个进程中。正因为如此,当你通过多线程来处理多个 CPU 密集型任务时,你会发现它实际上运行的更慢。多线程最擅长的领域是I/O 操作。用pickle部分地实现了变量共享。

       因为python使用全局解释器锁(GIL),它会将进程中的线程序列化,也就是多核cpu实际上并不能达到并行提高速度的目的,而使用多进程则是不受限的,所以实际应用中都是推荐多进程的。
  如果每个子进程执行需要消耗的时间非常短(执行+1操作等),这不必使用多进程,因为进程的启动关闭也会耗费资源。当然使用多进程往往是用来处理CPU密集型(科学计算)的需求,如果是IO密集型(文件读取,爬虫等)则可以使用多线程去处理。

GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL。

在python3.x中,GIL不使用ticks计数,改为使用计时器(执行时间达到阈值后,当前线程释放GIL) 。

多核多线程比单核多线程更差,原因是单核下多线程,每次释放GIL,唤醒的那个线程都能获取到GIL锁,所以能够无缝执行,但多核下,CPU0释放GIL后,其他CPU上的线程都会进行竞争,但GIL可能会马上又被CPU0拿到,导致其他几个CPU上被唤醒后的线程会醒着等待到切换时间后又进入待调度状态,这样会造成线程颠簸(thrashing),导致效率更低。

相关模块:

创建管理进程模块:

  • Process(用于创建进程模块)Process模块用来创建子进程,是Multiprocessing核心模块,使用方式与Threading类似,可以实现多进程的创建,启动,关闭等操作。
  • Pool(用于创建管理进程池)Pool模块是用来创建管理进程池的,当子进程非常多且需要控制子进程数量时可以使用此模块。
  • Queue(用于进程通信,资源共享)Queue模块用来控制进程安全,与线程中的Queue用法一样。
  • Value,Array(用于进程通信,资源共享)
  • Pipe(用于管道通信)Pipe模块用来管道操作。
  • Manager(用于资源共享)Manager模块常与Pool模块一起使用,作用是共享资源。

同步子进程模块:

  • Condition
  • Event
  • Lock
  • RLock
  • Semaphore

2.1 Process 类

Process 类用来描述一个进程对象。创建子进程的时候,只需要传入一个执行函数函数的参数即可完成 Process 对象的创建。

Process 类适合简单的进程创建,如需资源共享可以结合 multiprocessing.Queue 使用;如果想要控制进程数量,则建议使用进程池 Pool 类。

基本语法:

Process([group [, target [, name [, args [, kwargs]]]]])

multiprocessing.Process(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)

参数:与 threading 不同,传递给 multiprocessing Process 的参数必需是可序列化的。

  • group:实质上不使用,是保留项,便于以后扩展。线程组,目前还没有实现,库引用中提示必须是 None。
  • target:表示调用对象,要执行的方法。
  • args:表示调用对象的位置参数元组,以touple的形式传入。
  • kwargs:表示调用对象的字典,关键字参数。
  • name:为别名,即进程的名字。

相关函数和属性:

  • start() 方法启动进程。进程准备就绪,等待 CPU 调度。
  • run():表示进程的活动方法,可以在子类中覆盖它。如果实例进程时未制定传入 target,start 执行默认 run() 方法。
  • join() 方法实现进程间的同步,等待所有进程退出。是用来阻塞当前上下文,直至该进程运行结束,一个进程可以被join()多次,timeout单位是秒。
  • close() 用来阻止多余的进程进入进程池 Pool 造成进程阻塞。
  • terminate():结束进程。在Unix上使用的是SIGTERM,在Windows平台上使用TerminateProcess。不管任务是否完成,立即停止工作进程。
  • is_alive():判断进程是否还活着。
  • name:一个字符串,表示进程的名字,也可以通过赋值语句利用它来修改进程的名字
  • ident:进程的ID,如果进程没开始,结果是None
  • pid:同ident,大家可以看看ident和pid的实现,是利用了os模块的getpid()方法。进程号。
  • authkey:设置/获取进程的授权密码。当初始化多进程时,使用os.urandom()为主进程分配一个随机字符串。当创建一个Process对象时,它将继承其父进程的认证密钥, 但是可以通过将authkey设置为另一个字节字符串来改变。这里authkey为什么既可以设置授权密码又可以获取呢?那是因为它的定义使用了property装饰器。
  • daemon:一个布尔值,指示进程是(True)否(False)是一个守护进程。它必须在调用start()之前设置,否则会引发RuntimeError。它的初始值继承自创建它的进程;进程不是一个守护进程,所以在进程中创建的所有进程默认daemon = False。和线程的 setDeamon 功能一样(将父进程设置为守护进程,当父进程结束时,子进程也结束)。

       启动后台进程运行而不阻止主程序退出是有用的,例如为监视工具生成“心跳”的任务。输出不包括来自守护进程的“退出”消息,因为所有非守护进程(包括主程序)在守护进程从两秒休眠状态唤醒之前退出。守护进程在主程序退出之前自动终止,这避免了孤立进程的运行。这可以通过查找程序运行时打印的进程 ID 值来验证,然后使用 ps 命令检查该进程

  • exitcode:返回进程退出时的代码。进程运行时其值为None,如果为–N,表示被信号N结束。

示例:

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

import multiprocessing
print 'Parent process %s.' % os.getpid()
p = multiprocessing.Process(target=run_proc, args=('test',))
print 'Process will start.'
p.start()
p.join()
print 'Process end.'

Parent process 27832.
Process will start.
Run child process test (27312)...
Process end.

       创建子进程时,只需要传入一个执行函数函数的参数,创建一个Process实例,用start()方法启动,这样创建进程比fork()还要简单。join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步

以上是函数式启动多线程,还可以继承进程类,实现自定义进程类,并开启多个进程。 

使用传入函数的方式有一点不好的是封装性太差,如果功能稍微复杂点,将会有很多的全局变量暴露在外,最好还是将功能封装成类。

class MyProcess(multiprocessing.Process):
    """
    自定义进程类
    """
    def __init__(self,interval,group=None,target=None,name=None,args=(),kwargs={}):
        multiprocessing.Process.__init__(self,group,target,name,args,kwargs=kwargs)
        self.interval = interval

    def run(self):
        n = 5
        while n > 0:
            print("the time is %s"%datetime.datetime.now())
            time.sleep(self.interval)
            n -= 1


def worker_1(interval):
    print "worker_1"
    time.sleep(interval)
    print "end worker_1"

def worker_2(interval):
    print "worker_2"
    time.sleep(interval)
    print "end worker_2"

def worker_3(interval):
    print "worker_3"
    time.sleep(interval)
    print "end worker_3"


if __name__ == "__main__":
    p1 = MyProcess(interval=2,target = worker_1, args = (2,))
    p2 = MyProcess(interval=2,target = worker_2, args = (3,))
    p3 = MyProcess(interval=2,target = worker_3, args = (4,))

    p1.start()
    p2.start()
    p3.start()
    print "current process",multiprocessing.current_process(),multiprocessing.active_children()
    print("The number of CPU is:" + str(multiprocessing.cpu_count()))
    for p in multiprocessing.active_children():
        print("child   p.name:" + p.name + "\tp.id" + str(p.pid))
    print "END!!!!!!!!!!!!!!!!!"

2.2 Pool 

      如果要启动大量的子进程,可以用进程池的方式批量创建和管理子进程。 Pool 进程池可以提供指定数量的进程供用户使用,默认是 CPU 核数。当有新的请求提交到 Pool 的时候,如果池子没有满,会创建一个进程来执行,否则就会让该请求等待。

注:该函数threading没提供线程池实现。

  • Pool 对象调用 join 方法会等待所有的子进程执行完毕 
  • 调用 join 方法之前,必须调用 close 
  • 调用 close 之后就不能继续添加新的 Process 了

2.2.1 pool.apply_async

apply_async 方法用来同步执行进程,允许多个进程同时进入池子。

def run_task(name):
    print('Task {0} pid {1} is running, parent id is {2}'.format(name, os.getpid(), os.getppid()))
    time.sleep(1)
    print('Task {0} end.'.format(name))

if __name__ == '__main__':
    print('current process {0}'.format(os.getpid()))
    p = multiprocessing.Pool(processes=3)
    for i in range(6):
        p.apply_async(run_task, args=(i,))
    print('Waiting for all subprocesses done...')
    p.close()
    p.join()        # 调用join之前先调用close
    print('All processes done!')


current process 921
Waiting for all subprocesses done...
Task 0 pid 922 is running, parent id is 921
Task 1 pid 923 is running, parent id is 921
Task 2 pid 924 is running, parent id is 921
Task 0 end.
Task 3 pid 922 is running, parent id is 921
Task 1 end.
Task 4 pid 923 is running, parent id is 921
Task 2 end.
Task 5 pid 924 is running, parent id is 921
Task 3 end.
Task 4 end.
Task 5 end.
All processes done!

2.2.2 pool.apply

apply(func[, args[, kwds]])

该方法只能允许一个进程进入池子,在一个进程结束之后,另外一个进程才可以进入池子。

一种阻塞式添加任务的方法,p1.apply(test),其每次只能向进程池添加一条任务,然后for循环会被堵塞等待,
直到添加的任务被执行完毕,进程池中的5个进程交替执行新来的任务,相当于单进程了。

def run_task(name):
    print('Task {0} pid {1} is running, parent id is {2}'.format(name, os.getpid(), os.getppid()))
    time.sleep(1)
    print('Task {0} end.'.format(name))

if __name__ == '__main__':
    print('current process {0}'.format(os.getpid()))
    p = multiprocessing.Pool(processes=3)
    for i in range(6):
        p.apply(run_task, args=(i,))
    print('Waiting for all subprocesses done...')
    p.close()
    p.join()
    print('All processes done!')


ask 0 pid 928 is running, parent id is 927
Task 0 end.
Task 1 pid 929 is running, parent id is 927
Task 1 end.
Task 2 pid 930 is running, parent id is 927
Task 2 end.
Task 3 pid 928 is running, parent id is 927
Task 3 end.
Task 4 pid 929 is running, parent id is 927
Task 4 end.
Task 5 pid 930 is running, parent id is 927
Task 5 end.
Waiting for all subprocesses done...
All processes done!

 2.2.3 示例:

from multiprocessing import Pool
import os, time, random

def long_time_task(name):
    print 'Run task %s (%s)...' % (name, os.getpid())
    start = time.time()
    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()
    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.'


Parent process 669.
Waiting for all subprocesses done...
Run task 0 (671)...
Run task 1 (672)...
Run task 2 (673)...
Run task 3 (674)...
Task 2 runs 0.14 seconds.
Run task 4 (673)...
Task 1 runs 0.27 seconds.
Task 3 runs 0.86 seconds.
Task 0 runs 1.41 seconds.
Task 4 runs 1.91 seconds.
All subprocesses done.

解释:对Pool对象调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close(),调用close()之后就不能继续添加新的Process了。

请注意输出的结果,task 0123是立刻执行的,而task 4要等待前面某个task完成后才执行,这是因为Pool的默认大小在测试电脑上是4,因此,最多同时执行4个进程。这是Pool有意设计的限制,并不是操作系统的限制。如果改成:

p = Pool(5)

就可以同时跑5个进程。

由于Pool的默认大小是CPU的核数,如果你不幸拥有8核CPU,你要提交至少9个子进程才能看到上面的等待效果。

 

三、进程间通信

进程只能通过进程间通讯(interprocess communication, IPC),而不能直接共享信息。在Linux多线程中介绍的管道PIPE和消息队列message queue,multiprocessing包中有Pipe类和Queue类来分别支持这两种IPC机制。

Process之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing模块包装了底层的机制,提供了Queue、Pipes等多种方式来交换数据。

3.1 Queue 进程间通信(推荐)

Queue 类用来在多个进程间通信。Queue 有两个方法,get 和 put。

3.1.1 put 方法

Put 方法用来插入数据到队列中,有两个可选参数,blocked 和 timeout。 
- blocked = True(默认值),timeout 为正

该方法会阻塞 timeout 指定的时间,直到该队列有剩余空间。如果超时,抛出 Queue.Full 异常。

- blocked = False 
如果 Queue 已满,立刻抛出 Queue.Full 异常

3.1.2 get 方法

get 方法用来从队列中读取并删除一个元素。有两个参数可选,blocked 和 timeout 
- blocked = False (默认),timeout 正值

等待时间内,没有取到任何元素,会抛出 Queue.Empty 异常。
- blocked = True 
Queue 有一个值可用,立刻返回改值;Queue 没有任何元素,

示例:
 

# 写数据进程执行的代码:
def write(q):
    for value in ['A', 'B', 'C']:
        print 'Put %s to queue...' % value
        q.put(value)
        time.sleep(random.random())

# 读数据进程执行的代码:
def read(q):
    while True:
        value = q.get(True)
        print 'Get %s from queue.' % value

if __name__=='__main__':
    # 父进程创建Queue,并传给各个子进程:
    q = Queue()
    pw = Process(target=write, args=(q,))
    pr = Process(target=read, args=(q,))
    # 启动子进程pw,写入:
    pw.start()
    # 启动子进程pr,读取:
    pr.start()
    # 等待pw结束:
    pw.join()
    # pr进程里是死循环,无法等待其结束,只能强行终止:
    pr.terminate()


Put A to queue...
Get A from queue.
Put B to queue...
Get B from queue.
Put C to queue...
Get C from queue.

Queue的进程同步中会将Queue对象作为参数传入Process创建中。

multiprocessing.Queue(maxsize=0) #建立共享的队列实例,可以采用一般队列的方式访问,通过put()方法增加元素,通过get()方法获取元素。

multiprocessing.JoinableQueue(maxsize=0) #建立可阻塞的队列实例,采用一般队列的方式访问,但可以通过XXX.join()阻塞队列(即队列元素未全部处理完前,进程阻塞)

multiprocessing.SimpleQueue() #还有一种简化的队列,其只具有empty、get、put3个方法。

3.2 Pipe 进程间通信 

常用来在两个进程间通信,两个进程分别位于管道的两端(不推荐使用)。

Pipe不是类,是函数,该函数定义在 multiprocessing中的connection.py里,函数原型Pipe(duplex=True),

函数原型:

multiprocessing.Pipe([duplex])
Pipe(duplex=True)

参数:

  • 如果duplex是True(默认值),则管道是双向的。
  • 如果duplex是False,则管道是单向的:conn1只能用于接收消息,conn2只能用于发送消息。

返回值:

 返回一对通过管道连接的连接对象conn1和conn2。

Pipe()返回的两个连接对象表示管道的两端,每个连接对象都有send()和recv()方法(还有其它方法),分别是发送和接受消息。

示例:

from multiprocessing import Process, Pipe

def send(pipe):
    pipe.send(['spam'] + [42, 'egg'])   # send 传输一个列表
    pipe.close()

if __name__ == '__main__':
    (con1, con2) = Pipe()                         # 创建两个 Pipe 实例
    sender = Process(target=send, args=(con1, ))  # 函数的参数,args 一定是实例化之后
                                                  # 的 Pip 变量,不能直接写 args=(Pip(),)
    sender.start()                                # Process 类启动进程
    print("con2 got: %s" % con2.recv())           # 管道的另一端 con2 从send收到消息
    con2.close()                                  # 关闭管道


con2 got: ['spam', 42, 'egg']
from multiprocessing import Process, Pipe

def talk(pipe):
    pipe.send(dict(name='Bob', spam=42))            # 传输一个字典
    reply = pipe.recv()                             # 接收传输的数据
    print('talker got:', reply)

if __name__ == '__main__':
    (parentEnd, childEnd) = Pipe()                  # 创建两个 Pipe() 实例,也可以改成 conf1, conf2
    child = Process(target=talk, args=(childEnd,))  # 创建一个 Process 进程,名称为 child
    child.start()                                   # 启动进程
    print('parent got:', parentEnd.recv())          # parentEnd 是一个 Pip() 管道,可以接收 child Process 进程传输的数据
    parentEnd.send({x * 2 for x in 'spam'})         # parentEnd 是一个 Pip() 管道,可以使用 send 方法来传输数据
    child.join()                                    # 传输的数据被 talk 函数内的 pip 管道接收,并赋值给 reply
    print('parent exit')

parent got: {'name': 'Bob', 'spam': 42}
talker got: {'ss', 'aa', 'pp', 'mm'}
parent exit

 

四、进程间同步

multiprocessing具有多种锁类型,根据使用情况自行选择:

multiprocessing.Lock() #最简单的锁(非递归锁)

multiprocessing.RLock() #可复用的锁(递归锁)

multiprocessing.Semaphore(value=1) #计数器锁(信号量锁),value为初始计数

multiprocessing.BoundedSemaphore(value=1) #带上限的计数器锁(信号量锁),value即是初始计数,同时也是允许的计数上限

以上锁即可通过acquire/release方法获得/释放,也可采用with上下文方式来使用(with lock: …, 这样可以省去acquire/release语句)。

multiprocessing.Event() #事件锁,当事件触发时释放。其通过set/clear方法获得/释放。

multiprocessing.Condition(lock = None) #条件锁,当条件触发时释放。其通过wait_for来条件阻塞,当条件满足时自动释放;也可用作类事件锁,通过wait阻塞,notify或notify_all释放。

multiprocessing.Barrier(parties, action=None, timeout=None) #障碍锁,等待进程数达到parties要求数目后释放,可用于进程同步。其通过wait阻塞,等待进程数达标后自动释放;也可通过abort强行释放。

也可通过manager创建锁,这种方式创建的锁,不仅可以本地共享,也可网络共享。

4.1 互斥锁

进程同步也是利用锁来实现,和线程使用方法一致。

import multiprocessing
import time

def worker_with(lock,file):
    with lock:
        f=open(file,'a')
        f.write('Lock acquired via with')
        f.close()

def woker_no_with(lock,file):
    lock.acquire()
    try:
        f=open(file,'a')
        f.write('Lock acquired directly')
        f.close()
    finally:
        lock.release()

if __name__ == '__main__':
    l=multiprocessing.Lock()
    w=multiprocessing.Process(target=worker_with,args=(l,'c:\\test1.txt'))
    nw=multiprocessing.Process(target=woker_no_with,args=(l,'c:\\test1.txt'))
    w.start()
    nw.start()
    w.join()
    nw.join()

4.2 Semaphore

信号量,是在进程同步过程中一个比较重要的角色。可以控制临界资源的数量,保证各个进程之间的互斥和同步。

4.3 Event

Event类提供一种简单的方式进行进程之间的通信。可以在设置和未设置状态之间切换事件。事件对象的用户可以使用可选的超时值等待它从未设置更改为设置。

五、共享数据

多进程不推荐共享数据,应该避免。

5.1 共享值(共享内存)

multiprocessing.Value(typecode_or_type, *args, lock=True) #共享单个数据,其值通过value属性访问。如果在修改、访问数组时,希望能锁定资源,阻塞其他访问,可以将lock设为True,通过XXX.acquire()获得锁,XXX.release()释放锁。关于锁的概念后面再讲。

5.2 共享数组(共享内存)

multiprocessing.Array(typecode_or_type, size_or_initializer, *, lock=True) #其返回的数组实例可通过索引访问。类似共享值,同样可以加锁访问。

5.3 Manager模块

multiprocessing.Manager() #创建一个manager,用于进程之间共享数据。返回的manager实例控制了一个server进程,此进程包含的python对象可以被其他的进程通过proxies来访问。其具有'address', 'connect', 'dict', 'get_server', 'join', 'list', 'register', 'shutdown', 'start'等方法,'Array', 'Barrier', 'BoundedSemaphore', 'Condition', 'Event', 'JoinableQueue', 'Lock', 'Namespace', 'Pool', 'Queue', 'RLock', 'Semaphore', 'Value'等类

Manager对象类似于服务器与客户之间的通信 (server-client),与我们在Internet上的活动很类似。我们用一个进程作为服务器,建立Manager来真正存放资源。其它的进程可以通过参数传递或者根据地址来访问Manager,建立连接后,操作服务器上的资源。在防火墙允许的情况下,我们完全可以将Manager运用于多计算机,从而模仿了一个真实的网络情境。

通过Manager对象可以实现分布式进程

六、subprocess包

6.1 基本介绍

以前我一直用os.system()处理一些系统管理任务,因为我认为那是运行linux命令或win命令最简单的方式.
我们能从Python官方文档里读到应该用subprocess 模块来运行系统命令。subprocess模块允许我们创建子进程,连接他们的输入/输出/错误管道,还有获得返回值。
subprocess模块打算来替代几个过时的模块和函数,比如:os.system, os.spawn*, os.popen*, popen2.*命令。
让我们来看一下subprocess 有哪些不同的函数:

subprocess.call()

执行由参数提供的命令。我们可以用数组作为参数运行命令,也可以用字符串作为参数运行命令(通过设置参数shell=True)
注意,参数shell默认为False。
我们用subprocess.call()来做一个统计磁盘的例子:

subprocess.call(['df', '-h'])

下面的例子把shell设置为True

subprocess.call('du -hs $HOME', shell=True)

注意,python官方文档里对参数shell=True陈述了一个警告:

Invoking the system shell with shell=True can be a security hazard if combined
with untrusted input

现在,我们来看看输入与输出

Input and Output

subprocess模块能阻止输出,当你不关心标准输出的时候是非常方便的.
它也使你通过一种正确的方式管理输入/输出,有条理地整合python脚本中的的shell命令。

Return Codes

通过subprocess.call的返回值你能够判定命令是否执行成功.
每一个进程退出时都会返回一个状态码,你可以根据这个状态码写一些代码。

stdin, stdout and stderr

我在使用subprocess时,有一个微妙的部分是怎么使用管道把命令连接起来.
管道表明一个新的子管道应该被创建。
默认的设置为None,意味着没有重定向发生
标准错误可以指向标准输出,表明子进程的错误信息会被捕获到和标准输出同一个文件。

subprocess.Popen()

subprocess模块中基本的进程创建和管理由Popen类来处理.
subprocess.popen是用来替代os.popen的.
我们来做一些真实的例子,subprocess.Popen需要一个数组作为参数:

import subprocess

p = subprocess.Popen(["echo", "hello world"], stdout=subprocess.PIPE)print p.communicate()

>>>('hello world
', None)

注意,虽然你可以使用 "shell=True",但并不推荐这样的方式.
如果你知道你只用几个有限的函数,比如PopenPIPE,你可以单单指定这几个函数:

from subprocess import Popen, PIPEp1 = Popen(["dmesg"], stdout=PIPE)print p1.communicate()

Popen.communicate()

communicate()函数返回一个tuple(标准输出和错误)。
Popen.communicate()和进程沟通:发送数据到标准输入。从标准输出和错误读取数据直到遇到结束符。等待进程结束。
输入参数应该是一个字符串,以传递给子进程,如果没有数据的话应该是None.
基本上,当你用communicate()函数的时候意味着你要执行命令了.

6.2 详细用法

早期的Python版本中,我们主要是通过os.system()、os.popen().read()等函数来执行命令行指令的,另外还有一个很少使用的commands模块。​但是从Python 2.4开始官方文档中建议使用的是subprocess模块,所以os模块和commands模块的相关函数在这里只提供一个简单的使用示例,我们重要要介绍的是subprocess模块。 

6.2.1 os下的命令执行模块

Python中提供了以下几个函数来帮助我们完成命令行指令的执行:

函数名描述
os.system(command)返回命令执行状态码,而将命令执行结果输出到屏幕
os.popen(command).read()可以获取命令执行结果,但是无法获取命令执行状态码
commands.getstatusoutput(command)返回一个元组(命令执行状态码, 命令执行结果)

说明:

  1. os.popen(command)函数得到的是一个文件对象,因此除了read()方法外还支持write()等方法,具体要根据command来定;
  2. commands模块只存在于Python 2.7中,且不支持windows平台,因此commands模块很少被使用。另外,commands模块实际上也是通过对os.popen()的封装来完成的。

函数名及描述:
os.system(command) 返回命令执行状态码,而将命令执行结果输出到屏幕;
os.popen(command).read() 可以获取命令执行结果但是无法获取命令执行状态码;
commands.getstatusoutput(command) 返回一个元组(命令执行状态码, 命令执行结果);
os.popen(command)函数得到的是一个文件对象,因此除了read()方法外还支持write()等方法,具体要根据command来定;
commands模块只存在于Python 2.7中,且不支持windows平台,因此commands模块很少被使用。另外,commands模块实际上也是通过对os.popen()的封装来完成的。

示例:

import os
retcode = os.system('dir')
import os
ret = os.popen('dir').read()
print(ret)

需要注意的是commands模块不支持windows平台,因此该实例是在Linux平台下执行的

import commands
retcode, ret = commands.getstatusoutput('ls -l')
retcode
0
print(ret)

 通过查看commands模块提供的属性可知,它也提供了单独获取命令执行状态码和执行结果的函数,如下所示:

dir(commands)
​
['__all__', '__builtins__', '__doc__', '__file__', '__name__', '__package__', 'getoutput', 'getstatus', 'getstatusoutput', 'mk2arg', 'mkarg']

 6.2.2 subprocess

DESCRIPTION
This module allows you to spawn processes, connect to their
input/output/error pipes, and obtain their return codes.

即允许你去创建一个新的进程让其执行另外的程序,并与它进行通信,获取标准的输入、标准输出、标准错误以及返回码等。 

subprocess – 创建附加进程 ,subprocess是Python 2.4中新增的一个模块,它允许你生成新的进程,连接到它们的 input/output/error 管道,并获取它们的返回(状态)码

subprocess模块提供了一种一致的方法来创建和处理附加进程,与标准库中的其它模块相比,提供了一个更高级的接口。用于替换如下模块: 
os.system() , os.spawnv() , os和popen2模块中的popen()函数,以及 commands().

subprocess模块中的常用函数

函数描述
subprocess.run()Python 3.5中新增的函数。执行指定的命令,等待命令执行完成后返回一个包含执行结果的CompletedProcess类的实例。
subprocess.call()执行指定的命令,返回命令执行状态,其功能类似于os.system(cmd)。
subprocess.check_call()Python 2.5中新增的函数。 执行指定的命令,如果执行成功则返回状态码,否则抛出异常。其功能等价于subprocess.run(..., check=True)。
subprocess.check_output()Python 2.7中新增的的函数。执行指定的命令,如果执行状态码为0则返回命令执行结果,否则抛出异常。
subprocess.getoutput(cmd)接收字符串格式的命令,执行命令并返回执行结果,其功能类似于os.popen(cmd).read()和commands.getoutput(cmd)。
subprocess.getstatusoutput(cmd)执行cmd命令,返回一个元组(命令执行状态, 命令执行结果输出),其功能类似于commands.getstatusoutput()。

说明:

  1. 在Python 3.5之后的版本中,官方文档中提倡通过subprocess.run()函数替代其他函数来使用subproccess模块的功能;
  2. 在Python 3.5之前的版本中,我们可以通过subprocess.call(),subprocess.getoutput()等上面列出的其他函数来使用subprocess模块的功能;
  3. subprocess.run()、subprocess.call()、subprocess.check_call()和subprocess.check_output()都是通过对subprocess.Popen的封装来实现的高级函数,因此如果我们需要更复杂功能时,可以通过subprocess.Popen来完成。
  4. subprocess.getoutput()和subprocess.getstatusoutput()函数是来自Python 2.x的commands模块的两个遗留函数。它们隐式的调用系统shell,并且不保证其他函数所具有的安全性和异常处理的一致性。另外,它们从Python 3.3.4开始才支持Windows平台。

6.2.3 使用示例

 

6.2.4 总结

  1. Python2.4版本引入了subprocess模块用来替换os.system()、os.popen()、os.spawn*()等函数以及commands模块;也就是说如果你使用的是Python 2.4及以上的版本就应该使用subprocess模块了。
  2. 如果你的应用使用的Python 2.4以上,但是是Python 3.5以下的版本,Python官方给出的建议是使用subprocess.call()函数。Python 2.5中新增了一个subprocess.check_call()函数,Python 2.7中新增了一个subprocess.check_output()函数,这两个函数也可以按照需求进行使用。
  3. 如果你的应用使用的是Python 3.5及以上的版本,Python官方给出的建议是尽量使用subprocess.run()函数
  4. 当subprocess.call()、subprocess.check_call()、subprocess.check_output()和subprocess.run()这些高级函数无法满足需求时,我们可以使用subprocess.Popen类来实现我们需要的复杂功能。

可以使用subprocess包来创建子进程,但这个包有两个很大的局限性:

1) 我们总是让subprocess运行外部的程序,而不是运行一个Python脚本内部编写的函数。

2) 进程间只通过管道进行文本交流。以上限制了我们将subprocess包应用到更广泛的多进程任务。(这样的比较实际是不公平的,因为subprocessing本身就是设计成为一个shell,而不是一个多进程管理包,而multiprocessing包是Python中的多进程管理包)

七、多线程和多进程的区别及多进程的优缺点

7.1 区别

多进程:允许多个任务同时进行,Process可以分布到多台机器上,而Thread最多只能分布到同一台机器的多个CPU上。
多线程:允许单个任务分成不同的部分运行     

python多线程有个限制:全局解释器锁(global interpreter; lock;),这个锁的意思是任一时间只能有一个线程使用解释器,跟单cpu跑多个程序一个意思,大家都是轮着用的,这叫“并发”,不是“并行”。手册上的解释是为了保证对象模型的正确性!这个锁造成的困扰是如果有一个计算密集型的线程占着cpu,其他的线程都得等着,试想你的多个线程中有这么一个线程,得多悲剧,多线程生生被搞成串行;当然这个模块也不是毫无用处,手册上又说了:当用于IO密集型任务时,IO期间线程会释放解释器,这样别的线程就有机会使用解释器了!所以是否使用这个模块需要考虑面对的任务类型。主要针对于IO密集型任务

multiprocessing也可以实现多线程:

from multiprocessing import Pool

如果把这个代码改成下面这样,就变成多线程实现concurrency

from multiprocessing.dummy import Pool

multiprocessing和threading共享API的区别: 

  • 在UNIX平台上,当某个进程终结之后,该进程需要被其父进程调用wait,否则进程成为僵尸进程(Zombie)。所以,有必要对每个Process对象调用join()方法 (实际上等同于wait)。对于多线程来说,由于只有一个进程,所以不存在此必要性。
  • multiprocessing提供了threading包中没有的IPC(比如Pipe和Queue),效率上更高。应优先考虑Pipe和Queue,避免使用Lock/Event/Semaphore/Condition等同步方式 (因为它们占据的不是用户进程的资源)。
  • 多进程应该避免共享资源。在多线程中,我们可以比较容易地共享资源,比如使用全局变量或者传递参数。在多进程情况下,由于每个进程有自己独立的内存空间,以上方法并不合适。此时我们可以通过共享内存和Manager的方法来共享资源。但这样做提高了程序的复杂度,并因为同步的需要而降低了程序的效率。

7.2 优缺点

多进程模式最大的优点就是稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。(当然主进程挂了所有进程就全挂了,但是Master进程只负责分配任务,挂掉的概率低)著名的Apache最早就是采用多进程模式

多进程模式的缺点是创建进程的代价大,在Unix/Linux系统下,用fork调用还行,在Windows下创建进程开销巨大。另外,操作系统能同时运行的进程数也是有限的,在内存和CPU的限制下,如果有几千个进程同时运行,操作系统连调度都会成问题。

多线程模式通常比多进程快一点,但是也快不到哪去,而且,多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。在Windows上,如果一个线程执行的代码出了问题,你经常可以看到这样的提示:“该程序执行了非法操作,即将关闭”,其实往往是某个线程出了问题,但是操作系统会强制结束整个进程。

在Windows下,多线程的效率比多进程要高,所以微软的IIS服务器默认采用多线程模式由于多线程存在稳定性的问题,IIS的稳定性就不如Apache。为了缓解这个问题,IIS和Apache现在又有多进程+多线程的混合模式,真是把问题越搞越复杂。

异步IO

考虑到CPU和IO之间巨大的速度差异,一个任务在执行的过程中大部分时间都在等待IO操作,单进程单线程模型会导致别的任务无法并行执行,因此,我们才需要多进程模型或者多线程模型来支持多任务并发执行。

现代操作系统对IO操作已经做了巨大的改进,最大的特点就是支持异步IO。如果充分利用操作系统提供的异步IO支持,就可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型,Nginx就是支持异步IO的Web服务器,它在单核CPU上采用单进程模型就可以高效地支持多任务。在多核CPU上,可以运行多个进程(数量与CPU核心数相同),充分利用多核CPU。由于系统总的进程数量十分有限,因此操作系统调度非常高效。用异步IO编程模型来实现多任务是一个主要的趋势。

对应到Python语言,单进程的异步编程模型称为协程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序。

八、总结

(1)在Unix/Linux下,multiprocessing模块封装了fork()调用,使我们不需要关注fork()的细节。由于Windows没有fork调用,因此,multiprocessing需要“模拟”出fork的效果,父进程所有Python对象都必须通过pickle序列化再传到子进程去。所以,如果multiprocessing在Windows下调用失败了,要先考虑是不是pickle失败了。

(2)在Unix/Linux下,可以使用fork()调用实现多进程。要实现跨平台的多进程,可以使用multiprocessing模块。其充分利用了多线程无法做到的利用多核设备的优势。

(3)进程间通信是通过Queue、Pipes等实现的。

(4)join函数会等待调用进程的结束,阻塞主进程。但其他子进程不受影响。线程的join函数也是这样,主线程将自我阻塞,然后等待th表示的线程执行完毕再结束。

(5)multiprocessing模块和threading模块很相似,用法基本相同。

(6)multiprocessing模块也可以用来编写多线程。如果是多线程的话,用multiprocessing.dummy即可,用法与multiprocessing基本相同。

(7)threading 和 multiprocessing 的一处区别是在 __main__ 中使用时的额外保护。由于进程已经启动,子进程需要能够导入包含目标函数的脚本。在 __main__ 中包装应用程序的主要部分,可确保在导入模块时不会在每个子项中递归运行它。另一种方法是从单独的脚本导入目标函数。

(8)主进程执行完毕后会默认等待子进程结束后回收资源,不需要手动回收资源;join()函数用来控制子进程结束的顺序,其内部也有一个清除僵尸进程的函数,可以回收资源。

(9)当子进程执行完毕后,会产生一个僵尸进程,其会被join函数回收,或者再有一条进程开启,start函数也会回收
僵尸进程,所以不一定需要写join函数。

(10)windows系统在子进程结束后会立即自动清除子进程的Process对象,而linux系统子进程的Process对象
如果没有join函数和start函数的话会在主进程结束后统一清除。

(11)当执行完p1 = Pool(5)这条代码后,5条进程已经被创建出来了,只是还没有为他们各自分配任务,也就是说,无论有多少任务,实际的进程数只有5条,计算机每次最多5条进程并行。

(12)当Pool中有进程任务执行完毕后,这条进程资源会被释放,pool会按先进先出的原则取出一个新的请求给空闲的进程继续执行;

(13)当Pool所有的进程任务完成后,会产生5个僵尸进程,如果主线程不结束,系统不会自动回收资源,需要调用join函数去回收。

(14)创建Pool池时,如果不指定进程最大数量,默认创建的进程数为系统的内核数量。

(15)close()terminate()的区别在于close()会等待池中的worker进程执行结束再关闭pool,而terminate()则是直接关闭。

(16),一些著名的科学计算库(如 numpy)为了提升性能,其底层也是用 C 实现的,并且会在做一些线程安全操作(如 numpy 的数组操作)时释放 GIL。因此对于这些库,我们可以放心地使用多线程。

(17)subprocess主要用于执行命令,用于交互式环境。

其他模块:

multiprocess模块采用dill来序列化并传递数据,避免了multiprocessing模块采用pickle的限制。multiprocess模块的接口与multiprocessing基本相同;部分函数、方法的传参不完全一样,不过,但对于通常应用情景不会有差别。

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值