当前位置: 首页 > 后端技术 > Python

说说python协程和golang协程的区别

时间:2023-03-26 12:37:16 Python

背景最近在重构后端服务从python到go。这两种语言最大的特点和优势就是都支持协程。以前主要关注python的性能优化和架构优化。一开始觉得这两个协程的原理和应用应该是差不多的。后来发现还是有很大区别的。今天我就在这里总结一下。什么是协程在说两者的区别之前,我们先来说说什么是协程。官方似乎没有对它的定义,下面结合我们平时的应用经验和学习内容说说自己的理解。协程其实可以理解为用户态的一种特殊的程序调用。比较特别的是,在执行过程中,可以在子程序(或函数)内部中断,转而执行其他子程序,在适当的时候再返回继续执行。注意它有两个特点:Interruptible,这里的中断不是普通的函数调用,而是类CPU的中断,直接释放CPU,转移到其他程序断点处继续执行。它可以恢复。时机成熟时,它可以恢复到被中断的地方继续执行。至于什么时候合适,我们以后再说。与进程线程的区别以上两个特点导致其相对于线程和进程切换具有极高的执行效率。你为什么这么说?先说进程和线程。进程是操作系统资源分配的基本单位,线程是操作系统调度和执行的最小单位。这两句话应该是我们最常听到的两句话了。拆开来看,进程就是一个程序的启动实例,它有代码,有打开的文件资源,有数据资源,有独立的内存空间。线程属于进程,是程序的实际执行者。一个进程至少包含一个主线程,并且可能有多个子线程。线程有自己的栈空间。无论是进程还是线程,都是由操作系统来管理和切换的。我们再来看协程,也叫微线程,其实跟进程跟线程完全不是一个维度的概念。进程和线程的切换完全是用户不敏感的,由操作系统控制,从用户态到内核态再到用户态。协程的切换完全由程序代码控制。用户态的切换,就像函数回调的消费一样,可以在线程栈中完成。Python的协程(coroutine)python的协程其实就是我们通常意义上的协程。从概念上讲,python的协程也可以在适当的时候中断和恢复。那么什么时候合适,什么时候你觉得合适,因为程序在哪个协程之间切换是完全由开发者控制的。当然对于python来说,由于GIL锁,在CPU密集型代码上切换协程是没有意义的。CPU已经很忙了,并没有偷懒。切换到其他协程只是在单核中换了个地方。只是忙。显然,我们应该在IO密集的地方启动协程,让CPU停止等待,去其他地方工作,才能真正发挥协程的威力。在实现上,如果熟悉python生成器,也可以将协程理解为生成器+调度策略。生成器中的yield关键字可以打断生成器函数,调度策略可以驱动协程。程序执行和恢复。这就实现了协程的概念。这里的调度策略可能有很多,简单的比如busyroundrobin:whileTrue,更简单的甚至是for循环。它可以驱动生成器的运行,因为生成器本身也是可迭代的。复杂的可能是基于epool的事件循环。在python2的tornado和python3的asyncio中,对coroutines的使用进行了较好的封装。协程可以通过yield和await使用,通过事件循环监听文件描述符的状态来驱动协程恢复执行。让我们看一个简单的协程:importtimedefconsumer():r=''whileTrue:n=yieldrifnotn:returnprint('[CONSUMER]Consuming%s...'%n)time.sleep(1)r='200OK'defproduce(c):c.next()n=0whilen<5:n=n+1print('[PRODUCER]Producing%s...'%n)r=c.send(n)print('[PRODUCER]Consumerreturn:%s'%r)c.close()if__name__=='__main__':c=consumer()produce(c)显然这是传统的Producer-consumermodel,其中consumerfunction是协程(generator),在n=yieldr的地方被中断,producerproduce中的c.send(n)可以驱动协程的回收,并将数据n传递给协程函数并接收返回结果r。而n<5就是我们所说的调度策略。在生产中,这种模式非常适合我们消费一些管道数据。我们不需要杀掉几个生产者进程和几个消费者进程,而是用这种协程的方式来实现CPU的动态分配。调度。如果你看过上一篇文章,有没有发现golang中的管道模型有点类似,也是生产者和消费者之间的通信,但是go使用的是channel这样的安全数据结构,为什么python不需要,因为Python的协程在单个线程内切换是安全的。换句话说,协程本身是串行执行的。但是golang不是。想一个有趣的问题,如果我们把go管道模型中的channel设置成没有buffer,生产者绝对会带动消费者的执行,是不是很像python?那么从某种意义上说,python协程是不是golang协程的特例呢?后端在线服务中比较常用的python协程其实是用在异步IO框架中的。之前我们也提到过,python协程只有在IO密集型系统中才能发挥威力。并且大部分数据中间件已经提供了对异步包的支持。对了,这里介绍一个python3支持的异步IO库,基本支持常见的异步数据中间件。再看一个代码片段,asyncio支持的nativecoroutine:asyncio支持的epool-basedeventloop:defmain():define_options()options.parse_command_line()#Useuvloopinsteadofnativeeventloop#asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())app=tornado.web.Application(handlers=handlers,debug=options.debug)http_server=tornado.httpserver.HTTPServer(app)http_server.listen(options.port)asyncio.get_event_loop().run_forever()async/await支持的原生协程:classCreditHandler(BaseHandler):asyncdefpost(self):status,msg,user=self.check_args('uid','order_no','phone','name','product_id')ifstatus!=ErrorCodeConfig.SUCCESS:status,msg,report=status,msg,Noneelse:RcOutputFlowControler())status,msg,report=awaitrcoutput_flow_instance.get_rcoutput_result(user)res=self.generate_response_data(status,毫秒g、report)awaitself.finish(res)awaitAccompanyRunningFlowControler().get_accompany_data(user)总结python协程的特点:单线程内切换,适用于IO密集型程序,可以最大化IO复用效果无法利用多核。协程是完全同步的,不会被并行化。无需考虑数据安全。它的使用方式多种多样,可以用在web服务中,也可以用在pipeline数据/任务消费中。Golang的协程(goroutine)和传统的协程不太一样。它兼有协程和线程的优点。这也是go最大的特点,就是在语言层面支持并发。在Go语言中,启动一个goroutine很简单:只需gofunction。同样在概念上,golang的协程也可以在适当的时候中断和恢复。当协程发生通道读写阻塞或系统调用时,会切换到其他协程。具体代码示例可以参考上一篇文章,这里不再赘述。在实现上,goroutine可以运行在多核上,实现并行协程。我们先直接看go调度模型MPG。如上所示,M指的是Machine,一个M直接对应一个内核线程。由操作系统管理。P是指“处理器”,代表M所需要的上下文环境,也是处理用户级代码逻辑的处理器。它负责桥接M和G的调度上下文,连接G和M等待执行。G指的是Goroutine,本质上其实是一个轻量级的线程。包括调用栈,重要的调度信息,比如channel等。每次调用go时,会:创建一个G对象,如果还有空闲的P,则将其加入本地队列或全局队列,然后创建一个MM启动一个底层线程,执行G的执行顺序循环中能找到的tasks有的,先从本地队列找,如果本地没有,就从全局队列找(一次性转(全局G号/P号),然后再去给其他P找它(一次性转一半)对于上面2-3第一步是创建一个M。过程:先找一个空闲的P,没有人直接返回。(哈哈,这个place保证进程占用不会超过你设置的cpus数)调用系统api创建线程,不同的操作系统,调用不同,其实和c语言的创建过程是一致的,然后创建的线程才是真正要做的事情,循环执行G任务,协程阻塞切换时:M0转P给createM1接管P,其任务队列继续执行其他G。当阻塞结束后,M0会尝试获取一个空闲的P,如果失败,会将当前G放到全局队列的末尾。这里需要注意三点:1.M和P的关系数字没有绝对的关系。如果一个M被阻塞,P会创建或切换另一个M。因此,即使P的默认数量为1,也可能会创建很多M。2、创建P时:在确定了P的最大数量n之后,runtime系统会根据这个数量创建n个P。3、M创建时:M不足关联P,运行可运行的G。比如此时所有的M都阻塞了,P中有很多就绪任务,就会寻找空闲的M,没有空闲的就新建一个M。总结一下go协程的特点:数据协程之间需要保证安全,比如通过Channel或者lock。可以使用多核并行执行。协程不是完全同步的,可以并行运行,具体取决于通道的设计。抢占式调度可能不公平。协程(python)和goroutine(go)的区别在于python、C#、Lua语言都支持协程特性。Coroutine和goroutine在名字上很相似,都是可中断可恢复的协程。它们最大的区别是goroutine可能在多个核上并行执行,但是coroutine始终是Sequential执行的。基于此,我们应该知道协程适用于IO密集型程序,goroutine在IO密集型和CPU密集型程序中都有很好的表现。但话又说回来,go比python快吗?如果完全是IO并发密集型程序,python表现更好,因为在单线程中协程切换的效率更高。在运行机制上,协程的运行机制属于协同任务处理。程序需要主动交出控制权,让宿主获得控制权,交给其他协程。如果开发者无意或有意让应用程序长时间占用CPU,操作系统对此无能为力,其结果是计算机很容易变得无响应或死机。goroutine属于抢占式任务处理,与现有的多线程、多进程任务处理非常相似,虽然它无法控制自己获得高优先级的支持。但是,如果发现某个应用程序长时间消耗大量CPU,用户有权终止该任务。从协程来看:线程对应:N:1,Python协程模式,多个协程在一个线程中切换。IO密集时切换效率高,但没有采用多核1:1,Java多线程模式,每个协程只在一个线程中运行,所以协程和线程没有区别,虽然多核是用的,但是线程切换开销大。M:N,go模式,多个协程在多个线程上切换,既可以使用多核,又可以减少切换开销。(当都是cpu密集型时,切换到多核;当都是io密集型时,切换到单核)。从协程通信和调度机制来看: