用大白话来说一下BIO\NIO与I\O复用技术

本文介绍了BIO、NIO和I/O多路复用。BIO处理socket时一个对应一个线程且阻塞,浪费资源;NIO同步非阻塞,但轮询会浪费CPU资源。I/O多路复用是操作系统机制,NIO与IO复用配合可高效处理数据。还介绍了I/O复用的三种方式:select、poll、epoll及其优缺点。

BIO大家都不陌生,不熟悉的同学只需要了解,BIO处理socket时一个socket对应着一个线程来处理,而且BIO的线程处理socket时是阻塞的,之前的博客中也说到,阻塞意味着在socket里没数据进来的时候会线程会挂起来。

BIO用这种方式去处理socket,是因为在操作多个socket的时候,要一直保持有线程在做处理,才能知道当前的socket有没有数据进来,而没有数据线程没办法只能挂起来,而不能去做别的事情。这严重的导致了资源的浪费。

NIO的出现,是的操作IO的时候,操作系统可以通知线程,当前的socket有没有数据到来,在NIO模式下调用read方法时,若发现没有数据到来,会直接返回-1,被设置状态为未获取,再用while循环不断的去尝试获取数据(这里明显看出是同步非阻塞的)。

NIO的操作很明显比BIO要好的多,BIO挂起线程将会导致线程不断的挂起唤醒,内核态与用户态之间不断的切换,同时也会造成线程资源的浪费。

而NIO本身也有很大的缺点:NIO虽然不会挂起线程,但是会不断的轮询,尝试获取数据,导致CPU资源的浪费。而且这里NIO尝试获取数据,没有数据到来返回-1的过程,是对socket文件描述符的不断尝试得到的,也就意味着要不停的读取文件描述符,造成大量上下文的切换。这个时候I/O多路复用出现了。

I/O多路复用是一种操作系统的机制,意思是说程序会注册一组socket的文件描述符给操作系统,让操作系统去监听这些文件描述符是否有数据到了,有数据到就通知程序做处理。

这里可以看出来,NIO与IO复用其实是分开来的,NIO保证了一旦有数据,程序总是可以立刻做处理,不用挂起;IO复用保证了一旦有数据进入内核态,操作系统可以立刻监听到是哪个socket的数据到了,并通知线程做处理。所以一个是程序层面的,一个是系统层面的,两者配合使用使得到内核态的数据处理的快,切不耗费太多的系统资源。

NIO这里用的是轮询的方式做的非阻塞,系统的IO复用是如何实现的呢?

I/O复用有三种方式:select,poll,epoll:

select:select函数需要接收三个文件描述符数组,分别用于监听读,写,异常。调用select后,程序会Block住,直到一个事件发生了,或者等到最大1秒钟(tv定义了这个时间长度)就返回。然后依次遍历所有注册的socket,等待有事件到来。

缺点:select用于监听的数组,最大长度不得超过1024,这对高并发来说,是不可接受的;为了使用select要构造三个数组在内存中,每次调用select都要把这三个数组复制到内核态由操作系统使用;每次都要遍历所有的文件描述符,性能很差。

poll:poll相对于select有一定的优化,poll中文件描述符数组的大小不再有限制了,用链表数据结构存储文件描述符,而且文件描述符不再有三个,而是用一个pollfd数组;但每次同样要轮询所有的数组中fd才能返回。

epoll:在select和poll中,每次都需要把文件描述符从用户态复制到内核态去遍历,而epoll不同,epoll是先在内核建立一个由文件描述符指向的表,每次要监听的文件描述符有事件发生,就在表里标记一下(有状态的),调用epoll_wait时也只是将表里已经标记的文件描述符返回即可。可以看出,epoll代价相比较每次都对所有文件描述符做一次遍历的poll,select来说要好的多。

epoll分为水平触发与边沿触发:水平触发指的是,每次只要表里有状态为有数据的文件描述符,这些有数据的fd总是会在epoll_wait时被返回;边沿触发关系的是事件,边沿触发需要事件驱动,当文件描述符有新的事件产生,就返回。

水平触发相当于跑的比较快的poll,但边沿触发不同,边沿触发把处理数据的控制权交给了开发者,开发者可以设置一些事件,比如http请求,开发者可以在读完header的时候停下来,然后作出判断是否要做下一步处理。编程难度更大。

<think>我们正在讨论C++中的前置递增运算符(++i)和后置递增运算符(i++)的区别。根据提供的引用,我们可以总结如下: 1. **前置递增运算符(++i)**: - 先对操作数进行递增操作,然后返回递增后的值。 - 在运算符重载中,前置递增运算符返回的是对象的引用(因为修改了当前对象,然后返回该对象本身)。 - 因此,前置递增通常效率更高,因为它不创建临时对象。 2. **后置递增运算符(i++)**: - 先返回操作数当前值的一个副本(临时对象),然后再对操作数进行递增操作。 - 在运算符重载中,后置递增运算符有一个额外的整型参数(通常为0,用于区分前置和后置),并且返回的是递增前的副本(即一个临时对象,不是引用)。 - 因此,后置递增会涉及临时对象的创建和拷贝,效率相对较低。 引用[1]指出:后置递增运算符在C++中前置递增运算符的差别在于它需要传递一个额外的整型参数(通常使用默认值为0),以区分前置和后置操作。后置递增的意图是“返回当前对象的一个副本,并在该副本被返回后,修改原对象以使其值增加1”。 引用[2]和[4]进一步说明了运算符重载的实现方式: - 前置递增运算符重载函数原型:`ReturnType& operator++();` - 后置递增运算符重载函数原型:`ReturnType operator++(int);` (注意:这里的`int`是用于区分的哑元参数) **示例代码说明**: 假设我们有一个自定义的整数类`MyInteger`,我们可以这样重载前置和后置递增运算符: ```cpp class MyInteger { private: int value; public: MyInteger(int val) : value(val) {} // 前置递增运算符重载 MyInteger& operator++() { ++value; return *this; // 返回当前对象的引用 } // 后置递增运算符重载 MyInteger operator++(int) { MyInteger temp = *this; // 保存当前对象的值(副本) ++value; // 当前对象递增 return temp; // 返回递增前的副本 } int getValue() const { return value; } }; ``` **使用示例**: ```cpp MyInteger a(5); MyInteger b = ++a; // 前置递增:a先递增到6,然后b得到a的引用(即6) // 此时a的值是6,b的值也是6 MyInteger c(5); MyInteger d = c++; // 后置递增:d得到c的副本(5),然后c递增到6 // 此时c的值是6,d的值是5 ``` **性能考虑**: 由于后置递增需要创建临时对象,因此在不需要原始值的场景下,使用前置递增更高效。特别是在循环和迭代器操作中,推荐使用前置递增。 **总结区别**: | 特性 | 前置递增(++i) | 后置递增(i++) | |--------------|----------------|----------------| | 操作顺序 | 先递增,后使用 | 先使用(返回原值副本),后递增 | | 返回值 | 对象的引用 | 递增前的副本(临时对象) | | 效率 | 较高(无临时对象) | 较低(有临时对象) | | 重载函数参数 | 无 | 一个int类型的哑元参数(用于区分) | 希望这个解释能帮助你理解两者的区别。在实际编程中,除非需要后置递增的特性(即需要递增前的值),否则应优先使用前置递增。 §§相关问题§§ 1. 在C++中,如何重载一个类的前置递减和后置递减运算符? 2. 为什么在STL迭代器中,推荐使用前置递增而不是后置递增? 3. 除了递增递减运算符,还有哪些运算符可以重载为前置和后置形式? 4. 在C++中,后置递增运算符的哑元参数具体是什么作用?能否省略?</think>### 前置递增(++i)后置递增(i++)的区别详解 在编程中,前置递增运算符(`++i`)和后置递增运算符(`i++`)的核心区别在于**运算顺序和返回值的不同**: #### 1. **运算顺序** - **前置递增(++i)**: 先执行递增操作,再返回结果。 例如: ```cpp int i = 5; int j = ++i; // 先让 i 变为 6,再将 6 赋值给 j // 结果:i=6, j=6 ``` - **后置递增(i++)**: 先返回当前值,再执行递增操作。 例如: ```cpp int i = 5; int j = i++; // 先将 i 的原始值 5 赋值给 j,再让 i 变为 6 // 结果:i=6, j=5 ``` #### 2. **运算符重载实现(C++示例)** ```cpp class Counter { int value; public: // 前置递增:返回引用(修改自身) Counter& operator++() { ++value; return *this; // 返回自身引用 } // 后置递增:返回副本(需哑元参数 int 区分) Counter operator++(int) { Counter temp = *this; // 保存当前值 ++value; // 自身递增 return temp; // 返回递增前的副本 } }; ``` - **关键区别**: - 前置返回引用(`Counter&`),直接修改原对象[^2][^4]。 - 后置返回临时副本(`Counter`),需额外参数 `int` 标识[^1][^4]。 #### 3. **性能差异** - **前置递增(++i)** 效率更高: 直接修改对象并返回引用,无临时对象开销[^4]。 - **后置递增(i++)** 效率较低: 需创建副本存储原值,涉及拷贝构造和析构开销[^1][^4]。 *在循环或高性能场景中优先使用 `++i`*。 #### 4. **使用场景** - **前置递增**: 适用于不需要原始值的场景(如循环迭代器): ```cpp for (auto it = vec.begin(); it != vec.end(); ++it) // 推荐! ``` - **后置递增**: 仅当需要原始值时才使用: ```cpp int oldValue = counter++; // 需要递增前的值 ``` #### 总结 | 特性 | 前置递增(++i) | 后置递增(i++) | |--------------|----------------------|------------------------| | **运算顺序** | 先增后取值 | 先取值后增 | | **返回值** | 递增后的引用 | 递增前的副本 | | **性能** | 高效(无临时对象) | 较低(需创建副本) | | **重载语法** | `T& operator++()` | `T operator++(int)` | > 关键原则:**除非必须使用原始值,否则优先选择前置递增**。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值