Python多任务处理

进程

multiprocessing

multiprocessing模块提供了一个Process类来代表一个进程对象:

from multiprocessing import Process
import os

def run(name):
    print('sub process %s : %d' % (name, os.getpid()) )

if __name__ == '__main__':
    pro = Process(target = run, args = ('name',))
    pro.start()
    pro.join()
    print('main process: %d' % os.getpid() )

在创建线程对象时,将函数对象与target参数绑定作为新进程的主函数。调用进程对象的start方法,start方法将创建新进程并调用其target函数开始运行。

调用进程对象pro的join()方法,将使调用进程(主进程)等待进程pro运行完成后继续运行。

Pool

进程池Pool可以批量管理进程,Pool的构造函数接受一个int值作为最大进程数,默认为计算机逻辑核心数。

from multiprocessing import Process
from multiprocessing import Pool
import os

def run():
    print('sub process: %d' % os.getpid() )

if __name__ == '__main__':
    pool = Pool(4);
    for i in range(0,4):
        pool.apply_async(run)
    #向进程池中添加函数对象作为子进程的主函数

    pool.close()
#close()之后进程池中不能添加新的进程
#只有close()之后才能调用join()方法
    pool.join()
    #join()使得在进程池中所有进程都执行完毕后,主调进程继续执行
        print('process: %d' % os.getpid() )

fork

Unix/Linux操作系统提供了一个fork()系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。

子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。

Python的os模块封装了常见的系统调用,其中就包括fork,可以在Python程序中轻松创建子进程。由于Windows没有fork调用,该方法无法在Windows上运行。

subprocess

subprocess模块用于调用外部进程:

import subprocess
r = subprocess.call(['ping','www.python.org'])

subprocess.call方法接受一个列表作为参数,并将其作为命令行发送给OS并返回外部进程的返回值。上述调用,等价于在终端中输入ping www.python.org

进程间通信

Python提供了多种进程间通信机制,最常用的是Queue和Pipe:

from multiprocessing import Process,Pipe
import os

def fun(conn):
    conn.send(['msg:',os.getpid()]);
    conn.close();

if __name__ == '__main__':
    (sendConn,recvConn) = Pipe();
    pro = Process(target=fun,args=(sendConn,))
    pro.start();
    pro.join();
    print(recvConn.recv());

Pipe()返回一对相互连接的 对象,每个对象都具有发送send()和recv()方法,一个对象通过send()发送字符串,与其关联的对象使用recv()方法可以获得该消息。

Queue是python中的队列,为了在进程通信中使用一般采用其派生类JoinableQueue。

import multiprocessing

q = multiprocessing.Queue()
 
def readerProc():
    print(q.get())

if __name__ == '__main__':
    reader = multiprocessing.Process(target=readerProc)
    reader.start()
    q.put(100)
    reader.join()

分布式进程

线程与锁

Python标准库提供了_thread模块作为线程的底层实现,并提供了高级模块threading对_thread进行了封装。通常情况下可以使用高级模块threading,其语法与Process类似:

import threading

def run():
    print('current thread:%s' % threading.current_thread().name);

t = threading.Thread(target=run,name='new_thread');
t.start();
t.join();
print('current thread:%s' % threading.current_thread().name);

输出:

current thread:new_thread
current thread:MainThread

每个进程至少拥有一个线程,Python主进程的默认线程称为MainThread,threading模块的current_thread()方法将返回当前线程对象。

LOCK

线程与进程的区别在于进程拥有自己的内存空间,而同一个进程下的线程需要共享内存空间。对一个对象的修改一般是由多条机器指令完成的,多线程模式下操作系统调度不同线程的指令交替执行,可能出现一个线程对对象的修改没有完成,而另一个线程却在此时访问该对象造成错误。即使对对象的访问/修改具有原子性(操作不可分割,完成前不会有另外的操作执行),竞争多任务的机制也使得线程无法判断另外的线程是否修改了对象。为了防止可能错误的发生引入锁的机制。

import threading

shared = 0;
lock = threading.Lock();

def vary():
    global shared 
    # Declare "shared" is a global object instead of a local object
    shared = shared + 1;

def run():
    lock.acquire();
    try:
        vary();
        print('new_thread %d\n' % shared);
    except Exception, e:
        raise
    finally:
        lock.release();
        
t1 = threading.Thread(target=run,name='new_thread',);
t1.start();
t1.join();
print('MainThread %d' % shared);

获取锁之后必须释放,否则会造成死锁。global关键字声明对象为全局对象,Python将首先在全局作用域内搜索该变量,若失败才会在局部作用域内搜索或者建立局部对象。这里的局部是相对模块-函数而言的而非进程-线程,即其它线程调用该函数时仍然可以访问或修改同一个对象。

ThreadLocal

ThreadLocal机制使得线程可以建立自己的局部变量不与其它线程共享,ThreadLocal对象是全局的(线程共享),其每一个属性都是一个词典以线程ID作为key进行取值。

import threading

def run(name):
    local_name.name = name;
    print('Hello, %s' % local_name.name);


local_name = threading.local();
t1 = threading.Thread(target=run,args=('World',));
t2 = threading.Thread(target=run,args=('Home',));
t1.start();
t2.start();

GIL

GIL(Global Interpreter Lock)简单地说就是每一个只能同时仅有一个线程来执行, 获得相关的锁, 存取相关的资源,GIL不是Python自身的特性而是Python的主要解释器CPython中的机制。GIL的存在使得CPython多线程程序不可能真正并发,不能真正利用多核的计算能力。

此外,CPython会计算当前已执行的字节码数量(opcode计数),达到一定阈值后就强制释放GIL。而这时也会触发一次操作系统的线程调度(当然是否真正进行上下文切换由操作系统自主决定)。这种模式在只有一个CPU核心的情况下毫无问题。任何一个线程被唤起时都能成功获得到GIL(因为只有释放了GIL才会引发线程调度)。因为GIL释放和获取间隔较短而唤醒线程间隔较长,在多核情况下,原来持有GIL的线程可能再次获得GIL,运行在其它核心上的线程唤醒后却得不到GIL,白白浪费了CPU。

GIL使得CPython多线程程序效率下降严重但却确保了单线程程序的执行效率。如果确实需要多任务处理(如IO密集任务)可以考虑多进程与异步调用,如果需要多核的计算能力优势可以选择其它语言。

协程与异步调用

多线程与多进程模型虽然解决了并发问题,但是线程数量不能过多且在线程间调度有很大开销导致运行效率下降。

协程(Coroutine),又称微线程,纤程。Python对协程的支持是通过generator实现的。传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待。如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产。

def consumer():
    r = ''
    while True:
        n = yield r
        if not n:
            return
        print('Consuming %s' % n)
        r = 'OK'

def produce(c):
    c.send(None)
    n = 0
    while n < 5:
        n = n + 1
        print('Producing %s' % n)
        r = c.send(n)
        print('Consumer return: %s' % r)
    c.close()

c = consumer()
produce(c)

输出:

Producing 1
Consuming 1
Consumer return: OK
Producing 2
Consuming 2
Consumer return: OK
Producing 3
Consuming 3
Consumer return: OK
Producing 4
Consuming 4
Consumer return: OK
Producing 5
Consuming 5
Consumer return: OK

注意到consumer函数是一个generator,把一个consumer传入produce后:

  1. 首先调用c.send(None)启动生成器;

  2. 然后,一旦生产了东西,通过c.send(n)切换到consumer执行;

  3. consumer通过yield拿到消息,处理,又通过yield把结果传回;

  4. produce拿到consumer处理的结果,继续生产下一条消息;

  5. produce决定停止生产,通过c.close()关闭consumer,整个过程结束。

整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。

async

async / await 是Python3.5中引入的新语法使得coroutine语法可读性更高。asyncio是Python3.4中引入的模块,通过事件循环对corotnue提供支持。

asyncio内建一个事件循环(eventloop),事件循环在GUI编程中经常使用。单线程模式下,事件循环通过消息队列监测新消息的产生,并将消息分发给处理函数,然后等待处理函数执行。

当处理函数func遇到一个耗时操作时,将向事件循环发送一条消息(表示需要等待耗时操作)然后挂起等待。事件循环继续执行,开始处理消息队列中的剩余消息。

当事件循环收到耗时操作完成的消息后,将唤醒func的消息加入消息队列。当唤醒操作出队时,事件循环将唤醒func继续执行。

ayncio的面向事件机制使得主线程并未等待耗时操作,使得各事件处理函数几乎无间隔地交替执行(即并发并非并行)。耗时操作交由异步API的提供者在另外的线程中完成。

# python3.5
import threading
import asyncio

async def hello():
    print('Hello world! (%s)' % threading.currentThread())
    await asyncio.sleep(1)
    print('Hello again! (%s)' % threading.currentThread())

loop = asyncio.get_event_loop()
tasks = [hello(), hello()]
loop.run_until_complete(asyncio.wait(tasks))
# start event loop
loop.close()

输出:

Hello world! (<_MainThread(MainThread, started 7984)>)
Hello world! (<_MainThread(MainThread, started 7984)>)
Hello again! (<_MainThread(MainThread, started 7984)>)
Hello again! (<_MainThread(MainThread, started 7984)>)
({<Task finished coro=<hello() done, defined at <stdin>:1> result=None>, <Task finished coro=<hello() done, defined at <stdin>:1> result=None>}, set())

上述代码中耗时操作asyncio.wait(1)在另外的线程中完成。两个hello()实现了并发执行,并未等待耗时操作完成。

你可能感兴趣的:(Python多任务处理)