前言:并发编程在当前的软硬件系统架构下,是一个程序员必备的知识技能。本文希望通过整理网上资料、结合自己的经验,提供一个系列分享,将基本的并发概念解释清楚。并希望在此基础上有所扩展,将各种语言的现状也所有对比。
一、并发编程漫谈之 基本概念
二、并发编程漫谈之 python多线程和多进程
三、并发编程漫谈之 协程的演化
本文整理自 Python编程时光
一、创建多线程的几种方法
Python创建多线程主要有如下两种方法: - 函数 - 类
1.1 用函数创建多线程
在Python3中,Python提供了一个内置模块 threading.Thread,可以很方便地让我们创建多线程。
threading.Thread() 一般接收两个参数:
- 线程函数名:要放置线程让其后台执行的函数,由我们自已定义,注意不要加();
- 线程函数的参数:线程函数名所需的参数,以元组的形式传入。若不需要参数,可以不指定。
举个例子
mport time
from threading import Thread
# 自定义线程函数。
def main(name="Python"):
for i in range(2):
print("hello", name)
time.sleep(1)
# 创建线程01,不指定参数
thread_01 = Thread(target=main)
# 启动线程01
thread_01.start()
# 创建线程02,指定参数,注意逗号
thread_02 = Thread(target=main, args=("MING",))
# 启动线程02
thread_02.start()
output:
hello Python
hello MING
hello Python
hello MING
1.2 用类创建多线程
自定义一个类,对于这个类有两点要求:
- 必须继承 threading.Thread 这个父类;
- 必须覆写 run 方法。
这里的 run 方法,和我们上面线程函数的性质是一样的,可以写我们的业务逻辑程序。在 start() 后将会调用。
import time
from threading import Thread
class MyThread(Thread):
def __init__(self, name="Python"):
# 注意,super().__init__() 一定要写
# 而且要写在最前面,否则会报错。
super().__init__()
self.name=name
def run(self):
for i in range(2):
print("hello", self.name)
time.sleep(1)
if __name__ == '__main__':
# 创建线程01,不指定参数
thread_01 = MyThread()
# 创建线程02,指定参数
thread_02 = MyThread("MING")
thread_01.start()
thread_02.start()
output:
hello Python
hello MING
hello Python
hello MING
1.3 Thread 属性
t=Thread(target=func)
# 启动子线程
t.start()
# 阻塞子线程,待子线程结束后,再往下执行
t.join()
# 判断线程是否在执行状态,在执行返回True,否则返回False
t.is_alive()
t.isAlive()
# 设置线程是否随主线程退出而退出,默认为False
t.daemon = True
t.daemon = False
# 设置线程名
t.name = "My-Thread"
二、线程锁
具体线程锁,可以参考 并发编程漫谈之 基本概念
三、线程消息通信机制
要实现对多个线程进行控制,其实本质上就是消息通信机制在起作用,利用这个机制发送指令,告诉线程,什么时候可以执行,什么时候不可以执行,执行什么内容。
经过我的总结,线程中通信方法大致有下面几种:
- threading.Event
- threading.Condition
- queue.Queue 先进先出队列
- queue.LifoQueue 后进先出队列
- queue.PriorityQueue 带优先级的队列
3.1 Event事件
Python提供了非常简单的通信机制 Threading.Event,通用的条件变量。多个线程可以等待某个事件的发生,在事件发生后,所有的线程都会被激活。
event = threading.Event()
# 重置event,使得所有该event事件都处于待命状态
event.clear()
# 等待接收event的指令,决定是否阻塞程序执行
event.wait()
# 发送event指令,使所有设置该event事件的线程执行
event.set()
举个例子:
import time
import threading
class MyThread(threading.Thread):
def __init__(self, name, event):
super().__init__()
self.name = name
self.event = event
def run(self):
print('Thread: {} start at {}'.format(self.name, time.ctime(time.time())))
# 等待event.set()后,才能往下执行
self.event.wait()
print('Thread: {} finish at {}'.format(self.name, time.ctime(time.time())))
threads = []
event = threading.Event()
# 定义五个线程
[threads.append(MyThread(str(i), event)) for i in range(1,5)]
# 重置event,使得event.wait()起到阻塞作用
event.clear()
# 启动所有线程
[t.start() for t in threads]
print('等待5s...')
time.sleep(5)
print('唤醒所有线程...')
event.set()
output:
Thread: 1 start at Sun May 13 20:38:08 2018
Thread: 2 start at Sun May 13 20:38:08 2018
Thread: 3 start at Sun May 13 20:38:08 2018
Thread: 4 start at Sun May 13 20:38:08 2018
等待5s...
唤醒所有线程...
Thread: 1 finish at Sun May 13 20:38:13 2018
Thread: 4 finish at Sun May 13 20:38:13 2018
Thread: 2 finish at Sun May 13 20:38:13 2018
Thread: 3 finish at Sun May 13 20:38:13 2018
3.2 Condition
Condition和Event 是类似的,并没有多大区别。
cond = threading.Condition()
# 类似lock.acquire()
cond.acquire()
# 类似lock.release()
cond.release()
# 等待指定触发,同时会释放对锁的获取,直到被notify才重新占有琐。
cond.wait()
# 发送指定,触发执行
cond.notify()
举个网上一个比较趣的捉迷藏的例子来看看
import threading, time
class Hider(threading.Thread):
def __init__(self, cond, name):
super(Hider, self).__init__()
self.cond = cond
self.name = name
def run(self):
time.sleep(1) #确保先运行Seeker中的方法
self.cond.acquire()
print(self.name + ': 我已经把眼睛蒙上了')
self.cond.notify()
self.cond.wait()
print(self.name + ': 我找到你了哦 ~_~')
self.cond.notify()
self.cond.release()
print(self.name + ': 我赢了')
class Seeker(threading.Thread):
def __init__(self, cond, name):
super(Seeker, self).__init__()
self.cond = cond
self.name = name
def run(self):
self.cond.acquire()
self.cond.wait()
print(self.name + ': 我已经藏好了,你快来找我吧')
self.cond.notify()
self.cond.wait()
self.cond.release()
print(self.name + ': 被你找到了,哎~~~')
cond = threading.Condition()
seeker = Seeker(cond, 'seeker')
hider = Hider(cond, 'hider')
seeker.start()
hider.start()
output:
hider: 我已经把眼睛蒙上了
seeker: 我已经藏好了,你快来找我吧
hider: 我找到你了 ~_~
hider: 我赢了
seeker: 被你找到了,哎~~~
3.3 Queue队列
从一个线程向另一个线程发送数据最安全的方式可能就是使用 queue 库中的队列了。创建一个被多个线程共享的 Queue 对象,这些线程通过使用put() 和 get() 操作来向队列中添加或者删除元素。
from queue import Queue
# maxsize默认为0,不受限
# 一旦>0,而消息数又达到限制,q.put()也将阻塞
q = Queue(maxsize=0)
# 阻塞程序,等待队列消息。
q.get()
# 获取消息,设置超时时间
q.get(timeout=5.0)
# 发送消息
q.put()
# 等待所有的消息都被消费完
q.join()
# 以下三个方法,知道就好,代码中不要使用
# 查询当前队列的消息个数
q.qsize()
# 队列消息是否都被消费完,True/False
q.empty()
# 检测队列里消息是否已满
q.full()
举个老师点名的例子。
from queue import Queue
from threading import Thread
import time
class Student(Thread):
def __init__(self, name, queue):
super().__init__()
self.name = name
self.queue = queue
def run(self):
while True:
# 阻塞程序,时刻监听老师,接收消息
msg = self.queue.get()
# 一旦发现点到自己名字,就赶紧答到
if msg == self.name:
print("{}:到!".format(self.name))
class Teacher:
def __init__(self, queue):
self.queue=queue
def call(self, student_name):
print("老师:{}来了没?".format(student_name))
# 发送消息,要点谁的名
self.queue.put(student_name)
queue = Queue()
teacher = Teacher(queue=queue)
s1 = Student(name="小明", queue=queue)
s2 = Student(name="小亮", queue=queue)
s1.start()
s2.start()
print('开始点名~')
teacher.call('小明')
time.sleep(1)
teacher.call('小亮')
output:
开始点名~
老师:小明来了没?
小明:到!
老师:小亮来了没?
小亮:到!
需要注意的是,Queue不是广播,只要有人从Queue中取出了数据,其他人将无法获取。举个例子,如果修改一下上面老师点名的顺序,结果就会不同:
queue = Queue()
teacher = Teacher(queue=queue)
s1 = Student(name="小明", queue=queue)
s2 = Student(name="小亮", queue=queue)
s1.start()
s2.start()
print('开始点名~')
teacher.call('小亮')
time.sleep(1)
teacher.call('小明')
output:
开始点名~
老师:小明来了没?
老师:小亮来了没?
3.4 小结
Event 和 Condition 是threading模块原生提供的模块,原理简单,功能单一,它能发送 True 和 False 的指令,所以只能适用于某些简单的场景中。
而Queue则是比较高级的模块,它可能发送任何类型的消息,包括字符串、字典等。其内部实现其实也引用了Condition模块(譬如put和get函数的阻塞),正是其对Condition进行了功能扩展,所以功能更加丰富,更能满足实际应用。
四、线程本地数据
在Python中,其提供了threading.local这个类,可以很方便的控制变量的隔离,即使是同一个变量,在不同的线程中,其值也是不能共享的。
from threading import local, Thread, currentThread
# 定义一个local实例
local_data = local()
# 在主线中,存入name这个变量
local_data.name = 'local_data'
class MyThread(Thread):
def run(self):
print("赋值前-子线程:", currentThread(),local_data.__dict__)
# 在子线程中存入name这个变量
local_data.name = self.getName()
print("赋值后-子线程:",currentThread(), local_data.__dict__)
if __name__ == '__main__':
print("开始前-主线程:",local_data.__dict__)
t1 = MyThread()
t1.start()
t1.join()
t2 = MyThread()
t2.start()
t2.join()
print("结束后-主线程:",local_data.__dict__)
output:
开始前-主线程: {'name': 'local_data'}
赋值前-子线程: <MyThread(Thread-1, started 4832)> {}
赋值后-子线程: <MyThread(Thread-1, started 4832)> {'name': 'Thread-1'}
赋值前-子线程: <MyThread(Thread-2, started 5616)> {}
赋值后-子线程: <MyThread(Thread-2, started 5616)> {'name': 'Thread-2'}
结束后-主线程: {'name': 'local_data'}
可以知道,local实际是一个字典型的对象,其内部可以以key-value的形式存入你要做信息隔离的变量。local实例可以是全局唯一的,只有一个。因为你在给local存入或访问变量时,它会根据当前的线程的不同从不同的存储空间存入或获取。
基于此,我们可以得出以下三点结论:
- 主线程中的变量,不会因为其是全局变量,而被子线程获取到;
- 主线程也不能获取到子线程中的变量;
- 子线程与子线程之间的变量也不能互相访问。
再举一个例子。现在新手特别喜欢的爬虫项目。通常都是先给爬虫一个主页,然后获取主页下的所有链接,对这个链接再进行遍历,一直往下,直到把所有的链接都爬完,获取到我们所需的内容。
由于单线程的爬取效率实在是太低了,我们考虑使用多线程来工作。先使用socket和 www.sina.con.cn 建立一个TCP连接。然后在这个连接的基础上,对主页上的每个链接(我们这里只举 news.sina.com.cn 和 blog.sina.com.cn 这两个子链接做例子)创建一个线程,这样效率就高多了。
import threading
from functools import partial
from socket import socket, AF_INET, SOCK_STREAM
class LazyConnection:
def __init__(self, address, family=AF_INET, type=SOCK_STREAM):
self.address = address
self.family = AF_INET
self.type = SOCK_STREAM
self.local = threading.local()
def __enter__(self):
if hasattr(self.local, 'sock'):
raise RuntimeError('Already connected')
# 把socket连接存入local中
self.local.sock = socket(self.family, self.type)
self.local.sock.connect(self.address)
return self.local.sock
def __exit__(self, exc_ty, exc_val, tb):
self.local.sock.close()
del self.local.sock
def spider(conn, website):
with conn as s:
header = 'GET / HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n'.format(website)
s.send(header.encode("utf-8"))
resp = b''.join(iter(partial(s.recv, 100000), b''))
print('Got {} bytes'.format(len(resp)))
if __name__ == '__main__':
# 建立一个TCP连接
conn = LazyConnection(('www.sina.com.cn', 80))
# 爬取两个页面
t1 = threading.Thread(target=spider, args=(conn,"news.sina.com.cn"))
t2 = threading.Thread(target=spider, args=(conn,"blog.sina.com.cn"))
t1.start()
t2.start()
t1.join()
t2.join()
output:
Got 470 bytes
Got 768 bytes
五、线程池
5.1 使用python自带模块
在使用多线程处理任务时也不是线程越多越好,由于在切换线程的时候,需要切换上下文环境,依然会造成cpu的大量开销。为解决这个问题,线程池的概念被提出来了。预先创建好一个较为优化的数量的线程,让过来的任务立刻能够使用,就形成了线程池。
在Python3中,创建线程池是通过concurrent.futures函数库中的ThreadPoolExecutor类来实现的。
import time
import threading
from concurrent.futures import ThreadPoolExecutor
def target():
for i in range(5):
print('running thread-{}:{}'.format(threading.get_ident(), i))
time.sleep(1)
#: 生成线程池最大线程为5个
pool = ThreadPoolExecutor(5)
for i in range(100):
pool.submit(target) # 往线程中提交,并运行
output:
running thread-11308:0
running thread-12504:0
running thread-5656:0
running thread-12640:0
running thread-7948:0
running thread-11308:1
running thread-5656:1
running thread-7948:1
running thread-12640:1
running thread-12504:1
...
...
5.2 自定义线程池
import time
import threading
from queue import Queue
def target(q):
while True:
msg = q.get()
for i in range(5):
print('running thread-{}:{}'.format(threading.get_ident(), i))
time.sleep(1)
def pool(workers,queue):
for n in range(workers):
t = threading.Thread(target=target, args=(queue,))
t.daemon = True
t.start()
queue = Queue()
# 创建一个线程池:并设置线程数为5
pool(5, queue)
for i in range(100):
queue.put("start")
# 消息都被消费才能结束
queue.join()
output:
running thread-11308:0
running thread-12504:0
running thread-5656:0
running thread-12640:0
running thread-7948:0
running thread-11308:1
running thread-5656:1
running thread-7948:1
running thread-12640:1
running thread-12504:1
...
...
六、多进程的创建:
多进程与多线程的使用方式很类似,下面分别介绍。
6.1 用函数创建进程
import time
import random
from multiprocessing import Process
def piao(name):
print('%s piao' %name)
time.sleep(random.randrange(1,5))
print('%s piao end' %name)
p1=Process(target=piao,args=('e',)) #必须加,号
p2=Process(target=piao,args=('a',))
p3=Process(target=piao,args=('w',))
p4=Process(target=piao,args=('y',))
p1.start()
p2.start()
p3.start()
p4.start()
print('主线程')
output:
e piao
主线程
a piao
w piao
y piao
e piao end
y piao end
a piao end
w piao end
6.2 用类创建进程:
import time
import random
from multiprocessing import Process
class Piao(Process):
def __init__(self,name):
super().__init__()
self.name=name
def run(self):
print('%s piaoing' %self.name)
time.sleep(random.randrange(1,5))
print('%s piao end' %self.name)
p1=Piao('e')
p2=Piao('a')
p3=Piao('w')
p4=Piao('y')
p1.start() #start会自动调用run
p2.start()
p3.start()
p4.start()
print('主线程')
output:
e piaoing
主线程
a piaoing
w piaoing
y piaoing
e piao end
a piao end
y piao end
w piao end
注意:在windows中Process()必须放到# if name == ‘main’:下
6.3 进程属性
- 进程对象的其他方法一:terminate,is_alive
from multiprocessing import Process
import time
import random
class Piao(Process):
def __init__(self,name):
self.name=name
super().__init__()
def run(self):
print('%s is piaoing' %self.name)
time.sleep(random.randrange(1,5))
print('%s is piao end' %self.name)
p1=Piao('e1')
p1.start()
p1.terminate()#关闭进程,不会立即关闭,所以is_alive立刻查看的结果可能还是存活
print(p1.is_alive()) #结果为True
print('开始')
print(p1.is_alive()) #结果为False
output:
True
开始
False
- 进程对象的其他方法二:p.daemon=True,p.join
from multiprocessing import Process
import time
import random
class Piao(Process):
def __init__(self,name):
self.name=name
super().__init__()
def run(self):
print('%s is piaoing' %self.name)
time.sleep(random.randrange(1,3))
print('%s is piao end' %self.name)
p=Piao('e')
p.daemon=True #一定要在p.start()前设置,设置p为守护进程,禁止p创建子进程,并且父进程死,p跟着一起死
p.start()
p.join(0.0001) #等待p停止,等0.0001秒就不再等了
print('开始')
output:
Piao-1 is piaoing
开始
注意:p.join(),是父进程在等p的结束,是父进程阻塞在原地,而p仍然在后台运行
- name,pid
from multiprocessing import Process
import time
import random
class Piao(Process):
def __init__(self,name):
# self.name=name
# super().__init__() #Process的__init__方法会执行self.name=Piao-1,
# #所以加到这里,会覆盖我们的self.name=name
#为我们开启的进程设置名字的做法
super().__init__()
self.name=name
def run(self):
print('%s is piaoing' %self.name)
time.sleep(random.randrange(1,3))
print('%s is piao end' %self.name)
p=Piao('e')
p.start()
print('开始')
print(p.pid) #查看pid
七、进程锁
加锁的目的是为了保证多个进程修改同一块数据时,同一时间只能有一个修改,即串行的修改。
举个例子,文件当做数据库,模拟抢票(Lock互斥锁)
#文件db的内容为:{"count":2}
#注意一定要用双引号,不然json无法识别
from multiprocessing import Process,Lock
import json
import time
import random
import os
def work(filename,lock): #买票
# lock.acquire()
with lock:
with open(filename,encoding='utf-8') as f:
dic=json.loads(f.read())
# print('剩余票数: %s' % dic['count'])
if dic['count'] > 0:
dic['count']-=1
time.sleep(random.randint(1,3)) #模拟网络延迟
with open(filename,'w',encoding='utf-8') as f:
f.write(json.dumps(dic))
print('%s 购票成功' %os.getpid())
else:
print('%s 购票失败' %os.getpid())
# lock.release()
if __name__ == '__main__':
lock=Lock()
p_l=[]
for i in range(10):
p=Process(target=work,args=('db',lock))
p_l.append(p)
p.start()
for p in p_l:
p.join()
print('主线程')
output:
7932 购票成功
7933 购票成功
7934 购票失败
7935 购票失败
7936 购票失败
7937 购票失败
7938 购票失败
7939 购票失败
7940 购票失败
7941 购票失败
主线程
八、进程间的通信
进程彼此之间互相隔离,要实现进程间通信(IPC),multiprocessing模块支持两种形式:队列和管道,这两种方式都是使用消息传递的。
8.1. 队列(推荐使用)
创建队列的类(底层就是以管道和锁定的方式实现):
Queue([maxsize]):创建共享的进程队列,Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。maxsize是队列中允许最大项数,省略则无大小限制。
q.put:方法用以插入数据到队列中
put方法还有两个可选参数:blocked和timeout。
如果blocked为True(默认值),并且timeout为正值,该方法会阻塞timeout指定的时间,直到该队列有剩余的空间。
如果超时,会抛出Queue.Full异常。如果blocked为False,但该Queue已满,会立即抛出Queue.Full异常。
q.get: 方法可以从队列读取并且删除一个元素。
get方法有两个可选参数:blocked和timeout。
如果blocked为True(默认值),并且timeout为正值,那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。
如果blocked为False,有两种情况存在,如果Queue有一个值可用,则立即返回该值,否则,如果队列为空,则立即抛出Queue.Empty异常.
q.get_nowait():同q.get(False)
q.put_nowait():同q.put(False)
q.empty():调用此方法时q为空则返回True,该结果不可靠,比如在返回True的过程中,如果队列中又加入了项目。
q.full():调用此方法时q已满则返回True,该结果不可靠,比如在返回True的过程中,如果队列中的项目被取走。
q.qsize():返回队列中目前项目的正确数量,结果也不可靠,理由同q.empty()和q.full()一样
例子:
'''
multiprocessing模块支持进程间通信的两种主要形式:管道和队列
都是基于消息传递实现的,但是队列接口
'''
from multiprocessing import Process,Queue
import time
q=Queue(3)
#put ,get ,put_nowait,get_nowait,full,empty
q.put(3)
q.put(3)
q.put(3)
print(q.full()) #满了
print(q.get())
print(q.get())
print(q.get())
print(q.empty()) #空了
output:
True
3
3
3
True
8.2 生产者消费者模型
在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。
- 基于队列实现生产者消费者模型:
from multiprocessing import Process,Queue
import time,random,os
def consumer(q):
while True:
time.sleep(random.randint(1,3))
res=q.get()
if res is None:break
print('\033[45m消费者拿到了:%s\033[0m' %res)
def producer(seq,q):
for item in seq:
time.sleep(random.randint(1,3))
print('\033[46m生产者生产了:%s\033[0m' %item)
q.put(item)
if __name__ == '__main__':
q=Queue()
c=Process(target=consumer,args=(q,))
c.start()
producer(('包子%s' %i for i in range(5)),q)
q.put(None)
c.join()
print('主线程')
output:
生产者生产了:包子0
消费者拿到了:包子0
生产者生产了:包子1
消费者拿到了:包子1
生产者生产了:包子2
消费者拿到了:包子2
生产者生产了:包子3
消费者拿到了:包子3
生产者生产了:包子4
消费者拿到了:包子4
主线程
- 另外一种队列:
JoinableQueue([maxsize]):这就像是一个Queue对象,但队列允许项目的使用者通知生成者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。
maxsize是队列中允许最大项数,省略则无大小限制。
JoinableQueue的实例p除了与Queue对象相同的方法之外还具有:
q.task_done():使用者使用此方法发出信号,表示q.get()的返回项目已经被处理。如果调用此方法的次数大于从队列中删除项目的数量,将引发ValueError异常
q.join():生产者调用此方法进行阻塞,直到队列中所有的项目均被处理。阻塞将持续到队列中的每个项目均调用q.task_done()方法为止
例子:
from multiprocessing import Process,JoinableQueue
import time,random
def consumer(q):
while True:
# time.sleep(random.randint(1,2))
res=q.get()
print('消费者拿到了 %s' %res)
q.task_done()
def producer(seq,q):
for item in seq:
# time.sleep(random.randrange(1,2))
q.put(item)
print('生产者做好了 %s' %item)
q.join()
if __name__ == '__main__':
q=JoinableQueue()
seq=('包子%s' %i for i in range(5))
p=Process(target=consumer,args=(q,))
p.daemon=True #设置为守护进程,在主线程停止时p也停止,但是不用担心,producer内调用q.join保证了consumer已经处理完队列中的所有元素
p.start()
producer(seq,q)
print('主线程')
output:
生产者做好了 包子0
生产者做好了 包子1
生产者做好了 包子2
生产者做好了 包子3
生产者做好了 包子4
消费者拿到了 包子0
消费者拿到了 包子1
消费者拿到了 包子2
消费者拿到了 包子3
消费者拿到了 包子4
8.3 管道
Pipe([duplex]):在进程之间创建一条管道,并返回元组(conn1,conn2),其中conn1,conn2表示管道两端的连接对象,强调一点:必须在产生Process对象之前产生管道。dumplex:默认管道是全双工的,如果将duplex射成False,conn1只能用于接收,conn2只能用于发送。
属性:
conn1.recv():接收conn2.send(obj)发送的对象。如果没有消息可接收,recv方法会一直阻塞。如果连接的另外一端已经关闭,那么recv方法会抛出EOFError。
conn1.send(obj):通过连接发送对象。obj是与序列化兼容的任意对象
conn1.close():关闭连接。如果conn1被垃圾回收,将自动调用此方法
conn1.fileno():返回连接使用的整数文件描述符
conn1.poll([timeout]):如果连接上的数据可用,返回True。timeout指定等待的最长时限。如果省略此参数,方法将立即返回结果。如果将timeout射成None,操作将无限期地等待数据到达。
conn1.recv_bytes([maxlength]):接收c.send_bytes()方法发送的一条完整的字节消息。maxlength指定要接收的最大字节数。如果进入的消息,超过了这个最大值,将引发IOError异常,并且在连接上无法进行进一步读取。如果连接的另外一端已经关闭,再也不存在任何数据,将引发EOFError异常。
conn.send_bytes(buffer [, offset [, size]]):通过连接发送字节数据缓冲区,buffer是支持缓冲区接口的任意对象,offset是缓冲区中的字节偏移量,而size是要发送字节数。结果数据以单条消息的形式发出,然后调用c.recv_bytes()函数进行接收
conn1.recv_bytes_into(buffer [, offset]):接收一条完整的字节消息,并把它保存在buffer对象中,该对象支持可写入的缓冲区接口(即bytearray对象或类似的对象)。offset指定缓冲区中放置消息处的字节位移。返回值是收到的字节数。如果消息长度大于可用的缓冲区空间,将引发BufferTooShort异常。
基于管道实现进程间通信(与队列的方式是类似的,队列就是管道加锁实现的):
from multiprocessing import Process,Pipe
import time,os
def consumer(p,name):
left,right=p
left.close()
while True:
try:
baozi=right.recv()
print('%s 收到包子:%s' %(name,baozi))
except EOFError:
right.close()
break
def producer(seq,p):
left,right=p
right.close()
for i in seq:
left.send(i)
# time.sleep(1)
else:
left.close()
if __name__ == '__main__':
left,right=Pipe()
c1=Process(target=consumer,args=((left,right),'c1'))
c1.start()
seq=(i for i in range(10))
producer(seq,(left,right))
right.close()
left.close()
c1.join()
print('主进程')
output:
c1 收到包子:0
c1 收到包子:1
c1 收到包子:2
c1 收到包子:3
c1 收到包子:4
c1 收到包子:5
c1 收到包子:6
c1 收到包子:7
c1 收到包子:8
c1 收到包子:9
主进程
注意:生产者和消费者都没有使用管道的某个端点,就应该将其关闭,如在生产者中关闭管道的右端,在消费者中关闭管道的左端。如果忘记执行这些步骤,程序可能再消费者中的recv()操作上挂起。管道是由操作系统进行引用计数的,必须在所有进程中关闭管道后才能生产EOFError异常。因此在生产者中关闭管道不会有任何效果,付费消费者中也关闭了相同的管道端点。
管道可以用于双向通信,利用通常在客户端/服务器中使用的请求/响应模型或远程过程调用,就可以使用管道编写与进程交互的程序,如下
from multiprocessing import Process,Pipe
import time,os
def adder(p,name):
server,client=p
client.close()
while True:
try:
x,y=server.recv()
except EOFError:
server.close()
break
res=x+y
server.send(res)
print('server done')
if __name__ == '__main__':
server,client=Pipe()
c1=Process(target=adder,args=((server,client),'c1'))
c1.start()
server.close()
client.send((10,20))
print(client.recv())
client.close()
c1.join()
print('主进程')
output:
30
server done
主进程
注意:send()和recv()方法使用pickle模块对对象进行序列化。
九、进程池
创建进程池的类:
Pool([numprocess [,initializer [, initargs]]]):创建进程池
- numprocess:要创建的进程数,如果省略,将默认使用cpu_count()的值
- initializer:是每个工作进程启动时要执行的可调用对象,默认为None
- initargs:是要传给initializer的参数组
方法介绍:
p.apply(func [, args [, kwargs]]):在一个池工作进程中执行func(*args,**kwargs),然后返回结果。需要强调的是:此操作并不会在进程池并发执行func函数,而是会发生阻塞。如果要并发地执行func函数,必须从不同线程调用p.apply()函数或者使用p.apply_async()
p.apply_async(func [, args [, kwargs]]):在一个池工作进程中执行func(*args,**kwargs),然后返回结果。此方法的结果是AsyncResult类的实例,callback是可调用对象,接收输入参数。当func的结果变为可用时,将立即传递给callback。callback禁止执行任何阻塞操作,否则将接收其他异步操作中的结果。
p.close():关闭进程池,防止进一步操作。如果所有操作持续挂起,它们将在工作进程终止前完成 P.jion():等待所有工作进程退出。此方法只能在close()或teminate()之后调用
方法apply_async()和map_async()的返回值是AsyncResul的实例obj。实例具有以下方法:
obj.get():返回结果,如果有必要则等待结果到达。timeout是可选的。如果在指定时间内还没有到达,将引发一场。如果远程操作中引发了异常,它将在调用此方法时再次被引发。
obj.ready():如果调用完成,返回True
obj.successful():如果调用完成且没有引发异常,返回True,如果在结果就绪之前调用此方法,引发异常
obj.wait([timeout]):等待结果变为可用。
obj.terminate():立即终止所有工作进程,同时不执行任何清理或结束任何挂起工作。如果p被垃圾回收,将自动调用此函数
9.1 基本使用
提交任务,并在主进程中拿到结果(之前的Process是执行任务,结果放到队列里,现在可以在主进程中直接拿到结果)
from multiprocessing import Pool
import time
def work(n):
print('开工啦...')
time.sleep(3)
return n**2
if __name__ == '__main__':
q=Pool()
#异步apply_async用法:如果使用异步提交的任务,主进程需要使用jion,等待进程池内任务都处理完,然后可以用get收集结果,否则,主进程结束,进程池可能还没来得及执行,也就跟着一起结束了
res=q.apply_async(work,args=(2,))
q.close()
q.join() #join在close之后调用
print(res.get())
#同步apply用法:主进程一直等apply提交的任务结束后才继续执行后续代码
# res=q.apply(work,args=(2,))
# print(res)
output:
开工啦...
4
9.2 详解apply_async与apply
#一:使用进程池(非阻塞,apply_async)
#coding: utf-8
from multiprocessing import Process,Pool
import time
def func(msg):
print( "msg:", msg)
time.sleep(1)
return msg
if __name__ == "__main__":
pool = Pool(processes = 3)
res_l=[]
for i in range(10):
msg = "hello %d" %(i)
res=pool.apply_async(func, (msg, )) #维持执行的进程总数为processes,当一个进程执行完毕后会添加新的进程进去
res_l.append(res)
print("==============================>") #没有后面的join,或get,则程序整体结束,进程池中的任务还没来得及全部执行完也都跟着主进程一起结束了
pool.close() #关闭进程池,防止进一步操作。如果所有操作持续挂起,它们将在工作进程终止前完成
pool.join() #调用join之前,先调用close函数,否则会出错。执行完close后不会有新的进程加入到pool,join函数等待所有子进程结束
print(res_l) #看到的是<multiprocessing.pool.ApplyResult object at 0x10357c4e0>对象组成的列表,而非最终的结果,但这一步是在join后执行的,证明结果已经计算完毕,剩下的事情就是调用每个对象下的get方法去获取结果
for i in res_l:
print(i.get()) #使用get来获取apply_aync的结果,如果是apply,则没有get方法,因为apply是同步执行,立刻获取结果,也根本无需get
#二:使用进程池(阻塞,apply)
#coding: utf-8
from multiprocessing import Process,Pool
import time
def func(msg):
print( "msg:", msg)
time.sleep(0.1)
return msg
if __name__ == "__main__":
pool = Pool(processes = 3)
res_l=[]
for i in range(10):
msg = "hello %d" %(i)
res=pool.apply(func, (msg, )) #维持执行的进程总数为processes,当一个进程执行完毕后会添加新的进程进去
res_l.append(res) #同步执行,即执行完一个拿到结果,再去执行另外一个
print("==============================>")
pool.close()
pool.join() #调用join之前,先调用close函数,否则会出错。执行完close后不会有新的进程加入到pool,join函数等待所有子进程结束
print(res_l) #看到的就是最终的结果组成的列表
for i in res_l: #apply是同步的,所以直接得到结果,没有get()方法
print(i)
9.3 回调函数(apply_async的扩展用法)
进程池中任何一个任务一旦处理完了,就立即告知主进程:我好了额,你可以处理我的结果了。主进程则调用一个函数去处理该结果,该函数即回调函数。
我们可以把耗时间(阻塞)的任务放到进程池中,然后指定回调函数(主进程负责执行),这样主进程在执行回调函数时就省去了I/O的过程,直接拿到的是任务的结果。
from multiprocessing import Pool
import time,random,os
def get_page(url):
print('(进程 %s) 正在下载页面 %s' %(os.getpid(),url))
time.sleep(random.randint(1,3))
return url #用url充当下载后的结果
def parse_page(page_content):
print('<进程 %s> 正在解析页面: %s' %(os.getpid(),page_content))
time.sleep(1)
return '{%s 回调函数处理结果:%s}' %(os.getpid(),page_content)
if __name__ == '__main__':
urls=[
'http://maoyan.com/board/1',
'http://maoyan.com/board/2',
'http://maoyan.com/board/3',
'http://maoyan.com/board/4',
'http://maoyan.com/board/5',
'http://maoyan.com/board/7',
]
p=Pool()
res_l=[]
#异步的方式提交任务,然后把任务的结果交给callback处理
#注意:会专门开启一个进程来处理callback指定的任务(单独的一个进程,而且只有一个)
for url in urls:
res=p.apply_async(get_page,args=(url,),callback=parse_page)
res_l.append(res)
#异步提交完任务后,主进程先关闭p(必须先关闭),然后再用p.join()等待所有任务结束(包括callback)
p.close()
p.join()
print('{主进程 %s}' %os.getpid())
#收集结果,发现收集的是get_page的结果
#所以需要注意了:
#1. 当我们想要在将get_page的结果传给parse_page处理,那么就不需要i.get(),通过指定callback,就可以将i.get()的结果传给callback执行的任务
#2. 当我们想要在主进程中处理get_page的结果,那就需要使用i.get()获取后,再进一步处理
for i in res_l: #本例中,下面这两步是多余的
callback_res=i.get()
print(callback_res)
'''
打印结果:
(进程 52346) 正在下载页面 http://maoyan.com/board/1
(进程 52347) 正在下载页面 http://maoyan.com/board/2
(进程 52348) 正在下载页面 http://maoyan.com/board/3
(进程 52349) 正在下载页面 http://maoyan.com/board/4
(进程 52348) 正在下载页面 http://maoyan.com/board/5
<进程 52345> 正在解析页面: http://maoyan.com/board/3
(进程 52346) 正在下载页面 http://maoyan.com/board/7
<进程 52345> 正在解析页面: http://maoyan.com/board/1
<进程 52345> 正在解析页面: http://maoyan.com/board/2
<进程 52345> 正在解析页面: http://maoyan.com/board/4
<进程 52345> 正在解析页面: http://maoyan.com/board/5
<进程 52345> 正在解析页面: http://maoyan.com/board/7
{主进程 52345}
http://maoyan.com/board/1
http://maoyan.com/board/2
http://maoyan.com/board/3
http://maoyan.com/board/4
http://maoyan.com/board/5
http://maoyan.com/board/7
'''
9.4 一个爬虫的例子
from multiprocessing import Pool
import time,random
import requests
import re
def get_page(url,pattern):
response=requests.get(url)
if response.status_code == 200:
return (response.text,pattern)
def parse_page(info):
page_content,pattern=info
res=re.findall(pattern,page_content)
for item in res:
dic={
'index':item[0],
'title':item[1],
'actor':item[2].strip()[3:],
'time':item[3][5:],
'score':item[4]+item[5]
}
print(dic)
if __name__ == '__main__':
pattern1=re.compile(r'<dd>.*?board-index.*?>(\d+)<.*?title="(.*?)".*?star.*?>(.*?)<.*?releasetime.*?>(.*?)<.*?integer.*?>(.*?)<.*?fraction.*?>(.*?)<',re.S)
url_dic={
'http://maoyan.com/board/7':pattern1,
}
p=Pool()
res_l=[]
for url,pattern in url_dic.items():
res=p.apply_async(get_page,args=(url,pattern),callback=parse_page)
res_l.append(res)
for i in res_l:
i.get()