1,并发基本概念
并发和并行
- 并发:几个CPU可以做一大堆事
- 并行:几个CPU只能做几件事,真正同时运行
进程/线程/协程
- 进程:资源分配的最小单位,独立内存
- 线程:CPU调度的最小单位,共享内存,切换比进程快
- 协程:多协程只使用一个线程(CPU感知不到协程),规定代码块的执行顺序,进程/线程的调度由操作系统来决定,切换耗时较大
进程/线程/协程实现服务器的并发
- 多进程:实现简单,开销大性能差。每收到一个请求,创建一个新的进程,例如ForkingTCPServer。
- 多线程:涉及线程同步,可能有死锁。每收到一个请求,创建一个新的线程,例如ThreadingTCPServer。
- 协程:实现复杂,性能强大。每收到一个请求,放入一个事件列表,让主进程通过非阻塞I/O方式来处理请求,例如nginx。
Python的GIL
多进程处理CPU密集型,线程/协程处理I/O密集型。
Python有GIL,同一时刻只能有一个线程执行Python字节码,即一个Python进程只能使用一个CPU,即使开了多线程也只能使用一个CPU。但是Python标准库中所有执行阻塞型I/O操作的函数,在等待操作系统返回结果时都会释放GIL,意味着这个层次可以使用多线程,可以创建数以千计的线程处理I/O密集型。也就是说GIL只会对CPU密集型的程序产生影响,规避GIL限制主要有两种常用策略:一是使用多进程,二是使用C语言扩展,把计算密集型的任务转移到C语言中,使其独立于Python,在C代码中释放GIL。
多进程,Python多进程可以应付CPU密集型,其他语言中多线程也可以解决CPU密集?
多线程也能处理I/O密集型,为什么还要有协程呢?详见流畅的Python P448,协程有很多优点,减少线程间切换,做好全方位保护,自身会同步。。。。其他语言中,协程意义不大,因为多线程可以解决I/O阻塞?
IO操作(引自廖雪峰blog)
在IO编程一节中,我们已经知道,CPU的速度远远快于磁盘、网络等IO。在一个线程中,CPU执行代码的速度极快,然而,一旦遇到IO操作,如读写文件、发送网络数据时,就需要等待IO操作完成,才能继续进行下一步操作。这种情况称为同步IO。
在IO操作的过程中,当前线程被挂起,而其他需要CPU执行的代码就无法被当前线程执行了。
因为一个IO操作就阻塞了当前线程,导致其他代码无法执行,所以我们必须使用多线程或者多进程来并发执行代码,为多个用户服务。每个用户都会分配一个线程,如果遇到IO导致线程被挂起,其他用户的线程不受影响。
多线程和多进程的模型虽然解决了并发问题,但是系统不能无上限地增加线程。由于系统切换线程的开销也很大,所以,一旦线程数量过多,CPU的时间就花在线程切换上了,真正运行代码的时间就少了,结果导致性能严重下降。
由于我们要解决的问题是CPU高速执行能力和IO设备的龟速严重不匹配,多线程和多进程只是解决这一问题的一种方法。
另一种解决IO问题的方法是异步IO。当代码需要执行一个耗时的IO操作时,它只发出IO指令,并不等待IO结果,然后就去执行其他代码了。一段时间后,当IO返回结果时,再通知CPU进行处理。
同步和异步
- 进程/线程:同步机制,这里同步指的是在单个进程/线程内是同步的,同步意味着可能会被阻塞
- 协程/回调:异步机制,异步可以不被阻塞
五种IO模式
- 阻塞I/O
- 非阻塞I/O
- I/O复用:select,poll,epoll。epoll优点:没有最大文件数限制。Nginx、twisted使用epoll,Nginx 1G内存支持10W个连接。windows不支持epoll。一般只有做游戏或特别复杂爬虫可能用到。
- 信号驱动I/O
- 异步I/O:asyncio
同步IO(阻塞IP,非阻塞IO,I/O复用):数据准备好以后,用户还得read一下,即还需要等待内核态到用户态的转变,可能会卡
异步IO:完全不用等内核态/用户态转变
2,多线程 - threading
2.1,多线程的实现
主要用到:
- threading.current_thread() : 打印当前线程,看下是主线程还是子线程
- threading.active_count() : 看下当前活跃的线程个数
- t = threading.Thread(target, args):线程类的实例化
- t.start():启动线程
- t.join():连接线程,主线程连接t线程后,会等待该线程结束
开启多线程,执行如下代码:
st = time.time()
def run(n):
time.sleep(2)
print('线程{}:{}'.format(n, threading.current_thread()))
#### 创建子线程并运行 ####
ts = []
for i in range(6):
t = threading.Thread(target=run, args=(i,))
t.start()
ts.append(t)
#### 打印当前线程数 ####
print('当前线程数:', threading.active_count())
#### 将主线程"连接"到所有子线程,等待所有子线程完成后再往下运行 ####
for t in ts:
t.join()
#### 主线程打印输出 ####
print('主线程:', threading.current_thread())
print('耗时:', time.time() - st)
整体耗时只有2s,如果是串行执行run需要耗时2*6=12s,输出结果为:
当前线程数: 1 # 子线程已经运行结束,所以只看到1个线程,放在join之前打印会显示7个
线程4:<Thread(Thread-5, started 29768)>
线程5:<Thread(Thread-6, started 11404)>
线程2:<Thread(Thread-3, started 8368)>
线程1:<Thread(Thread-2, started 19244)>
线程0:<Thread(Thread-1, started 1120)>
线程3:<Thread(Thread-4, started 10296)>
主线程: <_MainThread(MainThread, started 26604)>
耗时: 2.0163538455963135
注意点:
如果主线程不join到子线程,则主线程不等子线程执行完毕就会打印输出,会看到耗时只有0.0s。
如果主线程在t.start()后面仅跟着t.join(),会变成串行执行多个线程。
2.2,守护线程 - daemon
设置守护线程方法:
- t = threading.Thread(target=run, args=(1,), daemon=True) # 实例化时创建
- t.setDaemon(True) # 实例化后创建,但必须要在t.start之前,否则会报错
如果主线程不join子线程,虽然主线程不等待子线程执行完毕就先打印了,但是主线程还是会等待子线程得到执行后,才最终结束:
st = time.time()
def run(n):
time.sleep(2)
print('线程{}:{}'.format(n, threading.current_thread()))
t = threading.Thread(target=run, args=(1,))
t.start()
print('主线程:', threading.current_thread())
print('当前线程数:', threading.active_count())
print('耗时:', time.time() - st)
运行结果:
主线程: <_MainThread(MainThread, started 25672)>
当前线程数: 2
耗时: 0.0
线程1:<Thread(Thread-1, started 21288)> # 主线程虽然先完成了上述打印,但还是等待子线程运行完毕后才结束
如果设置了守护线程,则主线程不会等待子线程运行完毕后,就先行结束,同时主线程结束后daemon线程会自动销毁:
st = time.time()
def run(n):
time.sleep(2)
print('线程{}:{}'.format(n, threading.current_thread()))
t = threading.Thread(target=run, args=(1,), daemon=True) # 设置daemon线程方法一
# t.setDaemon(True) # 设置daemon线程方法二
t.start()
print('主线程:', threading.current_thread())
print('当前线程数:', threading.active_count())
print('耗时:', time.time() - st)
运行结果:
主线程: <_MainThread(MainThread, started 25672)>
当前线程数: 2
耗时: 0.0
备注:python cookbook称daemon线程无法被join(),但python3.6.4中实验是可以被join()的。
3,多线程 - concurrent.futures.ThreadPoolExecutor()
futures.ThreadPoolExecutor(max_workers):max_workers:默认最大线程40个,以8核CPU为例,执行90个任务需耗时6s,如果设定最大线程100个,则执行90个任务需耗时2s。
3.1,map
执行相同的函数,依次返回结果:
st = time.time()
def run(n):
time.sleep(2)
print('线程{}:{}'.format(n, threading.current_thread()))
return n
with futures.ThreadPoolExecutor() as executor: # 创建executor
results = executor.map(run, range(6)) # 获取子线程执行结果,存入results
print(list(results)) # [0, 1, 2, 3, 4, 5]
print(time.time() - st) # 2.0158472061157227
创建excutor以及获取子线程执行结果results,如果不使用with...as...:
executor = futures.ThreadPoolExecutor()
results = executor.map(run, range(6))
传多个参数:
def run(m, n):
time.sleep(1)
print('线程{}运行结果是:{}\n'.format(threading.current_thread(), m + n))
with futures.ThreadPoolExecutor() as executor:
executor.map(run, (i for i in range(10, 16)), (j for j in range(0, 6)))
3.2,submit + as_completed
可以执行不同的函数,并且先结束的先返回结果
st = time.time()
def run1(n):
time.sleep(2)
print('线程{}:{}'.format(n, threading.current_thread()))
return n
def run2(n):
time.sleep(4)
print('线程{}:{}'.format(n, threading.current_thread()))
return n
with futures.ThreadPoolExecutor() as executor: # 创建executor
do = [executor.submit(run1, i) for i in range(3)] + [executor.submit(run2, i) for i in range(3, 6)]
results = [i.result() for i in futures.as_completed(do)] # 获取返回结果,存入results
print(list(results)) # [1, 0, 2, 3, 5, 4],前3个先返回
print(time.time() - st) # 4.016391754150391
创建excutor以及获取results,如果不使用with...as...:
executor = futures.ThreadPoolExecutor()
do = [executor.submit(run1, i) for i in range(3)] + [executor.submit(run2, i) for i in range(3, 6)]
results = [i.result() for i in futures.as_completed(do)]
3.3,多线程并发爬取某网站图片举例
某网页关于某主题可能有N个(N<40)图片,图片命名有规律,假如下载图片需1s,串行执行就是N秒,多线程只需要1s:
def get_jpg(jpg_path, save_path, fname): # 获取单个jpg
# jpg_path:图片路径,save_path:保存路径,fname:图片名称
if not os.path.exists('{}/{}.jpg'.format(save_path, fname)): # 已经存在就不用下载了
headers = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36'}
resp = requests.get(jpg_path, headers=headers, stream=True)
if resp.status_code == 200:
with open('{}/{}.jpg'.format(save_path, fname), 'wb') as f:
for chunk in resp:
f.write(chunk)
def get_jpgs(content, save_path, fname): # 获取多个jpg
# content:网页内容,save_path:保存路径,fname:图片名称
select = etree.HTML(content)
jpgs = select.xpath('//div[@class="photo-frame"]/img/@src')
jpgs = [i for i in jpgs if '-' in i] # 需要抓取图片路径中有‘-’的
if jpgs:
args = [[] for i in range(3)]
for i, jpg in enumerate(jpgs):
fname_new = '{}-{}'.format(fname, i)
jpg_path = '{}jp-{}'.format(*jpg.split('-'))
args[0].append(jpg_path)
args[1].append(save_path)
args[2].append(fname_new)
with ThreadPoolExecutor() as excutor:
res = excutor.map(get_jpg, *args) # map传参时需要注意,有n个参数就传n个列表
[r for r in res] # 执行
4,线程间通信
python cookbook:简单的(Event,Semaphore,Condition),复杂的(Queue,actor模式)
本章内容主要来自老男孩课件
4.1,threading.Lock()
python2.x中的用户锁:有了GIL还是可能有资源同时修改的情况,GIL每100多次重新释放? — 解决方法是真正执行加减时串行,再加一层用户锁
python3上加不加用户锁都默认有锁,但是还是建议加,因为python3官方没有声明默认加锁?
python3中没有这个问题,不用考虑加用户锁,用户锁会被程序变串行
lock = threading.Lock()
def run(n)
lock.acuqire()
global num
num += 1
time.sleep(1) # 可以看到程序编程串行了,50个线程就等50秒
lock.release()
4.2,递归锁
一般用不到,多把锁把程序锁死,这时候就需要用到递归锁
4.3,互斥锁mutex
信号量,同一时间允许n个线程同时修改数据,简单理解信号量同时有多把锁
threading.BoundedSemaphore(5) # 最多允许5个线程
应用场景:可以同一时间只放多少个连接
4.4,threading.Event()
常用方法:
event = threading.Event() # 生成事件
event.set() # 设置一个标志位
event.clear() # 清空标志位
event.wait() # 等待标志位被设定
event.is_set() # 判断标志位是否被设定
举例,event实现红绿灯:
event = threading.Event()
def light():
event.set() # 开始时要设置下标志位,一开始是绿灯
count = 0
while True:
if count > 20 and count < 31:
event.clear()
print('\033[41;1m 红灯 \033[0m')
elif count > 30:
event.set()
print('\033[42;1m 绿灯 \033[0m')
count = 0
else:
print('\033[42;1m 绿灯 \033[0m')
time.sleep(1)
count += 1
def car():
while True:
if event.is_set(): # 设置标志位,表示绿灯
print('绿灯行')
time.sleep(1)
else:
print('红灯停')
event.wait()
light = threading.Thread(target=light)
light.start()
car = threading.Thread(target=car)
car.start()
4.5,threading.Condition()
4.6,threading.Semaphore()
4.7,queue.Queue()
2个作用:解耦( 使得排队方和处理方没有关联关系,即低耦合),提高允许效率
队列可以简单理解为一个有顺序的容器
列表/元组有序,字典无序
列表取出数据后,还在列表里;队列取出数据后,不在队列中
队列方法:
class queue.Queue(maxsize=0) #先入先出FIFO
class queue.LifoQueue(maxsize=0) #后入先出LIFO
class queue.PriorityQueue(maxsize=0) #存储数据时可设置优先级的队列,根据优先级绝对出去的顺序
Queue.qsize()
Queue.empty() #return True if empty
Queue.full() # return True if full
Queue.put(item, block=True, timeout=None) # block队列满时抛不抛异常
Queue.put_nowait(item)
Queue.get(block=True, timeout=None) # block=True默认就是卡住,False不卡住,timeout是卡住的时间
Queue.get_nowait()
Queue.task_done()
Queue.join() # block直到queue被消费完毕
例如:
q1 = queue.Queue()
q1.put('d1')
q1.put('d2')
q1.get() # 输出d1
q1.get() # 输出d2
q1.get() # 会卡住,等在那
卡住的解决方法:
1)q1.get_nowait() # 不会卡住,如果取不到会抛出异常
2)用qsize判断后再取
队列应用实例:生产者-消费者模型
import queue
import threading
q = queue.Queue()
def Producer(name):
for i in range(10):
q.put('骨头%s'%i)
def Consumer(name):
while q.qsize() > 0:
print('%s吃%s'%(name, q.get()))
p = threading.Thread(target=Producer, args=('生产者', )) # 这里必须生产者后面有逗号,不然报错
c = threading.Thread(target=Consumer, args=('消费者', ))
p.start()
c.start()
4.8,actor模式
5,多进程 - multiprocessing
5.1,多进程的实现
基本与多线程的实现相同。
- multiprocessing.current_process() : 打印当前进程,看下是主进程还是子进程
- multiprocessing.active_children(): 看下当前活跃的子进程列表
- p = multiprocessing.Process(target, args):进程类的实例化
- p.start():启动子进程
- p.join():连接子进程,主进程连接p进程后,会等待该进程结束
- os.getppid():获取父进程id
- os.getpid():获取自己的进程id
必须在if __name__ == '__main__':下执行。
def run(n):
time.sleep(2)
print('进程{}:{}'.format(n, multiprocessing.current_process()))
if __name__ == '__main__':
st = time.time()
#### 创建子进程并运行 ####
ps = []
for i in range(6):
p = multiprocessing.Process(target=run, args=(i, ))
p.start()
ps.append(p)
#### 打印当前子进程列表 ####
print('当前子进程列表:', multiprocessing.active_children())
#### 将主进程"连接"到所有子进程,等待所有子进程完成后再往下运行 ####
for p in ps:
p.join()
#### 主进程打印输出 ####
print('主进程:', multiprocessing.current_process())
print('耗时:', time.time() - st)
5.2,进程池
apply:阻塞式,同步执行,单进程,和串行执行没什么区别,官方建议废除apply
def run(n):
time.sleep(2)
print('进程{}:{}'.format(n, multiprocessing.current_process()))
if __name__ == '__main__':
st = time.time()
#### 依次创建和调用6个进程 ####
pool = multiprocessing.Pool(6)
for i in range(6):
pool.apply(run, (i, ))
#### 主进程打印输出 ####
print('主进程:', multiprocessing.current_process())
print('耗时:', time.time() - st) # 串行执行,共耗时12s
apply_async:非阻塞式,异步执行,多进程可同时执行
def run(n):
time.sleep(2)
print('进程{}:{}'.format(n, multiprocessing.current_process()))
if __name__ == '__main__':
st = time.time()
#### 创建和调用6个进程 ####
pool = multiprocessing.Pool(3)
for i in range(3):
pool.apply_async(run, (i, ))
pool.close()
pool.join()
#### 主进程打印输出 ####
print('主进程:', multiprocessing.current_process())
print('耗时:', time.time() - st) # 并行执行,共耗时2s
进程池的回调举例,把进程需要写入文件的内容作为返回值返回给汇合的回调函数,使用回调函数向文件中写入内容。下例将12345678写入123.txt内容:
def mycallback(x):
with open('123.txt', 'a+') as f:
f.write(str(x))
time.sleep(2)
def fun(num):
return num
if __name__ == '__main__':
st = time.time()
pool = multiprocessing.Pool()
#### 创建和调用6个进程 ####
for i in range(8):
pool.apply_async(fun, args=(i,), callback=mycallback) # 回调函数汇总后执行写入
pool.close()
pool.join() # 注意必须先close再join
#### 主进程打印输出 ####
print('主进程:', multiprocessing.current_process())
print('耗时:', time.time() - st) # 共耗时16s
6,多进程 - concurrent.futures.ProcessPoolExecutor()
参考多线程 - concurrent.futures.ThreadPoolExecutor(),只需要改为futures.ProcessPoolExecutor()。
必须在if __name__ == '__main__':下执行。
def run(n):
time.sleep(2)
print('进程{}:{}'.format(n, multiprocessing.current_process()))
return n
if __name__ == '__main__':
st = time.time()
with futures.ProcessPoolExecutor(max_workers=8) as executor: # 创建executor
results = executor.map(run, range(6)) # 获取子进程执行结果,存入results
print(list(results)) # [0, 1, 2, 3, 4, 5]
print(time.time() - st) # 2.3747103214263916
进程里执行线程的简单例子例子一,来自老男孩python
def thread_run():
print(threading.get_ident()) # 打印线程号
def run(name):
time.sleep(2)
print('hello', name)
t = threading.Thread(target=thread_run,)
t.start()
if __name__ == '__main__':
for i in range(10):
p = multiprocessing.Process(target=run, args=('bob',))
p.start()
进程里执行线程的简单例子例子二,来自老男孩python
def info(title):
print(title)
print('module name', __name__)
print('parent process', os.getppid())
print('process id', os.getpid())
def f(name):
info('\033[31;1m called from process function f \033[0m') # 子进程里启动info,会显示父进程号16346
print('hello', name)
if __name__ == '__main__':
info('\033[32;1m main process line \033[0m') # 主进程里启动info,会显示进程号16346
p = Process(target=f, args=('bob',))
p.start()
7,进程间通信
来自老男孩python
线程间交互要考虑加锁,进程间交互不用考虑加锁,因为进程间共享数据实际上是做了拷贝,不是同一份数据。必须找一个翻译,帮助进程间交互。
7.1,multiprocessing.Queue()
上一节说的是线程Queue(queue.Queue),出了进程就访问不到,例如同时启动2个cmd窗口,一个put数据,另一个get数据get不到
本节的Queue是进程Queue(multiprocessing.Queue),可以在进程间访问,实际上是2个Q,拷贝Q,使用看上去像共享Q
线程Q1:
def f():
q.put([42, None, 'hello']) # 子线程能够访问线程Q
if __name__ == '__main__':
q = multiprocessing.Queue()
p = threading.Thread(target=f, ) # 主线程启动子线程
p.start()
print(q.get()) # 能够取出子线程放的数据
线程Q2:
def f():
q.put([42, None, 'hello']) # 子进程无法访问线程Q,会报错
if __name__ == '__main__':
q = multiprocessing.Queue()
p = multiprocessing.Process(target=f, ) # 主进程启动子进程
p.start()
print(q.get())
进程Q:
def f(qq):
qq.put([42, None, 'hello'])
if __name__ == '__main__':
qq = multiprocessing.Queue()
p = multiprocessing.Process(target=f, args=(qq, )) # 需要把进程Q传给子进程
p.start()
print(qq.get()) # OK,能够取出子进程放的数据
7.2,管道Pipes
def f(conn):
conn.send([42, None, 'Hello from child']) # 管道子头发送消息
conn.close() # 关闭管道
if __name__ == '__main__':
parent_conn, child_conn = multiprocessing.Pipe()
p = multiprocessing.Process(target=f, args=(child_conn,)) # 管道子头给另一个进程
p.start()
print(parent_conn.recv()) # 管道父头
备注:父进程也能给子进程发消息
7.3,managers
队列和管道无法实现数据共享,但是manager可以实现数据共享
def f(d, l):
d[1] = '1' # 字典添加一个item
l.append(os.getpid()) # 列表添加进程id
print('子进程的列表', l)
if __name__ == '__main__':
with multiprocessing.Manager() as manager:
d = manager.dict() # 生成一个字典,可在多个进程间共享和传递
l = manager.list(range(5)) # 生成一个列表,可在多个进程间共享和传递
p_list = []
for i in range(10):
p = multiprocessing.Process(target=f, args=(d, l))
p.start()
p_list.append(p)
for res in p_list:
res.join()
print('最后的字典', d) # 字典是一样的,因为字典自动去重,改成不一样的也能像列表不停加上
print('最后的列表', l) # 列表会不停的加上进程号
7.4,进程锁
from multiprocessing import Process, Lock
def f(l, i):
l.acquire() # 锁定
print('hello world', i)
l.release() # 开启
if __name__ == '__main__':
lock = Lock() # 生成一个锁
for num in range(10):
Process(target=f, args=(lock, num)).start() # 锁传给进程
备注:每个进程是独立的,不需要锁,但是进程是共享屏幕的,如果大家都抢着在屏幕上打印数据,出现可能helloworld没打完,其他进程会抢着打印
8,协程
线程的切换会保存到CPU寄存器,但是协程不会保存到寄存器,CPU感知不到协程
协程不用加锁,因为协程是单线程的
缺点:协程是单线程的,无法利用多核资源,阻塞时会阻塞掉整个程序
I/O比较耗时,协程之所以能大并发,主要是遇到I/O就切换,什么时候切回去呢?
8.1,协程的实现 - 官方yield
协程基本方法:
- 预激:1)手动next(),2)装饰器,3)yield from
- 发送值:send
- 终止:1)发送哨符值,2)close(),3)throw()输入未处理异常
- 异常处理:throw输入捕捉到的异常
- 获取返回值:1)PEP8,2)yield from
例子1,协程的预激和发送值:
def test1(a):
b = yield a
c = yield a + b
d = yield a + b + c
print(d)
t1 = test1(1) # 绑定调用方
print(next(t1)) # 输出:1,预激协程,有返回值a=1
print(t1.send(1)) # 输出:2,发送数据至b,有返回值a+b=2
print(t1.send(1)) # 输出:3,发送数据至c,有返回值a+b+c=3
t1.send(10) # 输出:10,发送数据至d,执行print(d),然后报错
例子2,协程的终止和异常处理:
class DemoException(Exception):
""""""
def test2():
t = 0
while True:
try:
x = yield t
except DemoException:
print('DemoException handled. Continuing...')
else:
t += x
print(t)
t2 = test2()
next(t2)
t2.send(1) # 输出:1
t2.send(1) # 输出:2
t2.throw(DemoException) # 输出:DemoException handled. Continuing...
t2.send(1) # 输出:3
t2.send(1) # 输出:4
t2.throw(ZeroDivisionError) # 报错:ZeroDivisionError
t2.send(1) # 不会执行
例子3,协程返回值(PEP8方式):
def test3():
total = 0
while True:
a = yield
if a is None:
break
total += a
return total
t3 = test3()
next(t3)
t3.send(1)
t3.send(1)
t3.send(1)
try:
t3.send(None)
except StopIteration as exc:
res = exc
print(res) # 输出:3
9,异步IO
9.1,asyncio - 官方
同步 —> 线程 —> 异步IO实现转圈打印
1)同步
i = 0
while i < 50:
char = '|/-\\'[(divmod(i , 4)[1])]
status = char + ' thinking!'
sys.stdout.write(status)
sys.stdout.flush()
sys.stdout.write('\x08' * len(status))
time.sleep(.1)
i += 1
sys.stdout.write('answer: 42')
2)线程threading
s = True
def spin(msg):
for char in itertools.cycle('|/-\\'):
status = char + ' ' + msg
sys.stdout.write(status)
sys.stdout.flush()
sys.stdout.write('\x08' * len(status))
time.sleep(.1)
if not s:
break
def slow_function():
time.sleep(3)
return 42
def supervisor():
global s # 必须声明global,因为后面本地s=False,不声明解释器会认为是local
spinner = threading.Thread(target=spin, args=('thinking!', ))
spinner.start()
result = slow_function()
s = False
spinner.join()
return result
def main():
result = supervisor()
print('answer:', result)
if __name__ == '__main__':
main()
3)异步asyncio
@asyncio.coroutine
def spin(msg):
for char in itertools.cycle('|/-\\'):
status = char + ' ' + msg
sys.stdout.write(status)
sys.stdout.flush()
sys.stdout.write('\x08' * len(status))
try:
yield from asyncio.sleep(.1)
except asyncio.CancelledError:
break
@asyncio.coroutine
def slow_function():
yield from asyncio.sleep(3)
return 42
@asyncio.coroutine
def supervisor():
spinner = asyncio.async(spin('thinking!'))
result = yield from slow_function()
spinner.cancel()
return result
def main():
loop = asyncio.get_event_loop()
result = loop.run_until_complete(supervisor())
loop.close()
print('anwser:', result)
if __name__ == '__main__':
main()
备注:spin()和slow_function()中,可以把@asyncio.coroutine替换为async def;把yield from替换为await,但是supervisor()不行,asyncio.async会报错
9.2,gevent - 第三方
备注:本节内容来自文字部分来自廖雪峰blog,案例来自老男孩python
gevent是第三方库,通过greenlet实现协程,其基本思想是:
当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。
由于切换是在IO操作时自动完成,所以gevent需要修改Python自带的一些标准库,这一过程在启动时通过monkey patch完成:
案例一,greenlet手动切换,类似于yeild
import greenlet
def test1():
print(12)
gr2.switch() # 切换到gr2
print(34)
gr2.switch() # 切换到gr2
def test2():
print(56)
gr1.switch()
print(78)
gr1=greenlet.greenlet(test1) # 启动一个协程
gr2=greenlet.greenlet(test2)
gr1.switch()
案例二,gevent自动切换,对greenlet进行封装,实现了自动切换,gevent只能在Unix/Linux下运行,在Windows下不保证正常安装和运行。
import gevent
def func1():
print('\033[31;1m李闯在跟海涛搞...\033[0m')
gevent.sleep(2)
print('\033[31;1m李闯又回去跟继续跟海涛搞...\033[0m')
def func2():
print('\033[32;1m李闯切换到了跟海龙搞...\033[0m')
gevent.sleep(1)
print('\033[32;1m李闯搞完了海涛,回来继续跟海龙搞...\033[0m')
gevent.joinall([gevent.spawn(func1), gevent.spawn(func2)])
gevent自己判断I/O操作,实现了func1和func2之间遇到sleep进行自动切换,如果有func3,会依次执行func1,func2,func3,再切换回去轮回
案例三,gevent协程实现并发爬虫
from urllib import request
import gevent
def f(url, filename):
print('get:%s'%url)
resp=request.urlopen(url)
data=resp.read()
f=open('%s.html'%filename,'wb')
f.write(data)
f.close()
print('%d bytes received from %s'%(len(data), url))
gevent.joinall([#相当于3个协程都执行这个参数
gevent.spawn(f, 'https://www.python.org/', '111'),
gevent.spawn(f, 'https://www.yahoo.com/', '222'),
gevent.spawn(f, 'https://github.com/', '333'),
])
gevent检测不到urlib的I/O操作,所以还是串行的,所以需要给它打个monkeypatch补丁,需要加上:
from gevent import monkey
monkey.patch_all() # 把当前程序的所有的I/O操作给我单独的做上标记
10,并发的综合实验
以8核CPU为例
import time
from functools import partial
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import ProcessPoolExecutor
import multiprocessing
import asyncio
def run(x, y):
time.sleep(5)
return '{}:{}'.format(x, y)
p = partial(run, '序号') # 有时需要传个固定参数,可以用partial冻结,想冻结y可以用y='序号'
单线程串行:100s
for i in range(20):
res = run('序号', i)
print(res)
多线程(futures.ThreadPoolExecutor):5s,默认最大开40个线程
with ThreadPoolExecutor() as excutor:
res = excutor.map(p, range(20))
for i in res:
print(i)
多进程(futures.ThreadPoolExecutor):15s,最终一次打印20个
with ProcessPoolExecutor() as excutor:
res = excutor.map(p, range(20))
for i in res:
print(i)
多进程(进程池 + map): 15s,最终一次打印20个
ps = multiprocessing.Pool()
res = ps.map(p, (range(20)))
for i in res:
print(i)
多进程(进程池 + apply_sync): 15s,分3次打印20个,先好的8个先打印
res = []
ps = multiprocessing.Pool()
for i in range(20):
res.append(ps.apply_async(run, (i, '序号'))) # apply是同步执行,应该使用apply_sync
for i in res:
print(i.get())
协程(异步IO asyncio):5s
async def run(x, y):
await asyncio.sleep(5)
return '{}:{}'.format(x, y)
tasks = [asyncio.ensure_future(run('序号', i)) for i in range(20)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
for t in tasks:
print(t.result())