golang 协程和IO多路复用

协程和IO多路复用



IO阻塞问题


我们知道。通过操作系统记录的进程控制信息,可以找到打开文件描述符表。其中,打开的文件、创建的socket等等,都会记录到这张表里面。


在这里插入图片描述


socket的所有操作都由操作系统来提供,也就是要通过系统调用来完成。每创建一个socket,就会在打开文件描述符表中对应增加一条记录,而返回给应用程序的只有一个socket描述符,用于识别不同的socket。而且每个TCP socket在创建时,操作系统都会为它分配一个读缓冲区和一个写缓冲区


在这里插入图片描述

要获得响应数据,就要从读缓冲区拷贝过来。同样的要通过socket发送数据,也要先把数据拷贝到写缓冲区才行。所以,问题出现了:
用户程序想要读数据的时候,读缓冲区里未必有数据,想发送数据的时候,写缓冲区里也未必有空间。那怎么办?

在这里插入图片描述

阻塞式IO

第一种办法是阻塞式IO,即乖乖的让出CPU,进到等待队列里。等socket就绪后,再次获得时间片就可以继续执行了。


在这里插入图片描述

使用阻塞式IO,要处理一个socket就要占用一个线程。等这个socket处理完成才能接手下一个,这在高并发场景下会加剧调度开销。


在这里插入图片描述

非阻塞式IO


第二种方法是:非阻塞式IO,也就是不让出CPU。但是需要频繁的检查socket是否就绪了,这是一种忙等待的方式,很难把握轮询的间隔时间,容易造成空耗CPU,加剧响应延时。


在这里插入图片描述

IO多路复用


第三种办法就是IO多路复用,由操作系统提供支持,把需要等待的socket加入到监听集合,这样就可以通过一次系统调用,同时监听多个socket。
有socket就绪了,就可以逐个处理了。既不用为等待某个socket而阻塞,也不会陷入忙等待之中。


在这里插入图片描述

Linux提供了三种IO多路复用实现方式,分别是selectpollepoll


select


第一种是select
我们可以设置要等待的描述符,也可以设置等待超时时间。如果有准备好的fd,或达到指定超时时间,select函数就会返回。


在这里插入图片描述

从函数签名来看,它支持监听可读、可写、异常三类事件。
因为这个fd_set是个unsigned long型的数组。共16个元素,每一位对应一个fd,最多可以监听1024个,这就有点少了。
而且每次调用select都要传递所有的监听集合。这就需要频繁的从用户态到内核拷贝数据。除此之外,即便有fd就绪了,也需要遍历整个监听集合,来判断哪个fd是可操作的。这些都会影响性能。


在这里插入图片描述

poll


第二种IO多路复用的实现方式是poll
虽然支持的fd数目,等于最多可以打开的文件描述符个数。但是另外两个问题依然存在。


epoll


epoll就没有这些问题了,它提供三个接口。

  • epoll_create1用于创建一个epoll,并获取一个句柄。
  • epoll_ctl用于添加或删除fd与对应的事件信息。
  • 除了指定fd和要监听的事件类型,还可以传入一个event data,通常会按需定义一个数据结构,用于处理对应的fd。可以看到,每次都只需传入要操作对的一个fd,无需传入所有监听集合,而且只需要注册这一次。通过epoll_wait得到的fd集合都是以及就绪的,逐个处理即可,无需遍历所有监听集合。

在这里插入图片描述


通过IO多路复用,线程再也不用为等待某一个socket,而阻塞或空耗CPU。并发处理能力因而大幅提升。


IO多路复用结合协程


但是IO多路复用也并非没有问题,例如:一个socket可读了,但是这回只读到了半条请求,也就是说需要再次等待这个socket可读。在继续处理下一个socket之前,需要记录下这个socket的处理状态。下一次这个socket可读时,也需要恢复上次保存的现场,才好继续处理。
也就是说,在IO多路复用中实现业务逻辑时,我们需要随着事件的等待和就绪,而频繁的保存和恢复现场,这并不符合常规的开发习惯。如果业务逻辑比较简单还好,若是比较复杂的业务场景,就有些悲剧了。


在这里插入图片描述


既然业务处理过程中,要等待事件时,需要保存现场并切换到下一个就绪的fd。而事件就绪时,又需要恢复现场继续处理。那岂不是很适合使用协程?


在IO多路复用这里,事件循环依然存在,依然要在循环中逐个处理就绪的fd,但处理过程却不是围绕具体业务,而是面向协程调度。
如果是用于监听端口的fd就绪了,就建立连接创建一个新的fd,交给一个协程来负责, 协程执行入口就指向业务处理函数入口,业务处理过程中,需要等待时就注册IO事件,然后让出,这样,执行权就会回到切换到该协程的地方继续执行。如果是其它等待IO事件的fd就绪了,只需要恢复关联的协程即可。

协程拥有自己的栈,要保存和恢复现场都很容易实现。这样,IO多路复用这一层的事件循环,就和具体业务逻辑解耦了。

可以把read、write、connect等可能会发生等待的函数包装一下,在其中实现IO事件注册与主动让出。这样在业务逻辑层面,就可以使用这些包装函数,按照常规的顺序编程方式,来实现业务逻辑了。

这些包装函数在需要等待时,就会注册IO事件,然后让出协程,这样我们在实现业务逻辑时,就完全不用担心保存与恢复现场的问题了。


在这里插入图片描述


协程和IO多路复用之间的合作,不仅保留了IO多路复用的高并发性能,还解放了业务逻辑的实现。


协程与IO多路复用结合的项目:

https://github.com/fengyoulin/ef


参考资料:

https://www.bilibili.com/video/BV1a5411b7aZ?spm_id_from=333.999.0.0

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
协程是轻量级的线程,可以在同一个程序中并发地执行多个任务。通过使用协程,我们可以更有效地利用计算资源并实现并发编程。而管道是用于在协程之间传递数据的通信机制。在Go语言中,我们可以使用管道来实现协程之间的同步和通信。 在Go语言中,我们可以通过以下步骤来使用协程和管道: 1. 使用关键字"go"来创建一个协程,让其并发执行一个函数或方法。 2. 使用"make"函数来创建一个管道,并指定其元素类型和容量。管道可以是有缓冲的(指定了容量)或者无缓冲的(未指定容量)。 3. 在协程中,使用"<-"操作符将数据发送到管道中,或者从管道中接收数据。 4. 如果管道是无缓冲的,发送操作和接收操作会导致发送方和接收方都会阻塞,直到对应的操作完成。这种情况下,协程之间的通信是同步的。 5. 如果管道是有缓冲的,发送操作只有在管道已满时才会阻塞,接收操作只有在管道为空时才会阻塞。这种情况下,协程之间的通信是异步的。 下面是一个示例代码来演示协程和管道的使用: ```go package main import ( "fmt" ) func worker(id int, jobs <-chan int, results chan<- int) { for j := range jobs { fmt.Println("worker", id, "processing job", j) results <- j * 2 } } func main() { jobs := make(chan int, 5) results := make(chan int, 5) // 创建3个协程来并发执行任务 for w := 1; w <= 3; w++ { go worker(w, jobs, results) } // 发送5个任务到管道中 for j := 1; j <= 5; j++ { jobs <- j } close(jobs) // 从结果管道中接收并打印结果 for r := 1; r <= 5; r++ { fmt.Println(<-results) } } ``` 在这个示例中,我们创建了一个有缓冲的"jobs"管道和一个有缓冲的"results"管道。然后,我们创建了3个协程来并发执行任务。每个协程从"jobs"管道中接收任务,处理任务后将结果发送到"results"管道中。最后,主函数从"results"管道中接收并打印结果。 希望这个示例能够帮助你理解如何在Go语言中使用协程和管道。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值