fork()方法:Unix/Linux操作系统提供了一个 fork() 系统调用,它非常特殊。 普通的函数调用,调用一次,返回一次, 但是 fork() 调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。参见《UNIX坏境高级编程》
multiprocessing:
由于Python是跨平台的, 自然也应该提供一个跨平台的多进程支持。multiprocessing 模块就是跨平台版本的多进程模块。
from multiprocessing import Process
import os
def run_proc(name):
print 'Run child process %s (%s)'%(name,os.getpid())
if __name__=='__main__':
print 'parent child process %s'% os.getpid()
p = Process(target=run_proc,args=('test',))
print 'Process will start'
p.start()
p.join()
print 'Process end'
Pool:
如果要启动大量的子进程, 可以用进程池的方式批量创建子进程:
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 5s 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 subprocess done...'
p.close()
p.join()
print 'All subprocess done'
对Pool对象调用join()方法会等待所有子进程执行完毕,调用 join() 之前必须先调用 close() ,调用 close() 之后就不能继续添加新的Process了。
Process 之间肯定是需要通信的, 操作系统提供了很多机制来实现进程间的通信。 Python的 multiprocessing 模块包装了底层的机制,提供了Queue、Pipes等多种方式来交换数据。
以 Queue 为例, 在父进程中创建两个子进程,一个往 Queue 里写数据,一个从 Queue 里读数据:
#!/usr/bin
# -*- coding:utf8 -*-
from multiprocessing import Process,Queue
import os, time, random
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 = a.get(True)
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()
pr.start()
pw.join()
# pr进程里是死循环, 无法等待其结束, 只能强行终止
pr.terminate()
在Unix/Linux下, 可以使用 fork() 调用实现多进程。要实现跨平台的多进程, 可以使用 multiprocessing模块。进程间通信是通过 Queue 、Pipes 等实现的。
Python的标准库提供了两个模块:thread 和threading,thread 是低级模块, threading是高级模块,对 thread进行了封装。 绝大多数情况下,我们只需要使用 threading 这个高级模块。
启动一个线程就就是把一个函数传入并创建Thread实例,然后调用start()开始执行。
多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改。
线程之间共享数据最大的危险在于多个线程同时改一个变量, 把内容给改乱了。
因为Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。
这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。
不过,也不用过于担心,Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。 多个Python进程有各自独立的GIL锁,互不影响。
多线程编程,模型复杂,容易发生冲突,必须用锁加以隔离,同时,又要小心死锁的发生(创建一个锁就是通过 threading.Lock() 来实现)。Python解释器由于设计时有GIL全局锁,导致了多线程无法利用多核。
ThreadLocal
一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁,
但是局部变量也有问题,就是在函数调用的时候,传递起来很麻烦,ThreadLocal应运而生。
# create global ThreadLocal object
local_school = threading.local()
def process_student(name):
std = local_school.student
print 'Hello, %s (in %s)' % (std, threading.current_thread().name)
def process_thread(name):
local_school.student = name
process_student()
t1 = threading.Thread(target= process_thread, args=('Alice',), name='Thread-A')
t2 = threading.Thread(target= process_thread, args=('Bob',), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()
多线程模式通常比多进程快一点, 但是也快不到哪去,而且,多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。 无论是多进程还是多线程,只要数量一多,效率肯定上不去。
我们可以把任务分为计算密集型和IO密集型。计算密集型任务的特点是要进行大量的计算,消耗CPU资源,计算密集型任务由于主要消耗CPU资源,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。
第二种任务的类型是IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。
如果充分利用操作系统提供的异步IO支持,就可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型。对应到python而言,单进程的异步模型成为携程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序。
在Thread和Process中,应当优先选择Process,因为Process更稳定,Process可以分布到不同机器上,而Thread最多只能分布到同一台机器的多个cpu上。
python的multiprocessing模块不但支持多进程,其中managers子模块还支持把多进程分布到多台机器上。一个服务进程可以作为调度者,将任务分布到其他多个进程中,依靠网络通信。
通过Queue通信的多进程程序,希望把发送任务的进程和处理任务的进程分布到两台机器上。怎么用分布式进程实现?
通过managers模块把Queue通过网络暴露出去,就可以让其他机器的进程访问Queue了,服务进程负责启动Queue,把Queue注册到网络上,然后往Queue里面写入任务。
当我们在一台机器上写多进程程序时,创建的 Queue可以直接拿来用,但是,在分布式多进程环境下,添加任务到Queue不可以直接对原始的task_queue进行操作,那样就绕过了QueueManager的封装, 必须通过manager.get_task_queue()获得的Queue接口添加。
# taskmanager
import random, time, Queue
from multiprocessing.managers import BaseManager
task_queue = Queue.Queue()
result_queue = Queue.Queue()
# BaseManager QueueManager
class QueueManager(BaseManager):
pass
# 把两个Queue都注册到网络上, callable参数关联了Queue对象
QueueManager.register('get_task_queue',callable=lambda:task_queue)
QueueManager.register('get_result_queue',callabel=lambda: result_queue)
# 绑定端口5000, 设置验证码'abc':
manager = QueueManager(address=('', 5000), authkey=b'abc')
# 启动Queue
manager.start()
# 获得通过网络访问的Queue对象:
task = manager.get_task_queue()
result = manager.get_result_queue()
# 放几个任务进去:
for i in range(10):
n = random.randint(0,10000)
print('Put task %d...'% n)
task.put(n)
print('Try get result...')
for i in range(10):
r = result.get(timeout=10)
print('Result:%s' % r)
manager.shutdown()
print('master exit.')
任务进程要通过网络连接到服务进程, 所以要指定服务进程的IP。
# taskworker
import time, sys, Queue
from multiprocessing.managers import BaseManager
# 创建类似的QueueManager:
class QueueManager(BaseManager):
pass
# 由于这个QueueManager只从网络上获取Queue, 所以注册时只提供名字:
QueueManager.register('get_task_queue')
QueueManager.register('get_result_queue')
# 连接到服务器, 也就是运行taskmanager的机器:
server_adrr = '127.0.0.1'
print('Connect to server %s...' % server_adrr)
# 端口和验证码注意保持与taskmanager设置的完全一致
m = QueueManager(address=(server_adrr,5000),authkey='abc')
# 从网络连接
m.connect()
# 获取Queue的对象:
task = m.get_task_queue()
result = m.get_result_queue()
for i in range(10):
try:
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.')
这个简单的Manager/Worker模型有什么用,其实这就是一个简单但真正的分布式计算,把代码稍加改造,启动多个worker, 就可以把任务分布到几台甚至几十台机器上。
而Queue之所以能够通过网络访问,就是通过QueueManager实现的。由于QueueManager不止管理一个Queue,所以
要给每一个Queue的网络调用起个名字,比如,get_task_queue。
Python的分布式进程接口简单,封装良好,适合需要把繁重任务分布到多台机器的环境下。
python通过yield提供了对协程的基本支持,但是不完全,第三方的gevent对python提供了比较完善的协程支持。
gevent是第三方库,通过greenlet实现协程,其基本思想是: 当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有gevenlet在执行,而不是等待IO。
由于切换时在IO操作时自动完成的,所以gevent需要修改python自带的一些标准库,这一过程在启动时通过monkey patch完成。
# -*- coding: UTF-8 -*-
from gevent import monkey
monkey.patch_socket()
import gevent
from gevent import greenlet
def f(n):
for i in range(n):
print gevent.getcurrent() , i
g1 = gevent.spawn(f,5)
g2 = gevent.spawn(f,5)
g3 = gevent.spawn(f,5)
g1.join()
g2.join()
g3.join()
要让greenlet交替进行,可以通过gevent.sleep()交出控制权
def f(n):
for i in range(n):
print gevent.getcurrent(), i
gevent.sleep(0)
实际代码里,我们不会用gevent.sleep()去切换协程,而是在执行到IO操作时,gevent自动切换,代码如下:
from gevent import monkey
from gevent import greenlet
monkey.patch_all()
import gevent
import urllib2
def f(url):
print('Get:%s' % url)
resp = urllib2.urlopen(url)
data = resp.read()
print('%d bytes received from %s' % (len(data)), url)
gevent.joinall( [
gevent.spawn(f,'https://www.python.org'),
gevent.spawn(f,'https://www.yahoo.com'),
gevent.spawn(f,'https://github.com/'),
] )
gevent是基于IO切换的协程,使用gevent,可以获得极高的并发性能,但gevent只能在Unix/Linux下运行,在Windows下不保证正常安装和运行。