python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python Golang协程区别

Python和Golang协程的区别

作者:张宝剑 PFinalClub

这篇文章主要为大家介绍了Python和Golang协程的区别示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

背景

最近在做后端服务 Python 到 Go 的迁移和重构,这两种语言里,最大的特色和优势就是都支持协程。之前主要做python的性能优化和架构优化,一开始觉得两个协程原理和应用应该差不多,后来发现还是有很大的区别,今天就在这里总结一下。

什么是协程

在说它们两者区别前,我们首先聊一下什么是协程,好像它没有一个官方的定义,那就结合平时的应用经验和学习内容来谈谈自己的理解。

协程,其实可以理解为一种特殊的程序调用。特殊的是在执行过程中,在子程序(或者说函数)内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。

注意,它有两个特征:

和进程线程的区别

上面两个特点就导致了它相对于线程和进程切换来说极高的执行效率,为什么这么说呢?我们先老生常谈地说一下进程和线程。

进程是操作系统资源分配的基本单位,线程是操作系统调度和执行的最小单位。这两句应该是我们最常听到的两句话,拆开来说,进程是程序的启动实例,拥有代码和打开的文件资源、数据资源、独立的内存空间。线程从属于进程,是程序的实际执行者,一个进程至少包含一个主线程,也可以有更多的子线程,线程拥有自己的栈空间。无论是进程还是线程,都是由操作系统所管理和切换的。

我们再来看协程,它又叫做微线程,但其实它和进程还有线程完全不是一个维度上的概念。进程和线程的切换完全是用户无感,由操作系统控制,从用户态到内核态再到用户态。而协程的切换完全是程序代码控制的,在用户态的切换,就像函数回调的消耗一样,在线程的栈内即可完成。

Python的协程(coroutine)

Python 的协程其实是我们通常意义上的协程 coroutine。

从概念上来讲,Python 的协程同样是在适当的时候可中断可恢复。那么什么是适当的时候呢,就是你认为适当的时候,因为程序在哪里发生协程切换完全控制在开发者手里。当然,对于 Python 来说,由于 GIL 锁,在 CPU 密集的代码上做协程切换是没啥意义的,CPU 本来就在忙着没偷懒,切换到其他协程,也只是在单核内换个地方忙而已。很明显,我们应该在 IO 密集的地方来起协程,这样可以让 CPU 不再空等转而去别的地方干活,才能真正发挥协程的威力。

从实现上来讲,如果熟知了 Python 生成器,还可以将协程理解为生成器+调度策略,生成器中的 yield 关键字,就可以让生成器函数发生中断,而调度策略,可以驱动着协程的执行和恢复。这样就实现了协程的概念。这里的调度策略可能有很多种,简单的例如忙轮循:while True,更简单的甚至是一个 for 循环。就可以驱动生成器的运行,因为生成器本身也是可迭代的。复杂的比如可能是基于 epool 的事件循环,在 Python2 的 tornado 中,以及 Python3 的 asyncio 中,都对协程的用法做了更好的封装,通过 yield 和 await 就可以使用协程,通过事件循环监控文件描述符状态来驱动协程恢复执行。

我们看一个简单的协程:

import time
def consumer():
    r = ''
    while True:
        n = yield r
        if not n:
            return
        print('[CONSUMER] Consuming %s...' % n)
        time.sleep(1)
        r = '200 OK'
def produce(c):
    c.next()
    n = 0
    while n < 5:
        n = n + 1
        print('[PRODUCER] Producing %s...' % n)
        r = c.send(n)
        print('[PRODUCER] Consumer return: %s' % r)
    c.close()
if __name__ == '__main__':
    c = consumer()
    produce(c)

很明显这是一个传统的生产者-消费者模型,这里 consumer 函数就是一个协程(生成器),它在 n = yield r 的地方发生中断,生产者 produce 中的 c.send(n) ,可以驱动协程的恢复,并且向协程函数传递数据n,接收返回结果r。而while n < 5,就是我们所说的调度策略。在生产中,这种模式很适合我们来做一些 pipeline 数据的消费,我们不需要写死几个生产者进程几个消费者进程,而是用这种协程的方式,来实现CPU动态地分配调度。

如果你看过上篇文章的话,是不是发现这个 Go 中流水线模型有点像呢,也是生产者和消费者间进行通信,但 Go是通过 channe l这种安全的数据结构,为什么 Python 不需要呢,因为 Python 的协程是在单线程内切换本身就是安全的,换句话说,协程间本身就是串行执行的。而 Go 则不然。思考一个有意思的问题,如果我们将 Go 流水线模型中channel设置为无缓冲区时,生产者绝对驱动消费者的执行,是不是就跟.Python 很像了呢。

所以 Python 的协程从某种意义来说,是不是 Go 协程的一种特殊情况呢?后端在线服务中我们更常用的 Python 协程其实是在异步IO框架中使用,之前我们也提过 Python 协程在 IO 密集的系统中使用才能发挥它的威力。

Python协程的特点

Golang的协程(goroutine)

Golang 的协程就和传统意义上的协程不大一样了,兼具协程和线程的优势。这也是 Golang 最大的特色,就是从语言层面支持并发。Go语言里,启动一个goroutine很容易:go function 就行。

同样从概念上来讲,Golang 的协程同样是在适当的时候可中断可恢复。当协程中发生 channel 读写的阻塞或者系统调用时,就会切换到其他协程。具体的代码示例可以看上篇文章,就不再赘述了。

从实现上来说,Goroutine 可以在多核上运行,从而实现协程并行,我们先直接看下 Golang 的调度模型 MPG。

M指的是Machine,一个M直接关联了一个内核线程。由操作系统管理。P指的是processor,代表了M所需的上下文环境,也是处理用户级代码逻辑的处理器。它负责衔接 M 和 G 的调度上下文,将等待执行的G与M对接。G指的是Goroutine,其实本质上也是一种轻量级的线程。包括了调用栈,重要的调度信息,例如 channel 等。

每次 go 调用的时候,都会:

对于上面的第2-3步,创建一个 M,其过程:

当协程发生阻塞切换时:

这里我们需要注意三点:

1、M 与 P 的数量没有绝对关系,一个 M 阻塞,P 就会去创建或者切换另一个 M,所以,即使P的默认数量是 1,也有可能会创建很多个 M 出来。

2、P 何时创建:在确定了 P 的最大数量 n 后,运行时系统会根据这个数量创建 n 个 P。

3、M 何时创建:没有足够的 M 来关联P并运行其中的可运行的 G。比如所有的 M 此时都阻塞住了,而 P 中还有很多就绪任务,就会去寻找空闲的 M,而没有空闲的,就会去创建新的 M。

Golang协程的特点

coroutine(Python)和 Goroutine(Golang)的区别

除了 Python,C#, Lua 语言都支持 coroutine 特性。coroutine 与 goroutine 在名字上类似,都是可中断可恢复的协程,它们之间最大的不同是,goroutine 可能在多核上发生并行执行,单但 coroutine 始终是顺序执行。也基于此,我们应该清楚 coroutine 适用于 IO 密集程序中,而 goroutine 在 IO 密集和 CPU 密集中都有很好的表现。不过话说回来,Golang 就一定比 Python 快么,假如在完全IO并发密集的程序中,Python 的表现反而更好,因为单线程内的协程切换效率更高。

从运行机制上来说,coroutine 的运行机制属于协作式任务处理, 程序需要主动交出控制权,宿主才能获得控制权并将控制权交给其他 coroutine。如果开发者无意间或者故意让应用程序长时间占用 CPU,操作系统也无能为力,表现出来的效果就是计算机很容易失去响应或者死机。goroutine 属于抢占式任务处理,已经和现有的多线程和多进程任务处理非常类似, 虽然无法控制自己获取高优先度支持。但如果发现一个应用程序长时间大量地占用 CPU,那么用户有权终止这个任务。

从协程:线程的对应方式来看:

从协程通信和调度机制来看:

其他文章的对比

https://www.jb51.net/article/160979.htm

以上就是Python和Golang协程的区别的详细内容,更多关于Python和Golang协程的区别的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文