多线程文件i/o_可扩展的I / O:事件-基于多线程

多线程文件i/o

一切始于对基础论文的复习阅读-是的,我使用一组论文和书籍作为参考资料。 本文的标题为:“ 为什么事件不是一个好主意(对于高并发服务器而言) ”,作者是罗伯·冯·贝伦(Rob von Behren),当时他在伯克利(Berkeley)担任博士生[18]。 冯·贝伦(Von Behren)开头说道:“ 近年来,基于事件的编程一直被视为编写高度并发应用程序的最佳方法。 在其中几个系统上工作之后,我们现在认为这种方法是错误的。 具体来说,我们认为线程可以实现事件的所有优势,包括对高并发,低开销和简单的并发模型的支持 ”,至少我可以说,他在论文中论证了以下观点:一组有关参考实现的有趣基准。

我想整理一些概念,并进行系统的演绎推理,用其他一些有趣的资源丰富我的复习阅读清单。 我想实现什么? 好吧,什么都没有,同时又没有,不是一个真正的目标,而对于像我这样好奇的工程师来说,这只是值得的工作……敬请期待!

记忆层级

访问时间从下图中建议的金字塔下降,存储容量也随之增加[1]。 通常,这种分层层次结构中的访问速度以CPU(中央处理单元)周期为单位,并且从上到下的周期数几乎成倍增加。 例如,大致而言,要访问大约3 = 3 * 10 ^ 0个周期的片上寄存器/缓存就足够了,相反,要访问网络/存储资源需要2M = 2 * 10 ^ 6个周期。 进入主存储器大约需要200 = 2 * 10 ^ 2个周期。 这么说来这么说:I / O成本很高,根据上下文和主要用例,需要在设计时进行仔细的管理:作为工程师,我们知道对于这种情况下的所有问题,并不存在唯一的解决方案或设计技巧世界。

线程与流程

流程是OS(操作系统)的可执行程序/任务:物理资源和内存分别分配给同时运行的每个流程。 一个进程可以产生许多共享内存空间和资源的线程[2]。 线程可以使用父进程共享内存轻松地相互通信:潜在地,线程可以在父资源共享区中的竞争资源上并行操作。 另一方面,进程可以使用OS的套接字,管道和共享内存(按目的分配,可以在进程之间共享的映射内存的特定区域)相互通信。 每个进程的线程都有一个专用的堆栈,但是与父进程共享任何打开的资源(例如文件描述符,套接字等),因此可能与其他线程共享。

线程可以看作是轻量级的进程:由于所述内存和资源与父进程共享,因此唯一的专用资源是堆栈。 直观地讲,线程上下文切换比进程之间的上下文切换轻巧快捷(只需保存一个很小的堆栈和几个寄存器)。

什么是线程/进程? 举例来说,线程和/或进程可以看作是定义良好的一组在CPU上可执行的机器代码/操作,由编译过程创建并由仅在运行时确定的条件跳转来替代。 而且,什么是上下文切换? 再次举例说明,上下文切换可以看作是定义良好的一组可在CPU上执行的机器代码/操作,目的是在将CPU(及其注册表)释放到新的进程或线程之前保存当前的执行上下文。

注意硬件线程是一个不同的概念。 通常,多路复用技术允许在硬件级别提高单核利用率,从而创建不同的执行流程。 超标量架构能够轻松地将两个单独的并行执行路径复用到单个内核上(操作系统看到两个单独的内核)[6]。 在下面的图片中,提供了提高单个物理核心的多任务处理能力的超线程示例:在前端对管道气泡进行了重新排序,以尽可能增加每个核心的使用。

非阻塞与阻塞I / O

阻塞I / O操作对主进程/线程强加了停止世界的暂停:调用方等待直到该操作完成。 另一方面,非阻塞I / O或异步I / O操作不会对主进程/线程强加任何停止世界的暂停:调用方继续执行,而在后台I / O将执行由操作系统完成。 从直觉上讲,在第二种情况下,调用者必须轮询一个缓冲区以检查状态,或者注册一个回调以在完成时异步通知[3]。

就设计和可调试性而言,使用无阻塞I / O的客户端代码相当复杂:先验性,无法对I / O操作的完成做出任何假设,代码必须考虑到这一点,并具有进行相应的组织(事件从通道中以不确定性流出,缓冲区被填充,并且客户端代码必须谨慎处理这些方面)。 相反,阻塞I / O通常是面向流的:根据I / O请求,将尽最大努力立即为请求提供服务,并将数据作为系统调用的结果返回给调用方。

无阻塞I / O在多种情况下是一种可扩展的方法。 但是,让我们立即发现一种情况,其中这种陈述很容易无法验证。 例如,让我们考虑一个处理来自许多文件描述符(FD)的大量事件的单个进程的情况。 很明显,在这种情况下,非阻塞I / O的好处立即消失了:该过程应该处理受CPU约束的工作单元(I / O约束的部分已经完成,网络事件现在处于缓冲区),在基础资源(即缓存,内存等)的单个软件执行路径中序列化,这意味着CPU必须一个接一个地执行每个事件。

C10K问题–事件循环来自何处

在1999年代左右,分析了在单个服务器上管理成千上万个面向流的活动连接的问题,并创造了术语C10K(即Connections 10K)。 分析的问题(甚至被称为“ Apache问题”)考虑了一个HTTP服务器在单个商品箱上运行,并使用“每连接线程数”模型服务于成千上万的活动连接(小心,不是每秒请求数)。 那时,CPU的参考体系结构是:在[166,300] MHz频率范围内工作的单个处理单元(也称为单核)。 显然,在这样的体系结构上,在高并发情况下,就处理时间而言,多个上下文切换的成本是相关的:一旦达到最大可管理连接数,吞吐量就会降低。 在1999年,用于商品服务器的10K主动连接是一个硬性限制,因此为了克服这种限制,应用了React性编程技术:Reactor设计模式[7]允许通过使用事件循环开发用于处理I / O的单进程和单线程服务器。 由于许多原因,该解决方案可以克服硬限制。 首先,像Linux这样的操作系统没有真正的操作系统级线程(仅在NTPL [16] Linux 2.6中具有这种抽象)。 其次,在当时的硬件体系结构(没有硬件支持的上下文切换功能)上,线程上下文切换是耗费时间(CPU较慢)的重量级操作,严重影响了整体可伸缩性。 最后,同样,当时的硬件体系结构不是多核和超线程的。

非阻塞I / O药丸:select()/ poll()/ epoll()

此类系统调用与FD配合使用,FD提取文件和套接字等资源。 根据POSIX规范[20],每个FD可以与一个inode [21]关联,并且潜在地一个inode可以由多个FD链接。 索引节点是操作系统内核的数据结构,用于抽象物理资源。 在一个非常简化的版本中,它可能被视为缓冲区–这是我以后将要使用的简化。

可以使用不同的机制和使用不同的系统调用来轮询FD的事件[10] [11]。 select()和poll()是等效的系统调用,用于访问由一组FD生成的事件,实际上poll()有点现代,并解决了select()的某些限制。 epoll()是poll()的改进,仅在Linux上可用(select()和poll()具有不同的命名,在几乎所有其他OS平台上都可用),从而简化了从一组FD提取事件的任务。 更有趣的是,它解决了select()和poll()的一系列可伸缩性问题,其中最著名的是检测FD接收新事件的线性扫描问题。

作为系统调用,select(),poll()和epoll()需要一个轻上下文切换:从用户模式切换到内核模式。 轮询频率是通过程序配置的,但是对于epoll(),它不能超过毫秒(这本身是一个限制)。 在select()和poll()的情况下,轮询频率可以设置为微秒。 现在,让我们想象一个实际案例,对数千个FD进行一次select()/ poll()并每隔几微秒轮询一次。 问题是:这是一种可扩展的方法吗? 在这种情况下,另一个问题是,如果每次仅更新数千个FD中的一个,该怎么办? …令​​人回味!

在[12]中,提供了一个有趣的基于epoll()的应用程序库基准,即libevent和libev。 下面的图片报告了基准测试会议的结果。 我将把注意力集中在处理事件上所花费的时间:FD的数量接近1K且活动客户端数量很少时,平均仅花费150微秒来处理基准测试中生成的事件。 事件处理引入的此类开销是在任何实际情况下都必须考虑的相关时间量。

短线与长寿命连接

短期连接和长期连接之间必须加以区分。 长期连接通常是客户端和服务器之间的交互式会话(在这种情况下,这种连接可能会导致通常按块执行的长时间数据传输); 另一方面,短暂会话通常是建立的1-off连接,仅用于获取一些非常小的数据-不需要连续的交互。

处理I / O –几个关键模型

前言

从实际的角度来看,使用通常可用的OS版本附带的默认网络堆栈和系统调用API不能避免I / O等待和上下文切换。 有趣的一点是:诸如select()/ poll()/ epoll()之类的技术会以编程方式配置并阻止并等待,并且每个系统调用都可能提取一帧或多帧数据(即,一个上下文切换可能会从多个FD检索事件) 。

单线程无阻塞I / O

从一个或多个缓冲区中提取事件,并由单个执行单元(即进程和/或线程)按顺序处理事件。 因此,即使OS支持多核非阻塞I / O,使用单个进程/线程来处理所谓的CPU绑定事件也是性能的杀手::事实证明这是一种基本的序列化方案。

算法

  1. 等待
  2. 进行系统调用selector()/ poll()
  3. 检查是否有必要做(通过FD线性迭代,在epoll()中进行了优化)
  4. 如果无所事事,转到(1)
  5. 遍历任务
    1. 如果其为OP_ACCEPT,则系统调用接受连接,保存密钥
    2. 如果其为OP_READ,请找到系统调用键以读取数据
    3. 如果还有更多任务,请转到(3)
  6. 后藤(1)

优点

  • 轻上下文切换(用户到内核模式)。

缺点

  • 短期连接效率低下(处理开销和轮询时间的事件可能会超过当前的工作量)。
  • 串行处理(从“进程/线程”角度看)。
  • 瓶颈(在事件循环中)。
  • 连续执行受CPU约束的任务会降低性能。
  • 复杂的代码。
  • 复杂的调试。
多线程无阻塞

事件从缓冲区中拉出并分派到工作线程上:Puller线程通过线程池中一组预先分配的工作线程分派事件。 事件处理与此调度方案同时进行,并且可以利用现代硬件体系结构的本机多核处理。

算法

  1. 等待
  2. 进行系统调用selector()/ poll()
  3. 检查是否有必要做(通过FD线性迭代,在epoll()中进行了优化)
  4. 如果无所事事,转到(1)
  5. 遍历任务并分派给工人***
    1. 如果还有更多任务,请转到(3)
  6. 后藤(1)

***对于每个工人

  1. 如果其为OP_ACCEPT,则系统调用接受连接,保存密钥
  2. 如果其为OP_READ,请找到系统调用键以读取数据

优点

  • 轻上下文切换(用户到内核模式)。
  • 处理CPU绑定任务时具有更高的可伸缩性和更好的性能。

缺点

  • 瓶颈(在事件循环中)。
  • 多重处理是由调度程序驱动的,因此会延迟时间(在开始处理新内容之前,必须在Dispatcher分析事件本身之后弱化Thread); 费时的间接访问级别。
  • 复杂代码。
  • 复杂的调试。
阻塞多线程

每个I / O请求均由单个线程提供服务,该线程解析该请求,阻止等待操作完成并最终处理I / O任务检索的数据。 线程独立工作,它们利用现代硬件体系结构的本机多处理功能来执行系统调用并实现总体计算。

算法

  1. 进行系统调用以接受连接(线程在那里阻塞,直到有一个为止)
  2. 进行系统调用以读取数据(线程在那里阻塞,直到得到一些)
  3. 后藤(2)

优点

  • 没有明显的瓶颈。
  • 纯多处理(无间接级别)。
  • 更好的性能(无需分析事件)。
  • 可运行线程的数量可能受可用虚拟内存的限制。
    • 规则NumberOfCPUs * 2 +1仅适用于与CPU绑定的硬任务,这里我们谈论的是混合I / O优先和CPU第二任务。
  • 简化代码。

缺点

  • 平均而言,上下文切换的数量更高。
  • 更高的内存使用率(与FD数量一样多的线程堆栈)。
  • 相当复杂的调试。

Mailinator案例–成千上万的线程和阻塞的I / O

那么在[5]中提出的问题是:为什么应用程序应该重新编码内核应该做什么? 可以说,通过选择事件并在应用程序级别上将这些事件分发给工作人员来管理多个内核上的并发性(单线程非阻塞I / O只是一个案例研究,无法在其上构建任何实用的可扩展应用程序)。 另一个问题是,内核是否应该在内核空间以及其他任何应用程序下执行此操作? Mailinator的创建者和前Google的架构师Paul Tyma证明了这一点:现代硬件体系结构是为并发设计的,而现代OS经过优化可以在现代硬件体系结构上工作。 根据现场经验,多线程和阻塞IO具有许多优点,其中包括:优越的性能,简单的代码,现代体系结构的本机功能的使用以及对低级调度机制的信任(由内核本身提供)

破坏者模式–大规模多线程

Disruptor是一种来自金融行业的设计模式,它定义了一种线程间通信机制,该机制可以确保在单个设备上每秒处理22M个事件[9]。 出乎意料的是,该参考实现是用Java编写的,并在设计中利用了多线程和机械的同感。 它是一种复杂的无锁交互方案[15],可确保极高的吞吐量和极低的处理延迟。

Nginx案例–工作者线程和非阻塞I / O

Nginx是在2000年代早期开发的现代Web服务器[22],此后不断发展成为托管业务关键服务的受信任平台。 Nginx是专为使用异步I / O和现代硬件体系结构的多处理功能而设计的:单个调度程序线程/进程提取事件,并通过工作线程/进程进行分配。 如[23]中所述,它在许多用例中均优于Apache Web Server。 当然,这并不意味着Nginx的I / O模型要比Apache的I / O模型更高效:Apache于1995年发布,Nginx于2004年发布,因此Nginx的代码库据说已针对运行基准测试的现代体系结构进行了优化。

最后的想法

至于所有工程学知识,最好始终是权衡取舍! 如今,认真地考虑,在事件和线程之间当然是最好的方法,正如Matt Matt Welsh [14](SEDA [19]的创建者)在这篇引人注目的论文中所证明的那样:“ 高度并发系统的设计框架 ”。 影响可伸缩系统设计的因素很多,例如:连接类型,任务类型,将任务拆分为子任务并进行流水线处理的能力,任务的特性(CPU和IO处理百分比)等所有这些因素都必须考虑到:I / O处理技术并不优于其他技术,但是其优劣取决于具体情况或至少是最常用的情况。

让我们尝试从上面刷新的概念中缩小一些想法。 下面是一个直接的比较。

同步I / O:每个连接单线程。

  • 阻止I / O
  • 高并发性(随着NPTL的出现[16]有了很多改进)
  • 与I / O FD一样多的线程
  • 内存使用率合理高
  • 更好地利用多核计算机
  • 编码比调试简单得多

异步I / O:单个调度程序线程,许多工作线程。

  • 非阻塞I / O
  • 并发级别降低(调度程序是瓶颈)
  • 许多线程可以有效地管理流量
  • 应用程序处理客户端之间的上下文切换(调度程序线程/进程):需要保存应用程序上下文以在事件循环中工作(通常,线程将其保存到自己的堆栈中,并在内核级别进行管理)
  • 内存使用率接近最佳
  • 编码和调试要困难得多(异步的两个层次:事件是从操作系统的缓冲区中拉出,然后推送到线程来处理它们)

在这一点上,一个合理的问题可能是? 异步I / O真的更快吗? 好了,在网上搜索并没有专门的基准来详尽地回答这个问题,但是一些有趣的讨论指出了相反的观点[17]:使用本地每连接线程模型阻塞I / O的速度比epoll快25%/ 30%基于()的解决方案,以及epoll()是迄今为止最快,更高级的异步I / O系统调用。 此外,Paul Tyma在[5]中提出了其结果:在Linux 2.6内核上,同步I / O服务器的参考实现优于异步I / O,显然单个实例的吞吐量为100 MB / s相对于75 MB / s。框。

直观地讲,如果线程在独立的数据上运行(计算的CPU约束部分),并且不需要同步(除了上面的算法中所述的其他上下文切换,就不需要其他的上下文切换),则本机高并发性以及纯多线程性能可以做一些改变。

资源资源

  1. https://zh.wikipedia.org/wiki/Memory_hierarchy
  2. https://msdn.microsoft.com/zh-CN/library/windows/desktop/ms684841(v=vs.85).aspx
  3. https://zh.wikipedia.org/wiki/异步_I / O
  4. https://zh.wikipedia.org/wiki/C10k_problem
  5. https://www.mailinator.com/tymaPaulMultithreaded.pdf
  6. https://zh.wikipedia.org/wiki/超线程
  7. https://zh.wikipedia.org/wiki/Reactor_pattern
  8. http://highscalability.com/blog/2013/5/13/the-secret-to-10-million-concurrent-connections-the-kernel-i.html
  9. https://lmax-exchange.github.io/disruptor/
  10. https://daniel.haxx.se/docs/poll-vs-select.html
  11. http://www.ulduzsoft.com/2014/01/select-poll-epoll-practical-difference-for-system-architects/
  12. http://libev.schmorp.de/bench.html
  13. http://www.kegel.com/c10k.html
  14. http://www.eecs.harvard.edu/~mdw/papers/events.pdf
  15. http://lmax-exchange.github.io/disruptor/files/Disruptor-1.0.pdf
  16. https://zh.wikipedia.org/wiki/Native_POSIX_Thread_Library
  17. http://www.theserverside.com/discussions/thread.tss?thread_id=26700
  18. http://www.cs.berkeley.edu/~brewer/papers/threads-hotos-2003.pdf
  19. https://en.wikipedia.org/wiki/Staged_event-driven_architecture
  20. https://zh.wikipedia.org/wiki/POSIX
  21. https://zh.wikipedia.org/wiki/Inode
  22. https://zh.wikipedia.org/wiki/Nginx
  23. http://wiki.dreamhost.com/Web_Server_Performance_Comparison

翻译自: https://www.javacodegeeks.com/2016/03/scalable-io-events-vs-multithreading-based.html

多线程文件i/o

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值