IO模型
阻塞,非阻塞,同步,异步
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程
同步处理是指被调用方得到最终结果之后才返回给调用方
异步处理是指被调用方先返回应答,然后再计算调用结果,计算完最终结果后再通知并返回给调用方
Java层面的IO模型
I/O,是Input/Output的简称,即输入/输出。通常指数据在内部存储器(内存)和外部存储器(硬盘,优盘等) 或其他周边设备之间的输入和输出。输入是系统接收的信号或者数据,输出则是发出的信号或数据。
在java中,提供了一些列的API供开发者来读写外部数据或文件。
BIO
同步阻塞的通信方式。是一个比较传统的通信方式。
优点:
简单,使用方便。
缺点:
并发处理能力差,通信耗时。
使用BIO进行文件的读取:
//Write Obj to File
TestObject testOb = new TestObject();
testOb.setContent("我是小强");
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
oos.writeObject(testOb );
} catch (IOException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(oos);
}
//Read Obj from File
File file = new File("tempFile");
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(file));
TestObject TestObject = (TestObject ) ois.readObject();
System.out.println(TestObject );
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(ois);
try {
FileUtils.forceDelete(file);
} catch (IOException e) {
e.printStackTrace();
}
}
NIO
同步非阻塞IO。在Java1.4版本之后新增。相比较BIO,它在数据打包和传输的方式上做了调整。BIO是基于流的方式处理数据,一个字节一个字节的处理数据,一个输入流产生一个字节的数据,一个输出流消耗一个字节的数据。NIO是基于块的方式处理数据,每个操作都产生或消费一个数据块,处理效率更高。对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。在Linux 2.6以后,Java中的NIO和AIO都是通过 epoll来实现的。
代码实现:
String pathname = "D:\\Test\\testOne.txt";
FileInputStream fin = null;
try {
fin = new FileInputStream(new File(pathname));
FileChannel channel = fin.getChannel();
int capacity = 100;// 字节
ByteBuffer bf = ByteBuffer.allocate(capacity);
int length = -1;
while ((length = channel.read(bf)) != -1) {
bf.clear();
byte[] bytes = bf.array();
System.out.write(bytes, 0, length);
System.out.println();
}
channel.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fin != null) {
try {
fin.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
String filename = "out.txt";
FileOutputStream fos = null;
try {
fos = new FileOutputStream(new File(filename));
FileChannel channel = fos.getChannel();
ByteBuffer src = Charset.forName("utf8").encode("你好你好你好你好你好");
int length = 0;
while ((length = channel.write(src)) != 0) {
System.out.println("写入长度:" + length);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
AIO
异步非阻塞IO。它在NIO的基础上引用了异步通道的概念,并提供了异步文件通道和异步套接字通道的实现。在Linux 2.6以后,Java中的NIO和AIO都是通过 epoll来实现的。
代码实现:
public class ReadFromFile {
public static void main(String[] args) throws Exception {
Path file = Paths.get("/usr/a.txt");
AsynchronousFileChannel channel = AsynchronousFileChannel.open(file);
ByteBuffer buffer = ByteBuffer.allocate(100_000);
Future<Integer> result = channel.read(buffer, 0);
while (!result.isDone()) {
ProfitCalculator.calculateTax();
}
Integer bytesRead = result.get();
System.out.println("Bytes read [" + bytesRead + "]");
}
}
class ProfitCalculator {
public ProfitCalculator() {
}
public static void calculateTax() {
}
}
public class WriteToFile {
public static void main(String[] args) throws Exception {
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
Paths.get("/asynchronous.txt"), StandardOpenOption.READ,
StandardOpenOption.WRITE, StandardOpenOption.CREATE);
CompletionHandler<Integer, Object> handler = new CompletionHandler<Integer, Object>() {
@Override
public void completed(Integer result, Object attachment) {
System.out.println("Attachment: " + attachment + " " + result
+ " bytes written");
System.out.println("CompletionHandler Thread ID: "
+ Thread.currentThread().getId());
}
@Override
public void failed(Throwable e, Object attachment) {
System.err.println("Attachment: " + attachment + " failed with:");
e.printStackTrace();
}
};
System.out.println("Main Thread ID: " + Thread.currentThread().getId());
fileChannel.write(ByteBuffer.wrap("Sample".getBytes()), 0, "First Write",
handler);
fileChannel.write(ByteBuffer.wrap("Box".getBytes()), 0, "Second Write",
handler);
}
}
操作系统层面的IO模型
阻塞IO模型

进程(线程)阻塞与recvfrom系统调用。假设我们要进行socekt读取数据。我们必然会调用read方法,此时这个read方法就会触发操作系统内核的一次recvfrom系统调用。此时有两种情况。
- 内核还未接收到远端数据,此时数据还没有准备好,那么读取数据的线程就会一直阻塞,知道远端发来数据。这一阻塞的过程对应上图序号1的过程。然后再数据被从内核复制到用户空间这一过程中,改线程会被再次阻塞,知道复制完成,这一过程对应序号2的过程。
- 内核已经收到远端的数据,此时数据已经准备好,那么数据就会被从内核复制到用户空间,这一过程是阻塞的,对应图中序号2的过程。
阻塞IO模式的话,读一次数据会发生一次recvfrom系统调用。整个过程是阻塞的。内核数据还未准备好,用户进程(线程)阻塞。内核数据准备好,还要把数据从内核拷贝到用户空间,用户进程(线程)阻塞。当然,如果用户进程(线程)在阻塞过程中,如果recvfrom系统调用被信号终端,此时阻塞也是会被唤醒的。
非阻塞阻塞IO模型

-
当内核中数据还没准备好的时候,此时
recvfrom系统调用会返回一个EWOULDBLOCK错误,即不会将用户进程(线程)至于阻塞状态。ServerSocketChannel.configureBlocking(false);或SocketChannel…configureBlocking(false);时,我们调用ServerSocketChannel.accept()的null或SocketChannel.read(buffer)不会阻塞的,若没有新连接接入或内核中没有数据报准备好,此时会理解返回null或0的返回结果,说白了这个返回结果就是对应EWOULDBLOCK错误; -
当内核中的数据已经准备好,此时
recvfrom系统调用,用户进程(线程)还是会阻塞,知道内核中的数据已经拷贝到了用户空间,此时用户进程(线程)才会被唤醒来处理接收的数据。
非阻塞IO在用户数据还没准备好的时候,recvfrom系统调用不会被阻塞,接着会执行下一轮的recvfrom系统调用看数据是否准备好,周而复试,进程(线程)不但轮询,因此这是一个非常消耗cpu的。这种模式不是很常用,适合用在某台cpu转为某些功能准备的场合。
IO复用模型

I/O复用模型,这不是跟IO阻塞模型差不多么?当内核无数据准备好,select系统调用会被阻塞。当内核数据拷贝到用户空间时,此时recvfrom系统调用依旧会阻塞。那他们有什么区别呢?
I/O复用模型的优势就在于select操作。这个select操作可以选择多个文件描述符,分别对应Java NIO中的OP_CONNECT,OP_ACCEPT,OP_READ和OP_WRITE就绪事件。正是基于一次recvfrom系统调用中一个线程的select操作可以选择多个文件描述符这个功能。我们可以通过一个线程就能监听不同channel的OP_CONNECT,OP_ACCEPT,OP_READ和OP_WRITE这些就绪事件,然后根据某个就绪事件拿到相应的channel来做对应的操作。而不用项阻塞IO模型或非阻塞IO模型那样,一次recvfrom系统调用中一个线程就只能选择一个文件描述符。
就比如拿阻塞IO模型来说,由于用户进程(线程)每一次recvfrom系统调用都是阻塞且只对应一个文件描述符,此时如果服务端线程阻塞于客户端A的读操作时,如果有另外的客户端B需要接入服务端,此时服务端线程由于阻塞于客户端A的读操作,因此无法处理客户端B的连接操作。
信号驱动IO模型

信号驱动IO模型在等待数据期间是不会阻塞的。即用户进程(线程)发送一个sigaction系统调用后,此时立刻返回,并不会阻塞,然后用户进程(线程)继续执行;当数据报准备好时,此时内核就为该进程(线程)产生一个SIGIO信号,此时该进程(线程)就发生一次recvfrom系统调用将数据报从内核复制到用户空间,注意,这个阶段是阻塞的。
异步IO模型

异步IO模型也很好理解。即用户进程(线程)在等待数据从内核拷贝到用户空间这两个阶段都是非阻塞的。即用户进程(线程)发生一次系统调用后,立即返回,然后该用户进程(线程)继续往下执行。当内核把接收到数据报并把数据报拷贝到了用户空间后,此时再通知用户进程(线程)来处理用户空间的数据报。也就是说,这一些列IO操作都交给了内核去处理了,用户进程无须同步阻塞,因此是异步非阻塞的。
emsp; 扩展: 异步IO模型跟信号驱动IO模型的区别在于当内核准备好数据报后,对于信号驱动IO模型,此时内核会通知用户进程说数据报准备好啦,你需要发起系统调用来将数据报从内核拷贝到用户空间,此过程是同步阻塞的;而对于异步IO模型,当数据报准备好时,内核不会再通知用户进程,而是自己默默将数据报从内核拷贝到用户空间后然后再通知用户进程说,数据已经拷贝到用户空间啦,你直接进行业务逻辑处理就行。
各种IO模型区别

通过5种IO模型的比对,可以发现,前4中IO模型都是同步阻塞IO模型,因为其第二阶段数据报从内核拷贝到用户空间都是同步阻塞的,只是第一阶段等待数据报的处理不同;最后一种IO模型(异步IO模型)才是真正的异步非阻塞IO模型,内核将一切事情都干完(内核:我真的好累)。
线程模型
代码实现验证。原博主连接。
用户线程与内核线程关系
死磕java线程系列之线程模型原作者
在Java中,我们平时所说的并发编程、多线程、共享资源等概念都是与线程相关的,这里所说的线程实际上应该叫作“用户线程”,而对应到操作系统,还有另外一种线程叫作“内核线程”。
多对一模型(用户线程模型)
多个用户线程对应到同一个内核线程,线程的创建,调度,同步的所有细节全部有进程的用户空间线程库来处理。
优点:
用户线程的很多操作对内核来说是透明的,不需要用户态和内核态的频繁切换,使线程的创建,调度,同步等非常快。
缺点:
由于多个用户对应到同一个内核线程,如果其中一个用户线程阻塞,那个改其他用户线程也无法执行。
内核并不知道用户态有哪些线程,无法像内核线程一样实现较完整的调度,优先级等。
一对一模型(内核线程模型)
一个用户线程对应一个内核线程。内核负责线程的调度,可以调度到其他处理器上面。
优点:
实现简单
缺点:
对用户线程的操作都会映射到内核线程上,引起用户态和内核态的频繁切换。
内核为每个线程都映射调度实体,如果系统出现大量线程,会对系统性能有影响。Java使用的就是一对一线程模型,所有在java中启动一个线程要慎重。
多对多模型
用户线程与内核线程是多对多的映射关系。
区别于多对一模型,多对多模型中的一个进程可以与中多个内核线程关联,于是进程内的多个用户线程可以绑定不同的内核线程。这点和一对一模型相似。
区别于一对一模型,它的进程里的所有用户线程并不是与内核线程一一绑定,而是可以动态绑定内核线程,当某个内核线程因为其绑定的用户线程的阻塞操作被内核调度让出cpu时,其关联的进程中其他线程可以重新与其他内核线程绑定运行。
所以,多对多模型既不是多对一模型那种完全靠自己调度的也不是一对一模型完全靠操作系统调度的,而是中间态(自身调度与系统调度协同工作),因为这种模型的高度复杂性,操作系统内核开发者一般不会使用,所以更多时候是作为第三方库的形式出现。
优点:
兼具多对一模型的轻量。
由于对应多个内核线程,则一个用户线程阻塞时,其他用户线程仍可以执行。
由于对应多个内核线程,则可以实现较完整的调度,优先级等。
缺点:
实现复杂
Reactor线程模型
Netty系列文章之Netty线程模型
为什么要用Reactor模型?
传统的IO,当客户端发起请求,每个请求都需要独立线程处理,当并发数大时,创建线程数多,占用资源。
采用阻塞IO模型,连接建立后,若当线程没有数据可读,线程会阻塞在读操作上,造成资源浪费。
Reactor模式是基于事件驱动开发的。核心组成部分包括Reactor和线程池。Reactor负责监听事件和分配事件。线程池负责执行事件。
Selector
Selector一般称为选择器,可以翻译为多路复用。它是Java NIO的核心组件,用于检查一个或者多个NIO Channel(通道)的状态是否处于可读,可写。通过它可以实现单线程管理多个Channels。
优点:
使用Selector的好处在于,使用更少的线程就可以来处理通道了,相比使用多个线程,避免了线程上下文切换带来的开销。
- 可选择通道(SelectableChannel)
不是所有的Channel都可以被Selector复用的。比如说,FileChannel就不能被选择器复用。判断一个channel能不能被复用,就看它有没有继承一个SelectableChannel。如果继承,就可以复用。
SelectableChannel提供了实现通道选择所需要的公众方法。它是所有支持就绪检查通道类的父类,所有socket通道,都继承SelectableChannel类都是可选择的。包括从通道(Pipe)对象中获取得到的通道。
一个通道可以被注册到多个选择器上。通道和选择器之间的关系,使用注册方式完成。SelectableChannel可以被注册到Selector对象上,在注册时候,需要指定通道的哪些操作,是Selector感兴趣的。
- Channel注册到Selector
使用Channel.register(Selector sel,int pos)方法,将一个通道注册到一个选择器时。第一个参数,指定通道要注册的选择器。第二个参数指定选择器需要查询的通道操作。
可供选择器查询的通道操作,从类型上分,包括一下四种:
(1)可读。SelectionKey.OP_READ
(2)可写。SelectionKey.OP_WRITE
(3)连接。SelectionKey.OP_CONNECT
(4)接收。SelectionKey.OP_ACCEPT
如果 Selector 对通道的多操作类型感兴趣,可以用“位或”操作符来实现:
SelectionKey.OP_READ | SelectionKey.OP_WRITE;
选择器查询的不是通道的操作,而是通道的某个操作的一种就绪状态。什么
操作的就绪状态?一旦通道具备完成某个操作的条件,表示改通道的某个操作已经就绪,就可以被Selector查询到,程序可以对通道进行对应的操作。
比如说,某个SocketChannel通道可以连接到一个服务器,则处理"连接就绪"状态(OP_CONNECT)。再比如说,一个ServerSocketChanel服务器通道准备好接收新接入的连接,则处于"接收就绪"(OP_ACCEPT)状态。还比如说,一个数据可读的通道,可以说是"读就绪"(OP_READ)。一个等待写数据的通道就可以说是"写就绪"(OP_WRITE)。
- 选择键(SelectionKey)
(1)Channel 注册之后,并且一旦通道处于某种就绪状态,就可以被选择器查询到。这个工作使用选择器 Selector 的 select() 方法完成。select 方法的作用,对感兴趣的通道操作,进行就绪状态的查询。
(2)Selector 可以不断的查询 Channel 中发生的操作的就绪状态。并且选择甘心去的操作就绪状态。一旦通道有操作的就绪状态达成,并且是 Selecor 感兴趣的操作,就会被 Selector 选中,放入选择键集合中。
(3)一个选择键,首先包含了注册在 Selector 的通道操作的类型,比方说: SelectionKey.OP_READ . 也包含了特定的通道与特定的选择器之间的注册关系。
开发应用程序是,选择键是编程的关键,NIO 编程,就是更具对应的选择键,进行不同的业务逻辑处理。
(4)选择键的概念,和事件的概念比较相似。一个选择键类似监听器模式里面的一个事件。由于 Selector 不是事件触发的模式,而是主动去查询的模式,所以不叫事件 Event, 而是叫 SelectionKey 选择键。
Selector的使用
- Selector的创建
Selector selector = new Selector.open();
- 注册channel到Selector
// 1. 获取 Selector 选择器
Selector selector = Selector.open();
// 2. 获取通道
ServerSocketChannel socketChannel = ServerSocketChannel.open();
// 3. 设置为非阻塞
socketChannel.configureBlocking(false);
// 4. 绑定连接
socketChannel.bind(new InetSocketAddress(9999));
// 5. 将通道注册到选择器
socketChannel.register(selector, SelectionKey.OP_ACCEPT);
需要注意:
与Selector一起使用,channel要处于非阻塞模式下。否则将抛出异常IllegalBlockingModeException。
- 轮询选择器查询到的操作
(1) 通过 Selector 的 select() 方法, 可以查询出已经就绪的通道操作,有些就绪的状态集合,包含在一个元素是 Selectionkey 对象的 Set 集合中
(2) 下面是 Selector 几个重载的查询 select() 方法:
select() 阻塞到至少有一个通道在你注册的事件上就绪。
select(long timeout) 和 select() 一样,但最长阻塞事件为 timeout 毫秒。
selectNow() 非阻塞,只要有通道就立即返回。
select() 方法返回的 int 之,表示有多少通道已经就绪,准确的说目前一次 select
// 查询已经就绪的通道操作
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 判断 key 就绪状态操作
if (key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
}
iterator.remove();
- 停止选择的方法
选择器执行选择的过程汇总,系统底层会依次询问每个通道是否已经就绪,这个过程可能会造成调用线程进入阻塞状态,那么我们有一下三种方式可以唤醒在 select()方法中阻塞的线程。
wakeup() 方法:通过调用 Selector 对象的 wakeup() 方法让处于阻塞状态的 select() 方法立刻返回。
该方法使得选择器上的第一个哈没有返回的选择操作立即返回。如果当前没有进行中的选择操作,那么下一次会对 select() 方法的一次调用立即返回。
close() 方法: 通过 close() 方法关闭 selector
该方法使得任何一个在选择操作中阻塞的线程都被唤醒(类似 wakeup()) , 同时使的注册到该 Selector 的所有 Channel 被注销,所有的键都被取消,但是 Channel 本身不会关闭。
示例代码
- 服务端代码
@Test
public void server() throws IOException {
//1. 获取服务端通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2. 切换非阻塞模式
serverSocketChannel.configureBlocking(false);
//3. 创建 buffer
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
writeBuffer.put("收到了。。。。".getBytes(StandardCharsets.UTF_8));
//4. 绑定端口号
serverSocketChannel.bind(new InetSocketAddress(20000));
//5. 获取 selector 选择器
Selector selector = Selector.open();
//6. 通道注册到选择器,进行监听
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//7. 选择器进行轮训,进行后续操作
while (selector.select() > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> selectionKeyIterator = selectionKeys.iterator();
// 循环
while (selectionKeyIterator.hasNext()) {
// 获取就绪状态
SelectionKey k = selectionKeyIterator.next();
// 操作判断
if (k.isAcceptable()) {
// 获取连接
SocketChannel accept = serverSocketChannel.accept();
// 切换非阻塞模式
accept.configureBlocking(false);
// 注册
accept.register(selector, SelectionKey.OP_READ);
} else if (k.isReadable()) {
SocketChannel socketChannel = (SocketChannel) k.channel();
readBuffer.clear();
socketChannel.read(readBuffer);
readBuffer.flip();
System.out.println("received:" + new String(readBuffer.array(), StandardCharsets.UTF_8));
k.interestOps(SelectionKey.OP_WRITE);
} else if (k.isWritable()) {
writeBuffer.rewind();
SocketChannel socketChannel = (SocketChannel) k.channel();
socketChannel.write(writeBuffer);
k.interestOps(SelectionKey.OP_READ);
}
}
}
}
- 客户端代码
@Test
public void client() throws IOException {
//1. 获取通道,绑定主机和端口号
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(20000));
//2. 切换到非阻塞模式
socketChannel.configureBlocking(false);
//3. 创建 buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
//4. 写入 buffer 数据
buffer.put(new Date().toString().getBytes(StandardCharsets.UTF_8));
//5. 模式切换
buffer.flip();
//6. 写入通道
socketChannel.write(buffer);
//7. 关闭
buffer.clear();
socketChannel.close();
}
单线程模型

-
Reactor内部通过
selector监控连接事件,收到事件后通过dispatch进行分发,如果是连接建立的事件,则由Acceptor处理,Acceptor通过accept接受连接,并创建一个Handler来处理连接后续的各种事件,如果是读写事件,直接调用连接对应的Handler来处理 -
Handler完成
read->(decode->compute->encode)->send的业务流程
这种模型好处是简单,坏处却很明显,当某个Handler阻塞时,会导致其他客户端的handler和accpetor都得不到执行,无法做到高性能,只适用于业务处理非常快速的场景
多线程模型

-
主线程中,Reactor对象通过Selector监控
连接事件,收到事件后通过dispatch进行分发,如果是连接建立事件,则有Acceptor处理,Acceptor通过accept接收连接,并创建一个Handler来处理后续事件,而Handler只负责响应事件,不进行业务操作,也就是只进行read读取数据和write写出数据,业务处理交给一个线程池进行处理。 -
线程池分配一个线程完成真正的业务处理,然后将响应交给主进程的Handler处理,Handler将结果send给client。(下面是核心代码)

单Reactor承当所有事件的监听和响应,而当我们的服务端遇到大量的客户端同时进行连接,或者在请求连接时执行一些耗时操作,比如身份认证,权限检查等,这种瞬时的高并发就容易成为性能瓶颈
主从多线程模型

- 存在多个Reactor,每个Reactor都有自己的
selector选择器,线程和dispatch。 - 主线程中的mainReactor通过自己的
selector监控连接建立事件,收到事件后通过Acceptor接收,将新的连接分配给某个子线程。 - 子线程中subReactor将mainReactor分配的连接加入
连接队列中通过自己的selector进行监听,并创建一个Handler用于处理后续事件。 - Handler完成read-》业务处理-》send的完整业务流程

补充操作系统基础了解
-
cup与核心
(1)物理核:物理核心数量 = cpu数(机子上装的cpu的数量)*每个cpu的核心数
(2)虚拟和:比如4核8线程。4核是物理核心。通过超线程技术,用一个物理核模拟两个虚拟核。通过超线程技术可以实现单个物理核实现线程级别的并行计算,但是性能上比不上两个物理核。
(3)单核cpu与多核cpu:都是一个cpu,不同的是每个cpu上的核心数。多核cpu是多个单核cpu的替代方案,多核cpu减少了体积,同时也减少了功耗。一个核心只能同时处理一个线程。 -
进程与线程
(1)进程是操作系统进行资源(cpu,内存,磁盘等资源)分配的最小单位。
(2)线程是cpu调度和分配的基本单位。
(3)资源分配给进程,进程中线程共享资源。 -
线程切换
(1)cpu给线程分配时间片(也就是分配给线程的时间),执行完时间片后切换到另外一个线程。
(2)切换之前会保存线程的状态,下次时间片再给这个线程时就能知道当时的状态。
(3)从保存线程A的状态再到切换到线程B的状态,重新加载线程B的状态的这个过程就叫做上下文切换。
(4)上下切换时会消耗大量的cpu时间。 -
线程开销
(1)上下文切换的消耗
(2)线程创建和消亡的开销
(3)线程需要保持维持线程本地栈,会消耗内存 -
串行,并发,并行
(1)串行:多个任务,执行时一个执行完再执行另一个
(2)并发:多个线程在单核运行,同时时间一个线程运行,系统不停切换线程,看起来想同时运行,实际上是线程不能切换
(3)并行:每个线程分配给独立的核心线程同时执行 -
多核下线程数量选择
(1)计算密集型:程序主要用于进行逻辑判断和复杂的运算。cpu的利用率高,不用开太多线程,开太多线程反而会因为线程上下文切换而浪费时间。
(2)IO密集型:程序主要为IO操作,比如磁盘I/O和网络I/O。因为I/O操作会阻塞线程,cpu利用率不高,可以开多点线程,阻塞的时候可以切换到其他就绪线程,提高cpu利用率。 -
操作系统提供select,poll,epoll函数供系统调用
(1)select 实现I/O多路复用
操作系统内核帮我们遍历fd查询是否有可读/可写等事件,并且可以阻塞timeout事件至有感兴趣的事件发生后返回。
// select函数的定义,nfds为fd最大 + 1,readfds、writefds、exceptfds分别为所关心的读事件、写事件、异常事件的文件描述符,timeout为阻塞时间;返回值为整数代表有事件的文件描述符个数;具体那个有事件在对应fd_set中获得
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
存在一下问题。1.调用时直接传入fd_set监听描述符集合,受fd_setsize限制。linux系统下默认1024。2.调用后阻塞,遍历描述符集合,找到就绪的,随着监听的文件描述符多时,效率会降低。
(2)poll 实现I/O多路复用
poll的调用和select基本一致,同样需要传入文件描述符和事件,内核进行轮询,返回是否有事件发生并将事件设置在传入的参数中。
poll解决了select的监听文件描述符数量受限的问题,因为不在使用fd_set,而是使用结构体数组。
// poll函数的定义,fds为一个结构体数组
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd; /* file descriptor 要监听的文件描述符*/
short events; /* requested events to watch ,感兴趣的事件,如果为负,将不检测*/
short revents; /* returned events witnessed,所发生的事件 */
};
(3)epoll 实现I/O多路复用
epoll同样是操作系统提供的I/O多路复用的函数,解决与select,poll相同的问题,但相对后两者要更加强大。
int epoll_create(int size); // 创建一个管理约size个需要监听的fd的fd;
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // 监听一个fd,放在红黑树中,一旦就绪,内核会采用类似callback的回调机制,激活这个描述符,再调用epoll_wait时便得到通知
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout); // 查询一波事件
epoll具体包含epoll_create、epoll_ctl、epoll_wait三个函数。
1)首先调用epoll_create创建一个epoll实例,返回一个文件描述符epfd,这个文件描述符将用来管理需要监听事件的文件描述符
2)使用epoll_ctl向epfd注册要监听的文件描述符(即socket连接),epoll_event中包含要监听的文件描述符、要监听的事件、一些配置项等;这一步之后,epoll将要监听的文件描述符加入到一个红黑树中,当事件就绪,将这个文件描述符放入到另一个就绪集合中
3)应用程序调用epoll_wait,直接返回2中的就绪集合
epoll具体一下几个特点。
1)监听的fd数量基本不受限制,上限为打开文件的个数。
2)IO效率不会随着监听的fd数量增加而下降,不需要遍历,采用每个fd回调通知方式。
3)epoll_wait查询是否有就绪事件的时候不需要复制fd,因为在epoll_ctl中已经注册
4)LT模式:就绪后如果不操作,会继续通知;(select,poll均是这种模式)
5)ET模式:就绪后仅通知一次。这种模式仅epoll支持,在这种模式下,比如有2M数据就绪,而应用程序只读取了1M,下次再调用epoll_wait,这个文件描述符不会出现在就绪集合中,剩下的数据只能应用程序自行循环获取,nginx使用到。
686

被折叠的 条评论
为什么被折叠?



