进程
Python实现多进程的方式主要有两种,一种方法是使用os模块中fork方法,另一种方法是使用multiprocessing模块。
fork 适用于Unix/Linux操作系统,对Windows不支持
multiprocessing 支持跨平台
使用os模块中的fork方法实现多进程
Python的os模块封装了常见的系统调用,其中有fork方法。fork方法来自于Unix/Linux操作系统中提供的一个fork系统调用。
这个方法是调用一次,返回两次,因为系统将当前进程(父进程)复制一份进程(子进程),两个进程几乎完全相同,于是fork方法分别再父进程和子进程中返回。
子进程永远返回0,父进程中返回的子进程的ID。
import os
if __name__ == '__main__':
print('current Process (%s) start ...' % (os.getpid()))
pid = os.fork()
if pid < 0:
print('error in fork')
elif pid == 0:
print('I am child Process(%s) and my parent process is(%s)', (os.getpid()), (os.getppid()))
else:
print('I (%s) created a child process (%s).', (os.getpid()), pid)
使用multiprocessing模块创建多进程
multiprocessing模块提供了一个Process类来描述一个进程对象。创建子进程,只需要传入一个执行函数和函数的参数,即可完成一个Process实例的创建,用start()方法启动进程,用join()方法实现进程间同步。
import multiprocessing
def run_proc(name):
print('Child process %s (%s) Running...' % (name, os.getpid()))
if __name__ == '__main__':
print('Parent process %s.' % os.getpid())
for i in range(5):
p = Process(target=run_proc, args=(str(i),))
print('Process will start')
p.start()
p.join()
print('Process end.')
以上创建进程的两种方法,如果只是需要少量的进程,可以直接手工创建,如果需要大量的进程,就需要进程池Poll发挥作用。
Pool对象调用 join() 方法会等待所有子进程执行完毕,调用 join() 之前必须先调用close(), 调用close()之后就不能继续添加新的Process了。
import os, time, random
from multiprocessing import Pool
def run_task(name):
print('Task %s (pid = %s) is running...' % (name, os.getpid()))
time.sleep(random.random() * 3)
print('Task %s end.' % name)
if __name__ == '__main__':
print('Current process %s.' % os.getpid())
p = Pool(processes=3)
for i in range(5):
p.apply_async(run_task, args=(i, ))
print('Waiting for all subprocesses done...')
p.close()
p.join()
print('All subprocesses done.')
进程间通信
如果创建大量的进程,那么进程间通信是必不可少的。
Python提供了多种进程间通信的方式,例如Queue, Pipe, Value+Array等。
Queue 用来多个进程间实现通信
Pipe 常用再两个进程间通信
Queue通信方式,Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。Put和Get可以进行操作。
Put方法用以插入数据到队列中,有两个参数:blocked和timeout。
如果blocked为True(默认值),并且timeout为正值,该方法会阻塞timeout指定的事件,直到该队列有剩余的空间。
如果超时,会抛出Queue.Full异常。如果blocked为False,但该Queue已满,会立即抛出Queue.Full异常。
Get方法可以从队列读取并删除一个元素,有两个参数:blocked和timeout
如果blocked为True(默认值),并且timeout为正值,那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。
如果blocked为False,如果Queue有一个值可用,则立即返回该值;否则,如果队列为空,则立即抛出Queue.Empty异常。
import os, time, random
from multiprocessing import Process, Queue
def proc_read(q):
print('Process(%s) is reading...' % os.getpid())
while True:
url = q.get(True)
print('Get %s from queue' % url)
if __name__ == '__main__':
# 父进程创建Queue,并传给各个子进程
q = Queue()
proc_write1 = Process(target=proc_write, args=(q, ['url_1', 'url_2', 'url_3']))
proc_write2 = Process(target=proc_write, args=(q, ['url_4', 'url_5', 'url_6']))
proc_reader = Process(target=proc_read, args=(q,))
# 启动子进程proc_writer 写入
proc_write1.start()
proc_write2.start()
# 启动子进程proc_reader 读取
proc_reader.start()
# 等待 proc_writer结束
proc_write1.join()
proc_write2.join()
# proc_reader进程里是死循环,无法等待其结束,只能强行终止
proc_reader.terminate()
Pipe通信机制,Pipe常用来在两个进程间进行通信,两个进程分别位于管道的两端。
Pipe方法返回(conn1, conn2)代表一个管道的两个端。Pipe方法有duplex参数。
如果duplex参数为True(默认值),那么这个管道是全双工模式,也就是说conn1 和 conn2均可收发。
若duplex为False,conn1值负责接受消息,conn2值负责发送信息。
send和recv方法分别是发送和接受信息的方法。
如果没有消息可接收,recv方法会一致阻塞。如果管道已经被关闭,那么recv方法会抛出错误
多线程
多线程类似于同时执行多个不同程序,多线程运行有如下优点:
可以把运行时间长的任务放到后台区处理
程序的运行速度可能加快
一些需要等待的任务上,线程就比较有用。
Python的标准库提供了两个模块:thread 和 threading, 主要用threading
threading模块创建多线程
1.把一个函数传入并创建Thread实例,然后调用start方法。
2.直接从threading.Thread继续并创建线程类,然后重写__init__方法和run方法。
import time, random
import threading
def proc_send(pipe, urls):
for url in urls:
print('Process(%s) send: %s' %(os.getpid(), url))
pipe.send(url)
time.sleep(random.random())
def proc_rcov(pipe):
while True:
print('Process(%s) rev:%s' %(os.getpid(), pipe.recv()))
time.sleep(random.random())
if __name__ == '__main__':
pipe = multiprocessing.Pipe()
p1 = Process(target=proc_send, args=(pipe[0], ['url_' + str(i) for i in range(10)]))
p2 = Process(target=proc_rcov, args=(pipe[1],))
p1.start()
p2.start()
p1.join()
p2.join()
class myThread(threading.Thread):
def __init__(self, name, urls):
threading.Thread.__init__(self, name=name)
self.urls = urls
def run(self):
print('Current %s is running...' % threading.current_thread().name)
for url in self.urls:
print('%s --->>> %s' % (threading.current_thread().name, url))
time.sleep(random.random())
print('%s ended.' % threading.current_thread().name)
if __name__ == '__main__':
print('%s is running...' % threading.current_thread().name)
t1 = myThread(name='Thread_1', urls=['url_1', 'url_2', 'url_3'])
t2 = myThread(name='Thread_2', urls=['url_4', 'url_5', 'url_6'])
t1.start()
t2.start()
t1.join()
t2.join()
print('%s ended.' % threading.current_thread().name)
线程同步
如果多个线程共同对某个数据修改,为了保证数据的正确性,需要对多个线程进行同步。
使用Thread对象的Lock和RLock可以实现简单的线程同步。
这个两个对象都有acquire方法和release方法。
Lock对象,如果一个线程连续两次进行acquire操作,那么由于第一次acquire之后没有release,第二次acquire将挂起线程。这会使线程死锁。
RLock对象允许一个线程多次对其进行acquire操作,因为在其内部通过一个counter变量维护这线程acquire的次数。每一次的acquire操作必须有一个release操作与之对应。
import threading
myLock = threading.RLock()
num = 0
class myThread(threading.Thread):
def __init__(self, name):
threading.Thread.__init__(self, name=name)
def run(self):
global num
while True:
myLock.acquire()
print('%s locked, Number: %d' % (threading.current_thread().name, num))
if num >= 4:
myLock.release()
print('%s released, Number: %d' % (threading.current_thread().name, num))
break
num += 1
print('%s released, Number: %d' % (threading.current_thread().name, num))
myLock.release()
if __name__ == '__main__':
thread1 = myThread('Thread_1')
thread2 = myThread('Thread_2')
thread1.start()
thread2.start()
协程
又称微线程,是一种用户级的轻量级线程。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,切换回来时候就恢复先前保存的寄存器上下文和栈。
因此协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态。
在并发中,协程与线程类似,每个协程表示一个执行单元,有自己的本地数据,与其他协程共享全局数据和其他资源.
协程需要用户自己来编写调度逻辑,对于CPU来说,协程其实时单元程,所以CPU不用区考虑怎么调度,切换上下文,省去了CPU的切换开销,所以协程在一定程度上又好于多线程。
Python通过yield提供了对协程的基本支持,但是不完全,而使用第三方gevent提供了比较完善的协程支持。
greenlet在libev事件循环顶部提供了一个有高级别并发性的API:
特征:
基于libev的快速事件循环,Linux上时epoll机制。
基于greenlet的轻量级执行单元
API复用了Python标准库里的内容。
支持SSL的协作式sockets。
可通过线程池或 c-ares实现DNS查询
通过monkey patching功能使得第三方模块变成协作式。
gevent对协程的支持,本质上是greenlet的实现切换工作。
greenlet工作流程如下:
加入进行访问网络的IO操作时,出现阻塞,greenlet就显式切换到另一段没有被阻塞的代码段执行,知道原先的阻塞状态消失以后,再自动切换回原来的代码段继续处理。greenlet式一种合理安排的串行方式。
由于IO操作非常耗时,而不是等待IO,这就是协程一般比多线程效率高的原因。由于切换是再IO操作时自动完成,所以gevent需要修改Python自带的一些标准库,将一些常见的阻塞,如socket,select等地方实现协程跳转,
这一过程在启动时通过monkey patch完成。
import gevent
from gevent.pool import Pool
import urllib3
def run_task(url):
print('Visit -- > %s' % url)
try:
http = urllib3.PoolManager()
response = http.request('GET', url)
data = response.data
print('%d bytes received from %s.' % (len(data), url))
except Exception as e:
print(e)
if __name__ == '__main__':
urls = ['https://github.com/', 'https://python.org/', 'http://www.cnblogs.com/']
greenlets = [gevent.spawn(run_task, url) for url in urls]
gevent.joinall(greenlets)
主要用了gevent和spawn方法和joinall方法。spawn方法可以看做是用来形成协程,joinall方法就是添加这些协程任务,并且启动运行。从运行结果来看,3个网络操作是并发执行的,而且顺序不同,但其实只有一个线程。
gevent中还提供了对池的支持,当拥有动态数量的greenlet需要进行并发管理(限制并发数)时,就可以使用池,这在处理大量的网络和IO操作时是非常需要的。
import gevent
from gevent.pool import Pool
import urllib3
def run_task(url):
print('Visit --> %s' % url)
try:
http = urllib3.PoolManager()
response = http.request('GET', url)
data = response.data
print('%d bytes received from %s.' % (len(data), url))
except Exception as e:
print(e)
return 'url:%s ---->finish' % url
if __name__ == '__main__':
pool = Pool(2)
urls = ['https://github.com/', 'https://python.org/', 'http://www.cnblogs.com/']
results = pool.map(run_task, urls)
print(results)