首页 > 其他分享 >从多线程到 epoll:如何优雅地处理高并发请求?

从多线程到 epoll:如何优雅地处理高并发请求?

时间:2024-10-21 08:52:01浏览次数:9  
标签:epoll 并发 描述符 线程 内核 红黑树 多线程 节点

文章参考于:

小林coding

最近在学习操作系统,服务器与客户端之间的通信离不开 socket 编程。然而,基于 TCP 的 socket 编程在默认情况下只能实现一对一的通信,因为它采用同步阻塞模型。在服务器处理完当前客户端的请求之前,无法响应其他客户端的请求。这种方式效率不高,显然浪费了系统资源。因此,今天我们来探讨几种实现服务器与多个客户端同时通信的方法。

多进程模型

如果不改变阻塞 I/O 模型,多进程是一种可行的选择。

在这种模式下,服务器的主进程用于监听客户端的连接请求。当有新的连接请求时,服务器会生成一个已连接的 socket,并 fork 一个子进程来处理这个客户端的通信。由于子进程继承了父进程的资源,因此每个子进程都可以使用该已连接的 socket 与客户端进行独立通信。

这种设计相当于主进程负责连接管理,而子进程专注于与各自的客户端进行数据交换。每个连接对应一个独立的进程,互不干扰。

缺点:随着连接数的增加,子进程的数量也会急剧上升,从而导致系统资源的消耗显著增加。尤其是进程间的上下文切换,开销较大,系统性能会受到显著影响。因此,当并发连接数较高时,多进程模型并不是最优的选择。


多线程模型

为了减少多进程模型中频繁的进程切换带来的性能损耗,可以使用多线程模型。

在这种模式下,当服务器监听到客户端的连接请求时,会创建一个新的线程来处理该客户端的通信。每个线程拥有自己独立的栈和线程控制块,但与同一进程的其他线程共享内存和文件描述符。线程间的上下文切换比进程间的切换要轻量得多,因此在一定程度上提高了性能。

缺点:多线程模型虽然减少了进程切换的开销,但仍存在创建和销毁线程的成本。如果服务器有大量的并发连接,每个连接都需要一个线程来处理,系统的资源消耗仍然不容小觑。并且,线程间的共享数据可能带来同步问题,需要开发者小心管理锁机制,否则容易出现线程安全问题。


线程池模型

为了进一步优化多线程模型的性能,我们可以引入线程池来复用线程,减少线程的创建和销毁开销。

线程池是一个用于存放可用线程的集合(通常使用 HashSetList 实现)。在服务器启动时,就会预先创建一定数量的线程放入池中。当有客户端连接到来时,服务器从线程池中取出一个空闲线程,分配给这个连接进行通信。通信完成后,线程不会被销毁,而是回归线程池,等待下一次任务的到来。

优点:线程池模型有效避免了频繁创建和销毁线程的性能开销,同时也能够在高并发场景下保持较高的响应效率。

缺点:需要合理设置线程池的大小,太小可能导致连接排队,太大会导致资源消耗增加。此外,仍然需要考虑线程间的同步问题。

I/O 多路复用

上面提到的方法(多进程、多线程)无论如何优化,在大量并发请求时依然会消耗大量的系统资源。那有没有一种方式,可以不使用进程或线程,就能高效地服务多个客户端呢?

答案是 I/O 多路复用。

虽然一个进程通常只能一次处理一个请求,但如果每次处理的时间仅需毫秒级,那么在1秒钟内就能处理成千上万个请求。这类似于CPU的时间分片调度,可以快速地在多个请求之间切换,最大化地利用时间片,从而实现高效并发。

I/O 多路复用的工作原理

I/O 多路复用提供了三种常见的实现方式:selectpollepoll。这些方法的核心思想是:将多个文件描述符(通常是 socket)传递给内核,由内核监视这些描述符的状态变化。一旦有事件发生(例如数据可读、可写等),内核就会返回这些发生了事件的文件描述符,进而用户态程序可以对这些描述符进行进一步处理。

这种方法让一个进程能够同时监控多个 I/O 事件,极大地提高了服务器处理并发连接的能力。

selectpoll

这两个方法非常相似,它们的工作原理和优缺点也基本一致:

  • 工作原理

    • selectpoll 都是通过某种数据结构(select 使用的是集合,而 poll 使用的是链表)来维护所有的 socket 文件描述符。
    • 调用 selectpoll 时,所有的文件描述符会被拷贝到内核态,然后内核会遍历这些描述符,查看哪些连接有事件需要处理,并将这些信息标记。
    • 内核会将标记的结果返回到用户态,用户态程序再次遍历这些返回结果,找出需要处理的连接并进行相应操作。
  • 优点

    • 比起传统的多进程、多线程模型,selectpoll 不需要为每个连接创建一个进程或线程,而是通过一个进程或线程同时处理多个连接,显著减少了系统资源的消耗。
  • 缺点

    • 重复拷贝:每次调用时都需要将所有的文件描述符集合拷贝到内核中,这在高并发时会带来不小的性能开销。
    • 遍历开销大:无论是内核态还是用户态,都需要对所有的文件描述符进行遍历,以确定哪些描述符有事件发生,这会造成不必要的 CPU 消耗。
    • 连接数限制select 的集合有大小限制,通常最多只能监听1024个文件描述符(可以通过修改内核参数提高这个限制)。而 poll 使用链表结构,虽然没有直接的大小限制,但依然受限于系统的最大文件描述符数。

红黑树

epoll的底层数据结构是红黑树,我们先来介绍一下。

红黑树的基本性质

红黑树满足以下五条性质,这些性质保证了树的平衡性,使其在最坏情况下的时间复杂度也能保持在 O(log n)

  1. 节点是红色或黑色
    • 每个节点要么是红色,要么是黑色。
  2. 根节点是黑色
    • 红黑树的根节点必须是黑色的。
  3. 红色节点的子节点必须是黑色(即红色节点不能连续):
    • 如果一个节点是红色的,那么它的子节点必须是黑色的。这意味着从根节点到叶节点的每一条路径上不能有两个连续的红色节点。
  4. 每个叶子节点(NIL)都是黑色
    • 这里的叶子节点是指所有空节点,也就是每个实际节点下不存在的子节点(虚拟的节点)。
  5. 从任意一个节点到其每个叶子节点的路径上都包含相同数量的黑色节点
    • 这个性质保证了树的平衡性,确保从根节点到叶子节点的最长路径不会超过最短路径的两倍。
红黑树的操作

红黑树的关键在于插入和删除操作,因为每次操作后都可能破坏红黑树的性质。因此,在插入和删除节点后,红黑树会通过重新着色旋转操作来恢复平衡。

  • 插入操作
    • 插入一个新节点时,默认会将其着色为红色。插入后如果违反了红黑树的性质(如连续的红色节点),需要通过旋转和重新着色来调整平衡。
  • 删除操作
    • 删除节点时,如果删除的是黑色节点,会打破红黑树的平衡,需要进行复杂的调整,包括重新着色和旋转。
  • 旋转操作
    • 左旋右旋是保持红黑树平衡的重要手段,通过旋转可以改变树的结构,同时保持二叉搜索树的性质。
红黑树的优势
  • 查找效率高:红黑树是一种平衡二叉树,能够保证插入、删除、查找操作的时间复杂度都是 O(log n)。因此,无论是存储文件描述符还是在 epoll 中快速查找和删除文件描述符,红黑树都能提供很高的效率。
  • 适合动态数据集:在服务器处理并发连接时,文件描述符的增加和减少是动态的。红黑树能很好地适应这种频繁的插入和删除操作,因此它被 epoll 选择作为管理文件描述符的底层数据结构。

epoll

第一:红黑树管理待检测的文件描述符
  • 红黑树的数据结构
    • epoll 内核实现中,待检测的所有文件描述符(通常指 socket)会被保存在一个红黑树数据结构中。
    • 通过调用 epoll_ctl() 函数,可以将需要监控的 socket 添加到内核的红黑树中。红黑树是一种平衡二叉树,它的插入、删除、查找操作复杂度为 O(log n),在大量文件描述符的情况下仍然能保持较高的性能。
  • epoll 的优势
    • epoll 通过在内核中维护红黑树,将所有需要监控的文件描述符管理起来。这样,当添加或移除文件描述符时,只需修改红黑树,极大地减少了内核与用户空间之间的数据传递开销。
    • 由于红黑树是平衡二叉树,因此它可以在 O(log n) 的时间复杂度内完成增删查操作,即使需要监控的文件描述符数量达到上万,epoll 依然能保持较高的查询效率。
第二:事件驱动机制
  • 事件驱动模型
    • epoll 使用事件驱动机制,在内核中维护了一个链表来记录就绪的事件。当某个 socket 上有事件发生时(例如可读、可写或异常事件),内核会将其添加到这个链表中。
    • 与传统的轮询方式不同,epoll 不需要频繁地遍历整个文件描述符集合,而是通过链表的方式将就绪的文件描述符传递给用户。
  • 回调函数机制
    • 当我们调用 epoll_wait() 函数时,如果有 socket 事件已经发生,内核就会返回发生事件的文件描述符列表。这个过程通过事件驱动机制,只返回有事件的文件描述符数量,而不需要像 selectpoll 那样在每次调用时都遍历整个文件描述符集合。
    • 这大大提高了高并发场景下的性能,因为 epoll 只需要关注那些实际有事件发生的连接,避免了无意义的扫描操作。
  • 效率提升
    • 使用这种事件驱动模式,epoll 在处理大量空闲连接时表现出色,因为它不会对那些没有事件发生的连接进行无效的遍历和检查。这样可以极大地减少CPU资源的消耗,提高系统的吞吐量。

总结

epoll 解决了经典的C10K问题,使单台服务器可以在高并发场景下处理数万个并发连接。它的两大核心优势是:

  1. 高效的数据结构:通过内核中的红黑树管理待检测的文件描述符,实现快速的插入、删除和查找操作,减少了内核和用户空间的数据传递开销。
  2. 事件驱动机制:通过事件回调和链表结构记录就绪事件,只返回发生事件的文件描述符,避免了无效的遍历和扫描操作,从而显著提高了在大量连接下的处理效率。

通过这些特性,epoll 在处理长连接(如 WebSocket)、高并发短连接(如 HTTP 服务器)以及高频 I/O 操作时表现出色,是现代高性能网络服务器的基础之一。

标签:epoll,并发,描述符,线程,内核,红黑树,多线程,节点
From: https://blog.csdn.net/qq_73210658/article/details/143081758

相关文章

  • python实现并发
    1.多线程#-*-coding:utf-8-*-importthreadingimporttimedefprint_hello_world():print("hello-world")defconcurrent_hello_world(n):threads=[]#记录开始时间start_time=time.time()#创建并启动多个线程for_inrange(n)......
  • STA模型、同步上下文和多线程、异步调度
    写过任何桌面应用,尤其是WinForm的朋友们应该见过,Main函数上放置了一个[STAThread]的Attribute。而且几乎所有的桌面应用框架,都是由同一个UI线程来执行渲染操作的,这表现为从其他线程修改控件的值就会抛异常:awaitTask.Run(()=>control.Content="");//throwsexception大家......
  • 多线程
    第七章——多线程1、多线程概述1、多线程概述进程:正在运行的程序,是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源。线程:是进程中的单个顺序控制流,是一条执行路径一个进程如果只有一条执行路径,则称为单线程程序。一个进程如果有多条执行路......
  • select、poll和epoll区别
    函数原型intselect(intnfds,structfd_set*readfds,structfd_set*writefds,structfd_set*execptfds,structtimeval*timeout);intpoll(structpollfd*fds,nfds_tfds,inttimeout);epoll:intepoll_create(intsize);intepoll_ctl(intepollfd,intop,intfd,st......
  • 并发请求太多,服务器崩溃了?试试使用 ASP.NET Core Web API 操作筛选器对请求进行限流
    前言请求限流(RateLimiting)主要是一种用于控制客户端对服务器的请求频率的机制。其目的是限制客户端在一定时间内可以发送的请求数量,保护服务器免受过多请求的影响,确保系统的稳定性和可靠性。请求限流通常会基于以下几个因素来进行限制:时间窗口:规定了在多长时间内允许的请求......
  • java_day18_多线程、线程安全问题、死锁、等待唤醒机制
    一、线程1、多线程进程:是系统进行资源分配和调用的独立单位,每一个进程都有它自己的内存空间和系统资源。举例:IDEA,阿里云盘,wegame,steam线程:是进程中的单个顺序控制流,是一条执行路径一个进程如果只有一条执行路径,则称为单线程程序。一个进程如果有多条执行......
  • 高清图解28个高并发之数据结构/数据结构场景匹配技巧分析(高并发精通篇三)
    Java中的Map家族包括基于哈希表的HashMap,维护插入顺序的LinkedHashMap,基于红黑树的TreeMap,线程安全的Hashtable和ConcurrentHashMap,以及基于身份比较的IdentityHashMap和基于弱引用的WeakHashMap。Queue家族则涵盖了Vector、Stack、Properties以及多种List和Deque实现,适用......
  • 多线程
    进程:是系统进行资源分配和调用的独立单位,每一个进程都有它自己的内存空间和系统资源。举例:IDEA,阿里云盘,wegame,steam线程:是进程中的单个顺序控制流,是一条执行路径一个进程如果只有一条执行路径,则称为单线程程序。一个进程如果有多条执行......
  • 【PostgreSQL】如何安装和配置PgBouncer以提高PostgreSQL的并发处理能力?
    安装和配置PgBouncer以提高PostgreSQL的并发处理能力是一个多步骤的过程。PgBouncer作为连接池器,可以有效地管理到PostgreSQL服务器的连接,从而减少每个新连接所需的开销,并且能够更高效地利用资源。下面是详细的步骤说明,包括如何在Debian/Ubuntu系统上安装PgBouncer以及如何......
  • 【Java】多线程 Start() 与 run() (简洁实操)
    Java系列文章目录补充内容Windows通过SSH连接Linux第一章Linux基本命令的学习与Linux历史文章目录Java系列文章目录一、前言二、学习内容:三、问题描述start()方法run()方法四、解决方案:4.1重复调用.run()4.2重复调用start()4.3正常调用start()不会报出......