单线程模型想象不到的高并发能力、多路复用是效率杠杆

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 单线程模型想象不到的高并发能力、多路复用是效率杠杆


🍊 多路复用

🎉 redis的多路复用模式

redis使用模型有:select、poll、epoll。这里简单讲二种。

📝 应用对外提供服务的过程

假设你有一个在线游戏应用,你想向全世界的用户提供游戏服务,那么应用对外提供服务的过程是如何进行的呢?

首先,你需要创建一个套接字来监听客户端的请求。在这个过程中,你可以指定监听的端口号,例如8080端口。当客户端发送一个请求到这个端口时,服务器就会接收到这个请求。

接下来,你需要绑定端口号,这个过程可以理解为在服务器上挂一个虚拟的标签,标明这个端口已经被你的应用占用。

开始监听就像打开你的应用大门,让用户进入你的游戏世界。当客户端连接到这个端口时,服务器会接收到这个连接请求,然后生成一个新的套接字,用于和客户端之间的通信。

如果需要同时处理多个连接,你可以使用多线程或多进程的方式进行处理。例如,在一个小时内,你的游戏应用可能会有数百个用户同时在线,这时你需要使用多线程或多进程来处理这些请求,以提高服务器的处理能力。

当客户端发送一个请求时,服务器会接收到该请求,并根据请求的内容来进行相应的处理。例如,如果客户端想要连接到游戏服务器并开始游戏,服务器会根据请求头来判断请求的类型和具体内容,并根据需要执行相应的处理逻辑,例如根据用户的游戏账号和密码来验证身份。

在处理完请求之后,服务器会通过套接字将响应发送回客户端。在这个过程中,服务器需要把响应的数据按照一定的数据协议进行封装,例如使用HTTP协议,在响应头中添加一些元数据,然后将数据通过套接字发送到客户端。

客户端收到响应之后会进行相应的处理,例如展示游戏画面和音效,然后等待用户的操作。当用户进行操作时,例如移动游戏角色,客户端会生成新的请求,发送到服务器端,然后服务器按照同样的逻辑处理这个请求,并返回相应的响应结果。

在整个过程中,套接字的作用就是提供了一个通讯的通道,使得服务器和客户端能够相互通信。其中,服务器会根据连接请求生成一个新的套接字,用于和客户端之间的通信,而客户端则会使用一个已有的套接字来和服务器进行通信。

在实际的应用场景中,还需要考虑到一些安全性和性能方面的问题。例如,为了保证安全性,可能需要对请求进行一些加密和验证操作,例如使用HTTPS协议,在传输层对数据进行加密;而为了提高性能,则需要使用一些优化技术,如缓存、负载均衡等,来优化服务器的处理能力。

📝 select

你是否想过,当你在聊天软件上发送消息时,你的电脑是如何处理这个消息并发送给对方的呢?或是当你在浏览网页的时候,你的电脑是如何处理并返回给你网页上的信息的呢?这些看起来平凡无奇的操作,其实背后隐藏着一些重要的原理和技术。其中一个就是我们今天所讲的“select”。

现在假设你需要同时处理多个客户端的请求,而又不能让主线程阻塞,这时,我们就需要使用多线程来处理这些请求。但是,如果我们使用主线程处理read,就会发生一件让人头疼的事情:主线程只能处理一个连接,而还有其他的连接需要处理。这时候,我们就需要使用select来解决这个问题。

那么,什么是select呢?简单来说,select是一种I/O复用模型,它能够让一个线程同时处理多个连接的请求。它的工作原理是:用户线程批量将要查询的连接发给操作系统,这个过程只发生一次进程的切换,用户线程告诉操作系统需要哪些数据,操作系统遍历查找,然后将结果返回给用户线程。

为了更好地理解,举个例子。假设你正在使用一个在线聊天软件,并且你和多个人同时聊天。每个人的聊天都是一个连接,这些连接都会被分配一个文件描述符。通过select,我们可以把这些文件描述符批量查询,然后操作系统就可以同时处理多个连接的请求,而不是一个一个地处理。这样,当客户端的连接数增多时,我们也不用再开多个线程去处理连接,而是可以在同一个线程中处理所有连接。

除此之外,select还有一个重要的特点:它能够让我们同时监控多个I/O流的状态,包括读、写和异常。这意味着,当一个客户端向服务器发送数据时,操作系统不用再去轮询每个连接的状态,而是可以直接通过select来监测数据的到来。这样一来,我们就可以更高效地处理连接,提高整个程序的性能。

不过,也要注意到,select也有一定的局限性。当文件描述符较多时,它的效率也会下降,因为需要扫描所有的文件描述符。同时,在某些系统中,select也可能有一些不够稳定的问题。因此,针对不同的应用场景,我们需要选择合适的I/O模型来进行处理。

📝 epoll

你是否有过这样的经历,想要找到一根针在大海里,却不知道具体在哪里?这就像我们在开发程序时遇到的一个问题:在海量的连接中寻找我们所需要的数据。这个问题看上去很简单,但实际上需要处理的操作却非常的繁琐,如果按照传统的方式去处理,那么我们就需要遍历所有的连接,一个一个地去寻找我们需要的数据,这不仅费时还费力,而且会严重占用系统资源。那么,该怎么办呢?这就需要引入今天要介绍的话题,epoll技术。

epoll技术是一种高效的I/O多路复用机制,主要是通过将我们要读取的文件描述符列表交给操作系统来维护,当有新的数据到达时,操作系统会通知我们需要的线程来进行读取操作。在这个过程中,操作系统会直接将我们所需的数据返回给我们,而不需要像传统的方式那样去遍历所有的连接,这样就大大减少了系统资源的占用,提高了程序的性能。

那么,epoll技术是如何实现的呢?我们可以通过一个简单的例子来帮助大家理解。

假设我们有10万个连接,其中只有一个连接会传输数据,那么我们每次查询都要将这10万个连接全部传过去,这样就会产生9999次无效操作。而如果使用epoll技术,我们可以直接知道哪些连接是有数据的,然后操作系统会通知我们需要的线程,这样我们就可以直接通过这个连接去读取数据了,不需要遍历所有的连接。

在实际的开发中,我们可以通过建立一个需要回调的连接,将需要监听的文件描述符都扔给操作系统,当有新数据到达时,操作系统会直接返回给我们所需的线程。我们将监听的列表交给操作系统维护,这样当有新数据来的时候,操作系统知道这是我们需要的,等我们下次来拿的时候,直接给我们就好了,这样就大大减少了遍历的次数,提高了程序的效率。

📝 多路复用的定义

“多路”指的是多个网络连接,“复用”指的是复用同一个线程。

采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗)。

📝 多路复用的举例

Redis是一种流行的内存数据库,但它需要同时处理多个IO请求并将它们的结果返回给客户端。由于Redis的命令处理是单线程的,所以它需要使用IO多路复用技术来暂停对某个IO事件的处理,转而去处理另一个IO事件,以便同时处理多个请求并提高性能。

想象一下你是一个忙碌的服务员,需要同时为多个客人提供服务。如果你只专注于为一个客人服务,那么其他客人就不得不等待。这会浪费时间,可能会让客人感到不满。所以你需要使用IO多路复用技术,像Redis一样,把注意力分散到多个客人身上,以便更快地为所有客人提供服务。

具体来说,当一个客户端连接到Redis并发送请求时,Redis会将请求放入队列中。当Redis处理第一个请求时,它会进行IO操作并在等待结果时暂停处理其他请求。但是,使用IO多路复用技术,Redis可以同时等待多个IO事件,而无需暂停对其他请求的处理。这种方法提高了Redis的性能,可以更快地处理多个请求并提供更好的用户体验。

举个例子,假设你是一个餐厅服务员,同时需要为多桌客人提供服务。每一个桌子都是一个IO事件。如果你只专注于为一个桌子服务,那么其他桌子的客人就会不满意。但是如果你能同时为多个桌子服务,就可以更快地为所有客人提供服务,而不必让他们等待。这就是IO多路复用技术的优势。

📝 多路复用的实现

I/O多路复用也存在着不同的实现方式,它们分别是select,poll和epoll。

select是最常见的I/O多路复用的实现方式之一。它是一种“懒惰”的方式,因为它需要不停地轮询每个socket,来看看是否有数据需要读取或写入,因此也被称为轮询方式。

当我们需要同时监听多个socket时,调用select函数,将要监听的socket的文件描述符注册进去,然后在select函数中阻塞等待,直到有任何一个socket有数据写入或读取,select函数就会返回,我们就可以开始处理数据了。如果有多个socket有数据写入或读取,我们只需要轮流处理即可。

然而,select也存在着一些问题。首先,它的效率比较低,因为它需要不停地轮询,如果监听的socket数量较多的话,轮询需要的时间就会很长,而且每次调用select函数都需要重新传入要监听的socket,增加了不必要的开销。其次,select只能监听最大文件描述符数量,由系统定义,通常是1024,如果要监听的socket数量超过了这个限制,就不能使用select了。

poll是select的改进版,它解决了select只能监听最大文件描述符数量的问题。poll使用的是链表来存储要监听的socket,因此它没有文件描述符数量的限制。

poll实现方式的原理和select相似。当我们需要同时监听多个socket时,调用poll函数,将要监听的socket的文件描述符注册进去,然后在poll函数中阻塞等待,直到有任何一个socket有数据写入或读取,poll函数就会返回,我们就可以开始处理数据了。如果有多个socket有数据写入或读取,我们只需要轮流处理即可。

与select相比,poll的效率有所提高,因为它使用链表来存储要监听的socket,不需要轮询所有socket,而且在每次调用poll函数时,我们可以只传入已经注册过的socket,不需要重复的操作。不过,poll和select一样,都存在着效率不高的问题。

epoll是I/O多路复用的又一种实现方式,它被广泛认为是最优秀的I/O多路复用实现方式。它的原理是通过内核维护一个事件表,将要监听的socket加入到事件表中,然后等待事件的发生。当有数据需要写入或读取时,内核会将事件添加到一个就绪队列中,然后epoll_wait函数就会返回,并将就绪队列中的事件返回给程序。

epoll的效率要比select和poll高很多,原因在于它采用了内核回调的机制,当有数据需要写入或读取时,内核会主动通知程序,而程序不需要不停地轮询所有socket,减少了不必要的开销。

🔥 过程一:数据未就绪

在一个社交网络应用中,可能会同时有许多用户在上传照片、发送消息、浏览个人主页等等。当这些请求同时到达时,怎么保证系统能够高效地处理呢?这个时候,就需要使用并发编程技术来提高系统的处理能力。

一个典型的并发编程场景就是多个客户端同时向服务器发起请求。在这个过程中,客户端的线程需要首先将socket监听列表添加到select中,然后由select调用操作系统的API进行处理。这个过程中,客户端的线程在激活select函数之前,一般会阻塞在等待数据到来的状态。

举个例子,假设有一个Web服务器,同时有100个客户端请求来访问某个Web页面。服务器需要同时处理这些请求,但是服务器资源有限,不能同时处理这么多请求。这个时候,就需要使用并发编程技术来提高服务器的处理能力。

当客户端的请求到达时,用户线程会将socket监听列表添加到select中,并调用操作系统的API进行处理。这个过程中,客户端线程处于阻塞状态,等待数据到来。此时,select函数会等待数据到来,并在数据到达时将结果返回给用户线程。如果有多个客户端同时发起请求,select函数会同时处理这些请求,并返回多个结果。

举个例子,如果有两个客户端同时向服务器发起请求,且数据同时到达,那么select函数会在两个socket监听结果都为可读时返回结果,告诉用户线程可以读取数据了。此时,用户线程才会正式发起read请求,读取数据,并将数据传送到服务器处理模块。

在这个过程中,用户线程的阻塞时间非常短,只有在等待数据到来时才会阻塞,而对于处理数据的过程则是非阻塞的。这个过程可以大大提高系统处理请求的效率,充分利用系统的资源,同时保证了系统的稳定性和可靠性。

需要注意的是,在并发编程中,由于多个线程同时操作同一个资源,可能会出现一些问题,例如死锁、竞态条件等等。因此,在实际编程中,需要采用一些技术来避免这些问题的出现,例如使用锁、信号量、互斥量等等。只有在正确地使用并发编程技术,才能最大限度地提高系统的处理能力,同时保证系统的稳定性和可靠性。

🔥 过程二:数据就绪

读函数,作为计算机网络中的一种重要方法,是我们进行数据收发的常用方式之一。它的作用就是在等待数据到达的同时,保证了我们的程序不会陷入无限等待的状态中。但是,要想真正理解它的原理和工作流程还需要更加详细的解释。

首先,我们来看看read函数的效果。它的作用在于读取网络上的数据,但如果没有数据到达时,它会立即返回一个错误值-1,而不是一直等待数据的到来。这个"立即返回"的过程就是非常关键的。它保证了我们的程序可以在不断开连接的情况下检查网络数据的到来,从而达到实时获取数据的目的。这个特点可以让我们更好地处理实时性较高的任务,比如网络游戏、直播等。

与此同时,我们还需要注意一个阻塞的过程。这个过程发生在操作系统通知用户线程数据已经准备就绪的时候,数据从内核缓冲区拷贝到用户缓冲区才通知用户进程调用完成,返回结果。这个过程的实现是需要一定时间的,因此我们的程序需要等待才能继续执行。不过这个延迟并不会对程序的性能造成太大的影响,因为它只是在数据到来时触发。

在使用read函数时,我们通常需要注册多个socket监听。这个socket就相当于网络上的一个端口,我们可以通过它来专门监听某个特定的连接。当数据到达时,操作系统会自动找到我们所需要的连接,然后将结果返回给我们。这个过程就需要使用到操作系统内部的某些机制,比如select。

举个例子,比如在游戏开发中,我们需要不断地获取用户自己所在地图范围内的其他玩家的位置信息,然后将其在地图上显示。这个过程就需要我们使用到read函数,不断地监听网络上的数据。当有数据到达时,我们的程序会自动捕捉到并进行处理。这个过程中,我们可以将自己玩家所处位置的信息注册为一个socket监听,然后不断地调用read函数等待其他玩家的位置信息到来。最终,我们的程序会将所有数据整合起来,并在地图上进行相应的显示。

当然,要想使用read函数进行网络编程,还需要掌握一些其他的技能和知识。比如我们需要了解不同操作系统的异步I/O机制、网络编程中的缓冲区管理等等。只有掌握了这些基本的知识和技能,我们才能更好地处理网络编程中的各种问题,并且写出高性能、低延迟的程序。

🍊 单线程模型

🎉 为什么redis使用单线程模型还能保证高性能?

首先,我们需要了解Redis的存储机制。Redis是一种纯内存操作的存储系统,一切操作都在内存中完成,而内存读写的速度确实非常快。根据数据分析,内存读写的响应时间大约在100纳秒左右,这是Redis超高QPS的重要基础。因此,当Redis处理大量数据时,单线程模型也可以处理大量的请求,这也就是Redis能够出色表现的第一个原因。

其次,Redis的核心是基于非阻塞的IO多路复用机制。在传统的多线程模型中,线程之间的切换和竞争会导致性能的消耗。因此,单线程模型通过提供非阻塞IO机制,避免了线程切换和竞争产生的消耗。这意味着,单线程模型可以在处理大量请求的情况下,保证高效的响应速度。可以想象,如果Redis采用了多线程模型,其性能将随着线程数的增加而降低,这也是Redis能够出色表现的第二个原因。

最后,我们需要了解Redis底层采用了C语言来实现。C语言是一种高效的编程语言,由于其接近底层的特性,C语言的执行速度相对比较快。因此,Redis底层采用了C语言来实现,这意味着Redis的执行速度相对较快。并且,由于Redis采用的是单线程模型,C语言的执行速度进一步得到了提升,这也是Redis能够出色表现的第三个原因。

🎉 你是如何理解redis单线程模型的?

Redis是一款高性能的开源键值存储系统,它以单线程模式运行在内存中,专注于网络IO和键值对读写,但并不意味着Redis只有一个线程在运行。实际上,在Redis的其他功能方面,比如持久化、异步删除和集群数据同步等,Redis还会利用额外的线程来处理这些任务。这里的单线程主要指Redis对外提供键值存储服务时的运行模式。

为了更好地理解Redis的单线程模式,可以将其想象成一个餐厅。在餐厅里,顾客们会排队等待服务员接待,服务员会记录每个顾客的点餐请求,并按照先来先服务的原则逐一处理。因为每个服务员只能同时为一个顾客服务,所以在繁忙的时候,顾客可能需要等待一段时间才能得到服务员的注意。类比到Redis的单线程模式中,每个客户端就像是一个顾客,Redis的服务端就像是一个服务员。每个客户端会被关联到一个指令队列中,Redis会根据队列中的顺序依次处理每个指令,处理完毕后再将结果返回给客户端。与餐厅类比,这就像是服务员记录每个顾客的点餐请求,并按照顺序逐一执行。

但是,如果餐厅里只有一个服务员,他能同时为多个顾客服务吗?显然是不行的,这对于Redis也同样适用。Redis在同一时间只能处理一个客户端队列中的指令或响应,这意味着如果有多个客户端同时发送请求,Redis只能按照先来先服务的原则逐一处理这些请求。因此,在高并发情况下,可能需要通过增加Redis实例的数量来提升系统性能。

除了顺序处理指令外,Redis还支持一些高级的数据结构和操作,如Pub/Sub、Lua脚本和事务等。尽管这些操作需要额外的线程来完成,但Redis的单线程模式仍然能够提供出色的性能,因为Redis在内部使用了高效的数据结构和算法来支持这些操作。



相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
2月前
|
Java
网络 I/O:单 Selector 多线程(单线程模型)
网络 I/O:单 Selector 多线程(单线程模型)
|
1月前
|
人工智能 JSON 前端开发
【Spring boot实战】Springboot+对话ai模型整体框架+高并发线程机制处理优化+提示词工程效果展示(按照框架自己修改可对接市面上百分之99的模型)
【Spring boot实战】Springboot+对话ai模型整体框架+高并发线程机制处理优化+提示词工程效果展示(按照框架自己修改可对接市面上百分之99的模型)
|
2月前
|
消息中间件 安全 Java
多线程(初阶七:阻塞队列和生产者消费者模型)
多线程(初阶七:阻塞队列和生产者消费者模型)
31 0
|
3月前
|
缓存 NoSQL 安全
Redis 新特性篇:多线程模型解读
Redis 新特性篇:多线程模型解读
50 5
|
3月前
|
存储 缓存 NoSQL
Redis 数据结构+线程模型+持久化+内存淘汰+分布式
Redis 数据结构+线程模型+持久化+内存淘汰+分布式
311 0
|
3月前
|
存储 缓存 NoSQL
《吊打面试官》系列-Redis双写一致性、并发竞争、线程模型
《吊打面试官》系列-Redis双写一致性、并发竞争、线程模型
40 0
|
3月前
|
网络协议 Linux API
基于Linux socket聊天室-多线程服务器模型(01)
基于Linux socket聊天室-多线程服务器模型(01)
46 0
|
3月前
|
网络协议 Linux API
从0实现基于Linux socket聊天室-多线程服务器模型(一)
从0实现基于Linux socket聊天室-多线程服务器模型(一)
54 0
|
3月前
|
Java C++
线程池-手写线程池C++11版本(生产者-消费者模型)
线程池-手写线程池C++11版本(生产者-消费者模型)
70 0
|
3月前
|
Java Linux C语言
线程池-手写线程池Linux C简单版本(生产者-消费者模型)
线程池-手写线程池Linux C简单版本(生产者-消费者模型)
44 0