当前位置: 首页 > 科技观察

5分钟完全掌握Python协程

时间:2023-03-15 23:19:02 科技观察

1。协程相关概念1.1进程和线程进程(Process)是应用程序启动的实例,具有代码、数据和文件以及独立的内存空间,是操作系统的最小资源管理单元。每个进程下都有一个或多个线程(Thread),负责执行程序的计算,是最小的执行单元。重点是:操作系统将负责进程的资源分配;控制权主要在操作系统。另一方面,作为一个任务的执行单元,可以新建一个线程,runnable(调用start方法,进入调度池,等待获取cpu使用权),runrunning(获取使用权使用cpu并开始执行程序),blocked(放弃cpu使用权,再等等)dead5中的不同状态。线程的转换也是由操作系统控制的。如果线程共享资源,就需要加锁。例如,在生产者和消费者模式中,生产者生产数据并共享队列,消费者从共享队列中消费数据。当线程和进程获得和放弃CPU使用权时,CPU使用权的切换会消耗性能,因为为了让一个线程在再次获得CPU使用权时继续执行任务,它必须记住上次执行的所有状态。此外,线程还有锁的问题。1.2并行和并发并行和并发听起来都像是同时执行不同的任务。但这种同时性具有不同的含义。并行性:只有多核CPU才能同时执行,即独立的资源可以不按顺序完成不同的任务。并发:好像是同时执行的。实际的微观层面是顺序执行。是操作系统对进程的调度和CPU的快速上下文切换。每个进程执行一段时间后停止,CPU资源切换到另一个进程,但是切换的时间很短,好像是多个任务在同时执行。为了实现大并发,需要将任务切割成更小的任务。上面提到的多核CPU可能同时执行,这可能与操作系统的调度有关。如果操作系统调度到同一个CPU,则CPU需要进行上下文切换。当然,在多核的情况下,操作系统调度会尽可能考虑不同的CPU。这里的上下文切换可以理解为需要保存不同执行任务的状态和数据。所有的并发处理都要排队等待、唤醒、执行至少三个这样的步骤。1.3协程我们知道,线程是为了在多核CPU的情况下实现并行而提出的。并且线程的执行完全由操作系统控制。协程在线程之下,控制权在用户。本质是让多组进程不单独占用所有资源,在一个线程中交叉执行,实现高并发。协程的优点:协程最大的优点就是协程的执行效率极高。因为子程序切换不是线程切换,而是由程序自己控制的,所以没有线程切换的开销。与多线程相比,线程越多,协程的性能优势越明显。第二大优势是不需要多线程。因为只有一个线程,所以不存在同时写变量的冲突。在协程中,对共享资源进行无锁控制,只需要判断状态,执行效率远高于多线程。协程与线程的区别:协程不参与多核CPU并行处理。协程不是并行线程。在多核处理器上,它们是并行的。在单核处理器上,协程由操作系统调度,需要保持上次调用线程的状态。系统的状态由操作系统控制。让我们暂时浏览一下这些文本中的概念。当你展示你的代码然后连接它们时,它会更清晰。2.python中的线程由于历史原因,python中的线程即使在多核CPU的情况下也无法实现真正??的并行。这样做的原因是全局解释器锁GIL(globalinterpreterlock)。准确的说,GIL并不是python的特性,而是cpython引入的一个概念。cpython解释器在解析多线程时,会锁住GIL,保证同一时间只有一个线程获得CPU使用权。为什么需要GIL?python中的一切都是对象。Cpython中对象的回收是通过对象的引用计数来判断的。当对象的引用计数为0时,会进行垃圾回收,自动释放内存。但是在多线程的情况下,引用计数就变成了一个共享变量。CPython是目前最流行的Python解释器。它使用引用计数来管理内存。在Python中,一切皆对象,引用计数指向对象。指针的数量。当这个数字变为0时,会进行垃圾回收,内存会自动释放。但问题是Cpython不是线程安全的。考虑如果两个线程A和B同时引用一个对象obj,此时obj的引用计数为2;A打算取消对obj的引用,当第一步完成时引用计数减1时,一个线程发生Switching,A挂起等待,还没有执行对象销毁操作。B进入运行状态。此时B也取消了对obj的引用,并完成引用计数减1,销毁对象。此时obj的引用号为0,释放内存。如果此时A再次醒来,它会继续销毁对象,但是此时没有对象。因此,为了保证不发生数据污染,引入了GIL。每个线程在使用前都会获取GIL权限,使用后释放GIL权限。释放线程的时机由python的另一个机制check_interval决定。在多核CPU的情况下,由于需要获取和释放GIL锁,还会有额外的性能损失。尤其是由于调度控制的原因,比如一个线程释放了锁,调度再分配cpu资源给同一个线程,当该线程发起一个应用,它重新获得了GIL,而其他线程实际上在等待,白白浪费了应用并且释放锁的操作是耗时的。python中的线程更适合I/O密集型操作(磁盘IO或网络IO)。线程使用importosimporttimeimportsysfromconcurrentimportfuturesdefto_do(info):foriinrange(100000000):passreturninfo[0]MAX_WORKERS=10param_list=[]foriinrange(5):param_list.append(('text%s'%i,'info%s'%i))workers=min(MAX_WORKERS,len(param_list))#with默认会等到所有任务完成后才返回,所以会阻塞withfutures.ThreadPoolExecutor(workers)asexecutor:results=executor.map(to_do,sorted(param_list))#printAllforresultinresults:print(result)#非阻塞方式,适用于不需要返回结果的情况workers=min(MAX_WORKERS,len(param_list))executor=futures.ThreadPoolExecutor(workers)results=[]foridx,paraminenumerate(param_list):result=executor.submit(to_do,param)results.append(result)print('result%s'%idx)#手动等待所有任务完成executor.shutdown()print('='*10)forresultinresults:print(result.result())3.python中的进程python提供的multiprocessing包避免了GIL的缺点,达到了在多核cpu上并行的目的。多处理还提供了一种在进程之间共享数据和内存的机制。此处介绍了concurrent.futures的实现。用法和thread基本一样,ThreadPoolExecutor改为ProcessPoolExecutorimportosimporttimeimportsysfromconcurrentimportfuturesdefto_do(info):foriinrange(10000000):passreturninfo[0]start_time=time.time()MAX_WORKERS=10param_list=[]foriinrange(5):param_list.app%'%i,'info%s'%i))workers=min(MAX_WORKERS,len(param_list))#with默认会等到所有任务完成后才返回,所以会阻塞withfutures.ProcessPoolExecutor(workers)asexecutor:results=executor.map(to_do,sorted(param_list))#打印所有forresultinresults:print(result)print(time.time()-start_time)#耗时0.3704512119293213s,而线程版本需要14.935384511947632s4。python中的Coroutine4.1简单Coroutines我们先来看看python是如何实现协程的。答案是收益率。下面例子的功能是实现计算移动平均fromcollectionsimportnamedtupleResult=namedtuple('Result','countaverage')#coroutinefunctiondefaverager():total=0.0count=0average=NonewhileTrue:term=yieldNone#pause,waiting为主程序传入数据唤醒iftermisNone:break#判断是否退出total+=termcount+=1average=total/count#累计状态,包括最后一个状态returnResult(count,average)#协程的触发coro_avg=averager()#预激活协程next(coro_avg)#调用者向协程提供数据异常属性值中包含的result=exc.valueprint(result)yield关键字有两个含义:output和yield;将yield右边的值yield给调用者,同时进行yield,暂停执行,让程序继续执行。上面的例子说明协程使用yield来控制流程,接收和生产数据next():预先激活协程send:协程从调用者接收数据StopIteration:控制协程结束,并获取返回值回顾一下1.3协程的概念:本质是让多组进程不单独占用所有资源,在一个线程中交叉执行,实现高并发。.如何解释上面的例子?一个协程可以一次分成一个任务,也就是移动平均。每个任务可以分成小步骤(也叫子程序),即每次计算一个数的平均值。如果需要执行多个任务怎么办?如何调用controller如果调用方有10个controller,可以想象当调用controller随机给每个task发送一个数据时,多个task会交叉执行,达到并发的目的。4.2asyncio协程应用封装asyncio是异步I/O,比如在高并发(比如百万并发)的网络请求。异步I/O是指你发起一个I/O操作而不用等待执行结束,你可以做其他的事情。asyncio底层是以协程的形式实现的。我们先来看一个例子,了解一下asyncio的内脏。importtimeimportasyncionow=lambda:time.time()#async定义协程asyncdefdo_some_work(x):print("waiting:",x)#await挂起阻塞,相当于yield,通常是一个耗时操作awaitasyncio.sleep(x)return"Doneafter{}s".format(x)#回调函数,类似于yield输出defcallback(future):print("callback:",future.result())start=now()tasks=[]foriinrange(1,4):#定义多个协程并同时预激活协程=do_some_work(i)task=asyncio.ensure_future(coroutine)task.add_done_callback(callback)tasks.append(task)#设置一个列表循环事件,把task放在里面,loop=asyncio.get_event_loop()try:#异步执行协程,直到所有操作完成,或者通过asyncio.gather收集多个taskloop.run_until_complete(asyncio.wait(tasks))fortaskintasks:print("Taskret:",task.result())exceptKeyboardInterruptase:#Coroutinetaskstatecontrolprint(asyncio.Task.all_tasks())fortaskinasyncio.Task.all_tasks():print(task.cancel())loop.stop()loop.run_forever()finally:loop.close()print("Time:",now()-start)中涉及的几个概念上面:event_loop事件循环:程序开始一个无限循环,把一些函数注册到事件循环中,当事件发生时,调用对应的协程函数coroutineCoroutine:协程对象,指的是使用async关键字定义的函数,它的调用不会立即执行该函数,而是会返回一个协程对象。协程对象需要注册到事件循环中,并被事件循环调用。task任务:协程对象是一个可以被挂起的native函数,task是对协程的进一步封装,包含了task的各种状态。future:表示将来执行或不执行的任务的结果。它和任务没有本质区别。async/await关键字:python3.5用于定义协程关键字,async定义协程,await用于挂起阻塞的异步调用接口。从上面可以看出,asyncio通过事件帮助我们实现了对协程调用者的控制处理,包括向协程发送数据。我们只需要通过async定义coroutine,await定义blocking,然后封装成futuretask,放到循环的事件列表中,等待数据返回即可。我们再来看一个http下载的例子。例如,你想下载5个不同的url(同样,你想接收数百万个外部请求)assession:asyncwithsession.get(url)asresponse:response=awaitresponse.read()#print(response)print('HelloWorld:%s'%time.time())if__name__=='__main__':loop=asyncio.get_event_loop()foriinrange(5):task=asyncio.ensure_future(hello(url.format(i)))tasks.append(task)loop.run_until_complete(asyncio.wait(tasks))4.3协议程序应用场景支持高并发I/O情况,比如写一个支持高并发的服务器而不是线程,提供并发性能。tornado和gevent都实现了类似的功能。上一篇文章提到Twisted也是5.总结这篇文章分享了关于python协程的概念和asyncio包的初步使用,同时也介绍了基本的相关概念,比如进程,线程,并发,并行等。希望对大家有所帮助你,欢迎交流(@mintel)。简要总结如下:并发和并行是不同的。并行是同时执行多个任务。并发就是在很短的时间内处理多个任务。多核cpu,进程是并行的,而python线程受制于GIL,所以不能并行化。耗时,协程正好可以弥补协程。协程不是并行的,而是任务交替执行。在I/O阻塞的情况下,可以异步执行,提高效率。asyncio异步I/O库可用于开发高并发应用