【并发编程】通过生产-消费者了解协程

admin 2024年5月24日22:29:37评论5 views字数 1814阅读6分2秒阅读模式

免责声明

【并发编程】通过生产-消费者了解协程

文章中所有内容仅供学习交流使用,不用于其他任何目的,抓包内容、敏感网址、数据接口均已做脱敏处理,严禁用于商业和非法用途,否则由此产生的一切后果与作者无关。若有侵权,请在公众号【爬虫逆向小林哥】联系作者

【并发编程】通过生产-消费者了解协程

01

引子

最近在封装基于coroutine的异步爬虫框架,在任务调度这块采用基于内存的asyncio.Queue当作任务池,遇到了有意思的事情,这里分享下

【并发编程】通过生产-消费者了解协程

考虑到给队列限定maxsize,因此生产者发送任务进入队列会存在IO阻塞,队列发出任务到消费者也会存在IO阻塞,消费者拿到任务做出请求一种IO。我们需要调度协程让整个程序宏观上上达到并行的效果(python的coroutine本质上是单线程的产物,因此不可能实现并发),不然在while循环中整个程序会永久阻塞在一方。

02

源码

先来看正确的逻辑,生产和消费并发运行

import asynciofrom asyncio import Queuequeue = Queue(maxsize=6)async def consumer():    for _ in iter(int, 1):        res = await queue.get()        print("消费: {}".format(res))async def producer():    num = 1    for _ in iter(int, 1):        await queue.put(num)        print("生产: {}".format(num))        num = num + 1async def run():    await asyncio.gather(        *[asyncio.create_task(producer())],  # 生产        *[asyncio.create_task(consumer()) for _ in range(5)],  # 消费    )asyncio.run(run())

【并发编程】通过生产-消费者了解协程

再来看错误的逻辑,只需要把生产者改为下面的代码

async def producer():    num = 1    for _ in iter(int, 1):        try:            queue.put_nowait(num)            print("生产: {}".format(num))        except:            print("队列已满")        num = num + 1

【并发编程】通过生产-消费者了解协程

会发现整个程序都停留在生产者上面,协程没有切换到消费者那里,更改的地方在于put的方式改为no_wait,也就意味着当队列满了之后put并不会发生阻塞,而是抛出异常(可以从pu_nowait源码看到)

【并发编程】通过生产-消费者了解协程

得出的结论就是:协程没有遇到阻塞也就不会发生切换,那这里的阻塞需要怎样定义呢?

一般都会说协程是用户态行程,简单说就是协程的切换跟线程不一样,完全是由你我决定的,而不是操作系统或者cpu内核。

【并发编程】通过生产-消费者了解协程

在async await协程实现中,当遇到await关键字时候(识别为阻塞),整个协程的事件循环就会把当前协程挂起,执行别的协程,直到其他协程也挂起,或者执行完毕,进行下一个协程的执行。

【并发编程】通过生产-消费者了解协程

这时候就很好理解上面发生的情况了

03

分析

本质上python协程还是基于单线程的,无法做到微观上生产、消费者并行的效果,而是采用用户态阻塞切换的形式带来并发。

但是有过Golang-goroutine编程经验的小伙伴就会知道go的协程是可以动态分配到不同的线程上面运行!这也是go的牛逼之处:

Go 语言中的协程是由 Go 运行时调度器(scheduler)进行管理和调度的。当程序启动时,Go 运行时会默认启动一个主协程,主协程会创建其他的子协程,这些协程会被分配到不同的系统线程上进行执行。当某个协程发生阻塞时,Go 运行时会将该协程挂起并让出 CPU,转而执行其他协程,以充分利用系统资源

package mainimport (  "fmt"  "os"  "os/signal"  "syscall")func aaa() {  for {    fmt.Println("aaa")  }}func bbb() {  for {    fmt.Println("bbb")  }}func main() {  go aaa()  go bbb()  sigterm := make(chan os.Signal, 1)  signal.Notify(sigterm, syscall.SIGINT, syscall.SIGTERM)  select {  case <-sigterm:  }}

【并发编程】通过生产-消费者了解协程

上述真正意义上实现了并行

Golang的调度器GMP巧妙的将协程分配到内核不同的线程来运行

【并发编程】通过生产-消费者了解协程

04

归纳总结

【并发编程】通过生产-消费者了解协程

【并发编程】通过生产-消费者了解协程

添加好友回复:交流群

原文始发于微信公众号(爬虫逆向小林哥):【并发编程】通过生产-消费者了解协程

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年5月24日22:29:37
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【并发编程】通过生产-消费者了解协程http://cn-sec.com/archives/2774049.html

发表评论

匿名网友 填写信息