⭐️1.概述在操作系统中,有多种IO模型,从传统的阻塞IO、到非阻塞IO、再到多路复用IO以及暂未成熟的异步IO。为了解其思想,这里整理了一份这几种IO的模拟流程。代码使用go语言进行模拟。⚠️注意:这里仅使用go语言协程和channel模拟各种IO模型的思想,不涉及IO模型的底层实现,实际
在操作系统中,有多种IO模型,从传统的阻塞IO、到非阻塞IO、再到多路复用IO以及暂未成熟的异步IO。为了解其思想,这里整理了一份这几种IO的模拟流程。代码使用go语言进行模拟。
⚠️ 注意:这里仅使用go语言协程和channel模拟各种IO模型的思想,不涉及IO模型的底层实现,实际上IO模型远比模拟的要复杂得多。本文更未深入探讨操作系统底层的select、poll、epoll的实现。仅供初学者了解IO模型的思想使用。
I/O(输入/输出)模型描述了应用程序如何与操作系统进行数据交换,特别是如何处理网络或磁盘 I/O 操作。不同 I/O 模型的主要区别在于等待数据可用的方式和数据从内核到用户空间的方式。
I/O模型主要用在优化I/O操作时的性能,提高系统的并发处理能力。在这里,我们主要探讨的是网络IO,这也是现代高性能服务器主要实现的方面。
对于网络IO,我们首先需要了解的是,客户端到服务端上的一条请求,主要走这条路径:客户端 --> 服务器 --> 业务代码。基于这条路径上,我们主要研究的是第一阶段的IO优化。在第一阶段上,使用各种IO模型对服务器性能进行优化。
废话不多说,直接上图。 如图所示,在同步IO同步过程中,网络请求打到服务器上是需要进行一对一绑定线程的。服务器需要等待网络请求完成才能释放这个线程锁占用的资源。那么在网络IO请求的时候,进行了长时间的阻塞,就会导致这个线程一直无法释放。
举个例子来说,一个线程假设占用内存1M,100000个请求就得占用100g的内存资源,对于服务器来说,是十分浪费的。
func Test_BIO(t *testing.T) {
client := resty.New()
// BIO GET
resp, err := client.R().
SetQueryParams(map[string]string{
"search": "golang",
"page": "1",
}).
SetHeader("Accept", "application/json").
Get("https://httpbin.org/get")
if err != nil {
log.Fatal(err)
}
fmt.Println("Status Code:", resp.StatusCode())
fmt.Println("Response Body:", resp.String())
}
因为阻塞IO需要占用大量的计算以及内存资源,那么有没有办法让一个线程非阻塞处理多个IO请求呢?有的,NIO就是一种解决方案。
在操作系统中,NIO机制当然不是使用协程来进行实现的,需要进行内核的调用, 实际是采用for循环发送请求来实现的(非协程)。但今天,我们并不研究这么深入,在这里,我们仅探讨非阻塞IO的思想。
在阻塞IO场景中,主要是要解决 客户端 ---> 服务器 这条路径上需要启动多个线程的问题。那么,假如我们可以在一个线程中,绑定多个网络IO请求,那么就可以减少IO的停顿时间,提高系统的并发量了。
在这里,我们采取了go语言的协程模拟实现在单个线程中绑定多个网络IO请求。
// goroutine模拟NIO
func TestNIO(t *testing.T) {
var wg sync.WaitGroup
ch := make(chan string, 3)
urls := []string{
"https://httpbin.org/get?name=go1",
"https://httpbin.org/get?name=go2",
"https://httpbin.org/get?name=go3",
}
// NIO GET
for _, url := range urls {
wg.Add(1)
go func(*sync.WaitGroup, chan string, string) {
defer wg.Done()
client := resty.New()
resp, err := client.R().Get(url)
if err != nil {
ch <- fmt.Sprintf("Request to %s failed: %v", url, err)
return
}
ch <- fmt.Sprintf("Response from %s: %d", url, resp.StatusCode())
}(&wg, ch, url)
}
// 等待所有请求完成
wg.Wait()
close(ch)
// 打印结果
for msg := range ch {
fmt.Println(msg)
}
}
利用NIO,我们可以实现在 客户端 ---> 服务器 这条路径上的IO优化,使得每个IO请求无须等待返回结果,但是最后,NIO仍然是需要对网络IO请求轮询一一检查调用结果的。
那么,有没有办法将很多IO请求映射到少量线程中呢?有的,操作系统中,实际是使用select、poll、epoll的机制来实现IO请求的多路复用。
在这里,我们将IO请求和协程分离进行模拟。
// multiple
func TestSimulateNIO(t *testing.T) {
type Job struct {
url string
idx int
}
// 任务队列
jobChan := make(chan Job, 10)
// 结果集
resultChan := make(chan string, 10)
// 将5个请求绑定到2个(少量)线程上,实现多路复用
workerCount := 2
for i := 0; i < workerCount; i++ {
go func(workerID int) {
for job := range jobChan {
client := resty.New()
resp, err := client.R().Get(job.url)
if err != nil {
resultChan <- fmt.Sprintf("[Worker %d] Request %d failed: %v", workerID, job.idx, err)
continue
}
resultChan <- fmt.Sprintf("[Worker %d] Response %d: %d", workerID, job.idx, resp.StatusCode())
}
}(i)
}
// 模拟5个请求
urls := []string{
"https://httpbin.org/get?name=go1",
"https://httpbin.org/get?name=go2",
"https://httpbin.org/get?name=go3",
"https://httpbin.org/get?name=go4",
"https://httpbin.org/get?name=go5",
}
// 模拟 Selector:统一调度任务
go func() {
for i, url := range urls {
jobChan <- Job{url: url, idx: i + 1}
}
close(jobChan)
}()
// 收集结果
for i := 0; i < len(urls); i++ {
fmt.Println(<-resultChan)
}
}
对于BIO、NIO、多路复用来说,他们始终是会有一小部分的同步阻塞的问题。但未来,真正异步的AIO可能成为主流,他的思想是:将获取结果这一步,完全交给操作系统内核来进行通知。用户态中,程序无须进行任何停顿。达到AIO的目的。
在这里,我们采用回调机制来模拟AIO的实现。
func asyncRequest(url string, callback func(string)) {
go func() {
client := resty.New()
resp, err := client.R().Get(url)
if err != nil {
callback(fmt.Sprintf("Request to %s failed: %v", url, err))
return
}
callback(fmt.Sprintf("Response from %s: %d", url, resp.StatusCode()))
}()
}
func TestSimulateAIO(t *testing.T) {
urls := []string{
"https://httpbin.org/get?name=go1",
"https://httpbin.org/get?name=go2",
"https://httpbin.org/get?name=go3",
}
var wg sync.WaitGroup
wg.Add(len(urls))
for _, url := range urls {
asyncRequest(url, func(result string) {
fmt.Println(result)
wg.Done()
})
}
wg.Wait()
}
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!