线程池、进程池(concurrent.futures模块)和协程
一、线程池
1、concurrent.futures模块
介绍
concurrent.futures模块提供了高度封装的异步调用接口
ThreadPoolExecutor:线程池,提供异步调用
ProcessPoolExecutor: 进程池,提供异步调用
在这个模块中进程池和线程池的使用方法完全一样
这里就只介绍ThreadPoolExecutor的使用方法,顺便对比multiprocessing的Pool进程池
2、基本方法
submit(fn, *args, **kwargs):异步提交任务 map(func, *iterables, timeout=None, chunksize=1) :取代for循环submit的操作,iterables的每个元素都作为参数传给func shutdown(wait=True) : 相当于进程池的pool.close()+pool.join()操作 wait=True,等待池内所有任务执行完毕回收完资源后才继续 wait=False,立即返回,并不会等待池内的任务执行完毕 但不管wait参数为何值,整个程序都会等到所有任务执行完毕 submit和map必须在shutdown之前 result(timeout=None):取得返回值 add_done_callback(fn):设置回调函数
3、例子:(对比multiprocessing的Pool模块)
3-1、
import time from concurrent.futures import ThreadPoolExecutor def func(i): print('thread', i) time.sleep(1) print('thread %s end' % i) tp = ThreadPoolExecutor(5) # 相当于tp = Pool(5) tp.submit(func, 1) # 相当于tp.apply_async(func,args=(1,)) tp.shutdown() # 相当于tp.close() + tp.join() print('主线程')
3-2、
import time from concurrent.futures import ThreadPoolExecutor from threading import currentThread def func(i): print('thread', i, currentThread().ident) time.sleep(1) print('thread %s end' % i) tp = ThreadPoolExecutor(5) for i in range(20): tp.submit(func, i) tp.shutdown() # shutdown一次就够了,会自动把所有的线程都join() print('主线程')
3-3、返回值
import time from concurrent.futures import ThreadPoolExecutor from threading import currentThread def func(i): print('thread', i, currentThread().ident) time.sleep(1) print('thread %s end' % i) return i * '*' tp = ThreadPoolExecutor(5) ret_lst = [] for i in range(20): ret = tp.submit(func, i) ret_lst.append(ret) for ret in ret_lst: print(ret.result()) # 相当于ret.get() print('主线程')
3-4、map
map接收一个函数和一个可迭代对象
可迭代对象的每一个值就是函数接收的实参,可迭代对象的长度就是创建的线程数量
map拿到的返回值是所有结果组成的迭代器(跟进程池Pool的map一样,就是返回值的类型不同,Pool的map拿到的返回值是列表)
import time from concurrent.futures import ThreadPoolExecutor def func(i): print('thread', i) time.sleep(1) print('thread %s end' % i) return i * '*' tp = ThreadPoolExecutor(5) ret = tp.map(func, range(20)) for i in ret: print(i)
3-5、回调函数
- 回调函数在进程池是由主进程实现的
- 回调函数在线程池是由子线程实现的
import time from concurrent.futures import ThreadPoolExecutor from threading import currentThread def func(i): print('thread', i, currentThread().ident) time.sleep(1) print('thread %s end' % i) return i def call_back(arg): print('call back : ', currentThread().ident) print('ret : ', arg.result()) # multiprocessing的Pool回调函数中的参数不需要get(),这里的参数需要result()才能取出值 tp = ThreadPoolExecutor(5) ret_lst = [] for i in range(20): tp.submit(func, i).add_done_callback(call_back) # 使用add_done_callback()方法实现回调函数 tp.shutdown() print('主线程', currentThread().ident)
从结果可以看出:
- 子线程func执行完毕之后才去执行callback回调函数
- 子线程func的返回值会作为回调函数的参数
4、总结
线程池
实例化线程池 ThreadPoolExcutor 5*cpu_count
异步提交任务 submit / map
阻塞直到任务完成 shutdown
获取子线程的返回值 result
回调函数 add_done_callback
二、协程概念介绍
1、介绍
进程 :计算机中最小的资源分配单位
线程 :计算机中能被cpu执行的最小单位
协程(纤程):一条线程在多个任务之间来回切换就叫协程
切换这个动作是浪费时间的
对于CPU、操作系统来说协程是不存在的
他们只能看到线程
协程的本质就是一条线程在多个任务之间来回切换,所以完全不会产生数据安全的问题
协程的理解:
把一个线程的执行明确的切分开
比如有两个任务,使用协程它帮助你记住哪个任务执行到哪个位置上了,并且实现安全的切换
当一个任务陷入阻塞,在这个任务阻塞的过程中切换到另一个任务中执行另一个任务
你的程序只要还有任务需要执行 你的当前线程永远不会阻塞
利用协程在多个任务陷入阻塞的时候进行切换来保证一个线程在处理多个任务的时候总是忙碌的
能够更加充分的利用CPU,抢占更多的时间片
无论是进程、还是线程都是由操作系统来切换的,开启过多的线程、进程会给操作系统的调度增加负担
如果我们是使用协程,协程在程序之间的切换操作系统感知不到,无论开启多少个协程对操作系统来说总是一个线程
操作系统的调度不会有任何压力
2、用生活中的例子来解释
协程:
比如你自己一个人:
做饭(协程)需要半个小时,你可以先洗米,然后把米饭交给电饭煲煮,这个时候煮饭就陷入阻塞了,你就可以去做其他家务了,比如这个时候你可以去洗衣服(协程),你把衣服放进洗衣机后,这个任务也陷入阻塞了,然后你又可以去做其他事情,比如这个时候你可以收拾屋子 (协程),然后在你收拾屋子的时候,米饭煮好了,你就去关电饭煲,过一段时间,衣服也洗好了,你就去关洗衣机,晾衣服,其他时间你都在收拾屋子,那么这样你的时间就利用得很充分了。
多线程(多进程):
上面的任务,你请了几个人帮你一起做,每个人都只做自己的那件事,
比如你做饭,然后一个人洗衣服,另一个人收拾屋子,这样的话在阻塞的时间里,每个人都是在等待的状态,没有充分利用时间,且成本高(你请人需要人工)
3、强调
- python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行)
- 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(非io操作的切换与效率无关)
4、对比
对比操作系统控制线程的切换,用户在单线程内控制协程的切换
优点:
- 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
- 单线程内就可以实现并发的效果,最大限度地利用cpu
缺点:
- 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
- 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
5、总结
必须在只有一个单线程里实现并发
修改共享数据不需加锁
用户程序里自己保存多个控制流的上下文栈
线程的调度是操作系统级别的
协程的调度是用户级别的
三、greenlet模块和gevent模块(都是扩展模块)
pip install greenlet
pip install gevent
1、介绍
greenlet:是gevent的底层,协程切换的模块,但是当切到一个任务执行时如果遇到io,那就原地阻塞,仍然是没有解决遇到IO自动切换来提升效率的问题。
gevent:基于greenlet优化而来的一个模块,gevent能提供更全面的功能,遇到io操作会自动切换任务(所以一般直接用这个模块就好)。
greenlet用法介绍
""" 使用greenlet()创建协程时可以传入两个参数,greenlet(run=None,parent=None) run表示需要调用的函数名称 parent表示它的父协程是哪个,意思是,如果本身协程结束后,会返回到它的父协程,继续执行;默认是主协程 若想让test2()执行完成后,返回到test1()中继续执行,那么只需要在g2 = greenlet(test2, g1)中,添加parent即可 从test2()返回到test1()时,会从g2.switch()下一行继续执行 """ 使用greenlet()创建协程,参数是函数名 g1 = greenlet(func1) g2 = greenlet(func2) 使用switch()函数启动或者切换协程 g1.switch() # 启动func1函数 g2.switch() # 切换到func2函数 g1.switch() # 切换到func1函数 如果需要传递参数,则使用switch(1, 2, name="dog")传参即可
gevent用法介绍
创建一个协程对象g1,spawn括号内第一个参数是函数名,后面可以有多个参数,可以是位置实参或关键字实参,都是传给你指定的函数 g1 = gevent.spawn(func, 1, 2, 3, x=4, y=5) g2 = gevent.spawn(func2) g1.join() # 等待g1结束 g2.join() # 等待g2结束 上面两个join可以写成一个:gevent.joinall([g1,g2]) # 参数是列表类型 g1.value # 拿到func的返回值 注意:在gevent模块中,有些阻塞它是不认识的,比如time模块的sleep,如果直接导入time模块,使用time.sleep(),在gevent模块的协程中并不会阻塞,因为gevent不认识time模块, 那么如何解决呢? 在导入time模块前先写上下面这两行代码,再导入time模块,这样time模块的内容它就认识了,time.sleep()也会阻塞了: from gevent import monkey monkey.patch_all() # patch_all就是把下面的模块的阻塞打成一个包,认识他们 import time 而在greenlet模块中,time的模块的阻塞它本身就是认识的。
2、例子
2-1、greenlet
import time from greenlet import greenlet def cooking(): print('cooking 1') g2.switch() # 切换到g2,让g2的函数工作 time.sleep(1) print('cooking 2') def watch(): print('watch TV 1') time.sleep(1) print('watch TV 2') g1.switch() # 切换到g1,让g1的函数工作 g1 = greenlet(cooking) g2 = greenlet(watch) g1.switch() # 启动g1,让g1的函数工作 ######## 传参 ######## import time from greenlet import greenlet def cooking(name): print('cooking ', name) g2.switch("cat") # 切换到g2,让g2的函数工作 time.sleep(1) print('cooking 2') def watch(name): print('watch TV', name) time.sleep(1) print('watch TV 2') g1.switch() # 切换到g1,让g1的函数工作 g1 = greenlet(cooking) g2 = greenlet(watch) g1.switch(name="dog") # 切换到g1,让g1的函数工作 greenlet的缺陷:很显然greenlet实现了协程的切换功能,可以自己设置什么时候切,在哪切,但是它遇到阻塞并没有自动切换, 因此并不能提高效率。所以一般我们都使用gevent模块实现协程
2-2、gevent
gevent常用方法
gevent.spawn() | 创建一个普通的Greenlet对象并切换 |
gevent.spawn_later(seconds=3) | 延时创建一个普通的Greenlet对象并切换 |
gevent.spawn_raw() | 创建的协程对象属于一个组 |
gevent.getcurrent() | 返回当前正在执行的greenlet |
gevent.joinall(jobs) | 将协程任务添加到事件循环,接收一个任务列表 |
gevent.wait() | 可以替代join函数等待循环结束,也可以传入协程对象列表 |
gevent.kill() | 杀死一个协程 |
gevent.killall() | 杀死一个协程列表里的所有协程 |
monkey.patch_all() | 非常重要,会自动将python的一些标准模块替换成gevent框架 |
# 无返回值 from gevent import monkey monkey.patch_all() import time import gevent def cooking(): print('cooking 1') time.sleep(1) print('cooking 2') def watch(): print('watch TV 1') time.sleep(1) print('watch TV 2') g1 = gevent.spawn(cooking) # 自动检测阻塞事件,遇见阻塞了就会进行切换 g2 = gevent.spawn(watch) g1.join() # 阻塞直到g1结束 g2.join() # 阻塞直到g2结束 # 有返回值 import gevent def cooking(i): print('%s号在煮饭' % i) return i g_lst = [] for i in range(10): g = gevent.spawn(cooking, i) # 函数名,参数 g_lst.append(g) # 把协程对象放入列表 for g in g_lst: g.join() print(g.value) # 打印返回值 # gevent.joinall(g_lst) # joinall一次性把全部对象都阻塞 # spawn_raw import gevent def cooking(i): print('%s号在煮饭' % i) for i in range(10): gevent.spawn_raw(cooking, i) # 函数名,参数 gevent.wait() # 让主协程阻塞,自动切换到cooking协程
2-3、协程名
from gevent import monkey monkey.patch_all() import time import gevent from threading import currentThread def cooking(): print('cooking name:',currentThread().getName()) print('cooking 1') time.sleep(1) print('cooking 2') def watch(): print('watch name:', currentThread().getName()) print('watch TV 1') time.sleep(1) print('watch TV 2') g1 = gevent.spawn(cooking) g2 = gevent.spawn(watch) g1.join() g2.join() # gevent.joinall([g1,g2]) # 结果: # cooking name: DummyThread-1 # Dummy的意思是假的,即协程是假线程,它只是同一个线程在任务间来回切换 # cooking 1 # watch name: DummyThread-2 # watch TV 1 # cooking 2 # watch TV 2
2-4、基于协程的爬虫例子
from gevent import monkey monkey.patch_all() import time import gevent import requests # 扩展模块 url_lst = [ 'http://www.baidu.com', 'http://www.4399.com', 'http://www.sohu.com', 'http://www.jd.com', 'http://www.sina.com', 'https://www.douban.com', 'http://www.sohu.com', 'http://www.baidu.com', 'http://www.4399.com', 'http://www.sohu.com', 'http://www.jd.com', 'http://www.sina.com', 'https://www.douban.com', 'http://www.sohu.com', 'http://www.baidu.com', 'http://www.4399.com', 'http://www.sohu.com', 'http://www.jd.com', 'http://www.sina.com', 'https://www.douban.com', 'http://www.sohu.com', 'http://www.baidu.com', 'http://www.4399.com', 'http://www.sohu.com', 'http://www.jd.com', 'http://www.sina.com', 'https://www.douban.com', 'http://www.sohu.com' ] def get_url(url): response = requests.get(url) if response.status_code == 200: # response.status_code的值是200的时候才代表爬取成功 print(url, len(response.text)) # response.text是爬取的网页内容,这里只打印一下内容的长度 # 普通方式爬取网页 # start = time.time() # for url in url_lst: # get_url(url) # print(time.time()-start) # 使用时间:5.616770267486572 # 使用协程爬取网页 start = time.time() g_lst = [] for url in url_lst: g = gevent.spawn(get_url, url) g_lst.append(g) gevent.joinall(g_lst) print(time.time() - start) # 使用时间:1.8181169033050537
2-5、基于gevent的socket
# 注意:from gevent import monkey;monkey.patch_all()一定要放到导入socket模块之前,否则gevent无法识别socket的阻塞 # Server端 from gevent import monkey monkey.patch_all() import socket import gevent from threading import currentThread def talk(conn): print('当前协程:', currentThread()) while 1: conn.send(b'hello') print(conn.recv(1024)) # 接收的时候阻塞,切换到另一个任务 sk = socket.socket() sk.bind(('127.0.0.1', 8000)) sk.listen() while 1: conn, addr = sk.accept() gevent.spawn(talk, conn) # 这里不需要join了,因为accept会阻塞,而且如果有join了之后, # 第一个协程没有运行完毕这里的循环就不会继续走了 # Client端 import socket from threading import Thread def client(): sk = socket.socket() sk.connect(('127.0.0.1', 8000)) while 1: print(sk.recv(1024)) sk.send(b'hi') for i in range(500): # 开启500个线程,即500个用户 Thread(target=client).start()
四、eventlet
1、介绍
Eventlet是Python的并发网络库,它允许你更改代码的运行方式,而不是编写方式。
- 它使用的epoll或kqueue的或libevent的为高度可扩展的非阻塞I / O。
- 协程确保开发人员使用类似于线程的阻塞式编程,但具有非阻塞I / O的优点。
- 事件分派是隐式的,这意味着您可以轻松地从Python解释器使用Eventlet,也可以将其作为大型应用程序的一小部分。
常用的方法
import eventlet eventlet.spawn(func, *args, **kwargs) 该函数创建一个使用参数 *args 和 **kwargs 调用函数 func 的绿色线程,多次孵化绿色线程会并行地执行任务。该函数返回一个greenthread.GreenThread 对象,可以用来获取函数 func 的返回值。 eventlet.spawn_n(func, *args, **kwargs) 作用类似于spawn(),只不过无法获取函数 func 执行完成时的返回值或抛出的异常。该函数的执行速度更快 eventlet.spawn_after(seconds, func, *args, **kwargs) 作用同于spawn(),等价于 seconds 秒后执行spawn()。可以对该函数的返回值调用 GreenThread.cancel() 退出孵化和阻止调用函数 func eventlet.sleep(seconds=0) 挂起当前的绿色线程,允许其他的绿色线程执行 实际上上面的这些方法是 greenthread 的方法,只是封装好了 eventlet.sleep = eventlet.greenthread.sleep eventlet.spawn = eventlet.greenthread.spawn eventlet.spawn_n = eventlet.greenthread.spawn_n eventlet.spawn_after = eventlet.greenthread.spawn_after eventlet.kill = eventlet.greenthread.kill
2、例子
# spawn import eventlet def func(name): print(name) return ("I am %s" % name) e1 = eventlet.spawn(func, 'dog') e2 = eventlet.spawn(func, 'cat') print(e1.wait()) print(e2.wait()) # 使用wait获取这个协程的返回值 # 结果 # dog # cat # I am dog # I am cat # spawn_n import eventlet def func(name): print(name) return ("I am %s" % name) # 实际不会返回这个值 e1 = eventlet.spawn_n(func, "dog") e2 = eventlet.spawn_n(func, "cat") eventlet.sleep() # 让主协程阻塞,才会切换到其他协程 # 或者主动执行 # e1 = eventlet.spawn_n(func) # e2 = eventlet.spawn_n(func) # e1.run("dog") # e2.run("cat") # 结果 # dog # cat # spawn_after import eventlet def func(name): print(name) return ("I am %s" % name) e1 = eventlet.spawn_after(2, func, 'dog') e2 = eventlet.spawn_after(1, func, 'cat') print(e1.wait()) print(e2.wait()) # 使用wait获取这个协程的返回值 # 结果 # cat # dog # I am dog # I am cat
五、eventlet.greenpool
1、介绍
该模块提供对 greenthread 池的支持。
greenthread 池提供了一定数量的备用 greenthread ,有效限制了孵化 greenthread 过多导致的内存不足,当池子中没有足够的空闲 greenthread 时,孵化过程被暂停,只有当先前工作中的 greenthread 完成当前工作,才能为下一个任务做孵化准备。
本模块包括两个类:
- eventlet.greenpool.GreenPool
- eventlet.greenpool.GreenPile
注意:
eventlet.GreenPool = eventlet.greenpool.GreenPool
eventlet.GreenPile = eventlet.greenpool.GreenPile
2、GreenPool协程池的常用方法
1.free() 返回当前对象中可用的greenthreads。 如果为 0 或更少,那么 spawn() 和 spawn_n() 将会阻塞调用 greenthread 直到有新的可用的 greenthread 为止。 至于为什么此处可能返回负值,请查看3. resize() 2.imap(function, *iterables) 效果等同于 itertools.imap() ,在并发和内存使用上等同于 starmap() 。 例如,可以非常方便地对文件做一些操作: def worker(line): return do_something(line) pool = GreenPool() for result in pool.imap(worker, open("filename", 'r')): print(result) 3. resize(new_size) 改变当前允许同时工作的 greenthreads 最大数量 如果当前有多于 new_size 的 greenthreads 处于工作中,它们可以完成自己的执行,只不过此时不许任何的新 greenthreads 被分配。只有当足够数量的 greenthreads 完成自己的工作,然后工作中的 greenthreads 总数低于 new_size 时,新的 greenthreads 才能被分配。在此之前,free() 的返回值将会使负的。 4.running() 返回当前池子中正在执行任务的 greenthreads 。 5.spawn(function, *args, **kwargs) # 常用 从当前的池子中孵化一个可用的greenthread,在这个 greenthread 中执行 function ,参数 *args, **kwargs 为传给 function 的参数。返回一个 GreenThread 对象,这个对象执行着 function ,可以通过该 GreenThread 对象获取 function 的返回值。 如果当前池子中没有空余的 greenthread ,那么该方法阻塞直到有新的可用的 greenthreads 被释放。 该函数可以重用, function 可以调用同一个 GreenPool 对象的 spawn 方法,不用担心死锁。 6.spawn_n(function, *args, **kwargs) 创建一个 greenthread 来运行 function,效果等同于 spawn()。 只不过这个函数返回 None,即丢弃 function 的返回值。 7.starmap(function, iterable) 等同于 itertools.starmap(),除了对于可迭代对象中的每一个元素,都会在一个 greenthread 里面执行 func 。 并发的上限由池子的容量限制。在实际的操作中, starmap() 消耗的内存与池子的容量成比例,从而格外适合遍历特别长的输入列表。 8. waitall() 等待池子中的所有 greenthreads 完成工作。 9. waiting() 返回当前等待孵化的 greenthreads 数。
3、协程池例子
# spawn from eventlet.greenpool import GreenPool def worker(work): return work pool = GreenPool() # 实例化一个协程池 res1 = pool.spawn(worker, "打杂") res2 = pool.spawn(worker, "打狗") print(res1.wait()) print(res2.wait()) # 结果 # 打杂 # 打狗 # spawn_n """ spawn并不是去开了一个线程,而是greenthread Spawn只是将这个方法放入到了需要调度的一个pool里面,只有调用wait了,eventlet才会用自己的调度器去对刚刚放入pool的协程(greenthread)进行调度执行 spawn_n 则需要waitall() GreenPool中创建的线程是绿色线程.这意味着它们都存在于操作系统级别的一个线程中, 并且Python解释器处理它们之间的切换.仅当一个线程产生(故意为其他线程提供运行机会)或正在等待I/O时,才会发生这种切换. 除了wait和waitall,还能主动sleep进行切换 import eventlet eventlet.sleep() """ from eventlet.greenpool import GreenPool def worker(work): print(work) # 没有返回值 pool = GreenPool() # 实例化一个协程池 pool.spawn_n(worker, "打杂") pool.spawn_n(worker, "打狗") pool.waitall() # 或者 eventlet.sleep() # 结果 # 打狗 # 打杂 # imap from eventlet.greenpool import GreenPool def worker(work): print(work) return work pool = GreenPool() # 实例化一个协程池 work_list = ["打杂", "打狗"] res = pool.imap(worker, work_list) for result in res: print(result) # 结果 # 打杂 # 打狗