java iocp_LearningJDK/Iocp.java at master · zihuaVeryGood/LearningJDK · GitHub

/*

* Copyright (c) 2008, 2013, Oracle and/or its affiliates. All rights reserved.

* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.

*

* This code is free software; you can redistribute it and/or modify it

* under the terms of the GNU General Public License version 2 only, as

* published by the Free Software Foundation. Oracle designates this

* particular file as subject to the "Classpath" exception as provided

* by Oracle in the LICENSE file that accompanied this code.

*

* This code is distributed in the hope that it will be useful, but WITHOUT

* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or

* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License

* version 2 for more details (a copy is included in the LICENSE file that

* accompanied this code).

*

* You should have received a copy of the GNU General Public License version

* 2 along with this work; if not, write to the Free Software Foundation,

* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.

*

* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA

* or visit www.oracle.com if you need additional information or have any

* questions.

*/

package sun.nio.ch;

import java.io.Closeable;

import java.io.FileDescriptor;

import java.io.IOException;

import java.nio.channels.Channel;

import java.nio.channels.ShutdownChannelGroupException;

import java.nio.channels.spi.AsynchronousChannelProvider;

import java.util.HashMap;

import java.util.HashSet;

import java.util.Map;

import java.util.Set;

import java.util.concurrent.RejectedExecutionException;

import java.util.concurrent.locks.ReadWriteLock;

import java.util.concurrent.locks.ReentrantReadWriteLock;

import jdk.internal.misc.Unsafe;

/**

* Windows implementation of AsynchronousChannelGroup encapsulating an I/O completion port.

*/

// 通道组在windows上的实现,该实现与windows的完成端口机制息息相关

class Iocp extends AsynchronousChannelGroupImpl {

private static final long INVALID_HANDLE_VALUE = -1L;

/** handle to completion port */

private final long port; // "完成端口"对象(的本地引用)

private int nextCompletionKey; // 完成键,用来关联一个支持重叠IO结构的通道

/*

* 完成键到通道的映射。

* 将完成键与通道一一对应起来,这样可以通过完成键找到其对应的通道。

*/

private final Map keyToChannel = new HashMap<>();

/** maps completion key to channel */

// 操作keyToChannel时使用的锁

private final ReadWriteLock keyToChannelLock = new ReentrantReadWriteLock();

// 指示完成端口是否已经关闭

private boolean closed;

/**

* the set of "stale" OVERLAPPED structures.

* These OVERLAPPED structures relate to I/O operations

* where the completion notification was not received in a timely manner after the channel is closed.

*/

// 记录失效的重叠IO结构的本地引用

private final Set staleIoSet = new HashSet<>();

private static final Unsafe unsafe = Unsafe.getUnsafe();

static {

IOUtil.load();

initIDs();

}

/*▼ 构造器 ████████████████████████████████████████████████████████████████████████████████┓ */

/*

* 构造异步通道组,实际上是进行完成端口的初始化操作

*

* 涉及的完成端口操作:

*【1】通知系统内核创建"完成端口"对象;

*   限制本地(native层)允许新建的工作线程(先入后出)数量;

*   初始化IO完成队列(先入先出)

*

* 参见:Iocp#createIoCompletionPort()

*/

Iocp(AsynchronousChannelProvider provider, ThreadPool pool) throws IOException {

super(provider, pool);

/*

* 获取待创建的(本地)工作线程数量。

* 该参数用来告知IO完成端口在同一时间内最多能有多少个线程处于可运行状态。

*/

int threadCount = fixedThreadCount();

// 通知内核创建"完成端口"对象(此时只有最后一个参数有用),并返回"完成端口"对象(的本地引用)

this.port = createIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, threadCount);

// 初始化完成键

this.nextCompletionKey = 1;

}

/*▲ 构造器 ████████████████████████████████████████████████████████████████████████████████┛ */

/*▼ 启动 ████████████████████████████████████████████████████████████████████████████████┓ */

/*

* 在Java层和本地(native层)批量启动工作线程,以便后续处理已经完成的IO操作

*

* 涉及的完成端口操作:

*【2】启动Java层的工作线程;

*   启动本地(native层)的工作线程;

*   Java层与本地(native层)的工作线程是一一对应的;

*/

Iocp start() {

/*

* 构造一个轮询任务

*

* 该任务由工作线程执行,通过轮询从"完成端口"获取通知。

* 当没有新的通知时,工作线程陷入阻塞。

*/

EventHandlerTask task = new EventHandlerTask();

// 在Java层和本地(native层)批量启动工作线程,以执行上述构造的轮询任务

startThreads(task);

// 返回启动后的通道组

return this;

}

/*▲ 启动 ████████████████████████████████████████████████████████████████████████████████┛ */

/*▼ 执行任务 ████████████████████████████████████████████████████████████████████████████████┓ */

/*

* 将指定的任务推送到任务队列中,并且向阻塞的工作线程发送模拟IO信号,

* 以唤醒工作线程来处理任务队列中的task。

*/

@Override

void executeOnHandlerTask(Runnable task) {

synchronized(this) {

if(closed) {

throw new RejectedExecutionException();

}

// 将指定的任务推送到任务队列中

offerTask(task);

// 向阻塞的工作线程发送模拟IO信号,以唤醒阻塞在getQueuedCompletionStatus()上的工作线程来处理任务队列中的task

wakeup();

}

}

/*▲ 执行任务 ████████████████████████████████████████████████████████████████████████████████┛ */

/*▼ 外部通道 ████████████████████████████████████████████████████████████████████████████████┓ */

// 添加一个外部通道(不会与完成端口绑定),返回其对应的完成键

@Override

final Object attachForeignChannel(final Channel channel, FileDescriptor fdObj) throws IOException {

OverlappedChannel ch = new OverlappedChannel() {

public PendingFuture getByOverlapped(long overlapped) {

return null;

}

public void close() throws IOException {

channel.close();

}

};

return associate(ch, 0L);

}

// 根据指定的完成键,移除其对应的外部通道

@Override

final void detachForeignChannel(Object key) {

disassociate((Integer) key);

}

/*▲ 外部通道 ████████████████████████████████████████████████████████████████████████████████┛ */

/*▼ 关闭 ████████████████████████████████████████████████████████████████████████████████┓ */

// 关闭通道组内关联的所有通道

@Override

void closeAllChannels() {

/*

* On Windows the close operation will close the socket/file handle

* and then wait until all outstanding I/O operations have aborted.

* This is necessary as each channel's cache of OVERLAPPED structures

* can only be freed once all I/O operations have completed. As I/O

* completion requires a lookup of the keyToChannel then we must close

* the channels when not holding the write lock.

*/

final int MAX_BATCH_SIZE = 32;

OverlappedChannel[] channels = new OverlappedChannel[MAX_BATCH_SIZE];

int count;

do {

// grab a batch of up to 32 channels

keyToChannelLock.writeLock().lock();

count = 0;

try {

// 遍历映射中的完成键

for(Integer key : keyToChannel.keySet()) {

// 根据完成键获取对应的通道

channels[count++] = keyToChannel.get(key);

if(count >= MAX_BATCH_SIZE) {

break;

}

}

} finally {

keyToChannelLock.writeLock().unlock();

}

// 关闭所有通道

for(int i = 0; i

try {

// 关闭(释放)资源

channels[i].close();

} catch(IOException ignore) {

}

}

} while(count>0);

}

// 关闭所有工作线程(包括保底线程)

@Override

void shutdownHandlerTasks() {

// 获取Java层工作线程的数量(包含了保底线程的数量)

int nThreads = threadCount();

// 唤醒所有阻塞在getQueuedCompletionStatus()上的工作线程,并指示其结束运行

while(nThreads-->0) {

wakeup();

}

}

/*▲ 关闭 ████████████████████████████████████████████████████████████████████████████████┛ */

/*▼ 完成端口 ████████████████████████████████████████████████████████████████████████████████┓ */

/**

* Associate the given handle with this group

*/

/*

* 将指定文件(包括Socket)的引用handle关联到"完成端口"上,并在keyToChannel中记录handle所在通道(支持重叠IO结构)的引用。

* 返回值为与通道channel建立关联的完成键。

*

* 涉及的完成端口操作:

*【4】将通道channel与完成端口handle关联起来。

*   这样的话,当作用在channel上的IO操作结束后,才能通知到绑定的完成端口。

*/

int associate(OverlappedChannel channel, long handle) throws IOException {

// 加锁(写锁)

keyToChannelLock.writeLock().lock();

// generate a completion key (if not shutdown)

int completionKey;

try {

// 如果通道组已关闭,则抛异常

if(isShutdown()) {

throw new ShutdownChannelGroupException();

}

// generate unique key

do {

// 生成一个完成键(有效的编号从1开始)

completionKey = nextCompletionKey++;

// 如果该完成键已经在keyToChannel中了,则需要重试;简单地说,这里需要确保完成键的唯一性

} while((completionKey == 0) || keyToChannel.containsKey(completionKey));

/* associate with I/O completion port */

// 如果文件/Socket有效,则将其引用(handle)与完成端口进行关联

if(handle != 0L) {

createIoCompletionPort(handle, port, completionKey, 0);

}

// 对完成键与通道做映射

keyToChannel.put(completionKey, channel);

} finally {

// 解锁(写锁)

keyToChannelLock.writeLock().unlock();

}

return completionKey;

}

/**

* Disassociate channel from the group.

*/

// 解除与指定的完成键关联的异步IO通道

void disassociate(int key) {

boolean checkForShutdown = false;

keyToChannelLock.writeLock().lock();

try {

keyToChannel.remove(key);

/* last key to be removed so check if group is shutdown */

// 如果keyToChannel已经为空了,则需要做标记,稍后会关闭通道组

if(keyToChannel.isEmpty()) {

checkForShutdown = true;

}

} finally {

keyToChannelLock.writeLock().unlock();

}

// 如果keyToChannel已经为空了,而且异步IO通道组正在准备关闭

if(checkForShutdown && isShutdown()) {

try {

// 立即关闭异步IO通道组,包括:关闭通道、关闭工作线程、关闭线程池

shutdownNow();

} catch(IOException ignore) {

}

}

}

/*

* 该方法有两种用途:

* 1. 通知内核创建"完成端口"对象(此时只有最后一个参数有用),并返回"完成端口"对象(地址)

* concurrency: "完成端口"在并行中允许的最大线程数,一个经验值是可用CPU内核数量的两倍,以便提高资源利用率。

* 如果设置为0,则创建的工作线程数量由本地决定。

* 2. 为handle(文件/Socket引用)关联"完成端口"(existingPort)

* handle : 文件/Socket引用

* existingPort : 现有的"完成端口"对象

* completionKey: 完成键,用来关联一个支持重叠IO结构的通道(系统本身不关心这个值是多少)

*/

private static native long createIoCompletionPort(long handle, long existingPort, int completionKey, int concurrency) throws IOException;

/*

* 无限阻塞,直到"完成端口"completionPort有新的通知就绪时,获取该通知的内容,并存入status中

*

* completionPort:指向"完成端口"内核对象的指针

* status:存储IO结束后的返回的通知(数据)

*/

private static native void getQueuedCompletionStatus(long completionPort, CompletionStatus status) throws IOException;

// 唤醒阻塞在getQueuedCompletionStatus()上的工作线程

private void wakeup() {

try {

// 向"完成端口"port处的"完成端口"对象发送消息,可以唤醒阻塞的getQueuedCompletionStatus()方法

postQueuedCompletionStatus(port, 0);

} catch(IOException e) {

// should not happen

throw new AssertionError(e);

}

}

/*

* 向"完成端口"completionPort处的"完成端口"对象发送消息,可以唤醒阻塞的getQueuedCompletionStatus()方法

* 注:postQueuedCompletionStatus中传递的数据会到达getQueuedCompletionStatus()的CompletionStatus参数中

*/

private static native void postQueuedCompletionStatus(long completionPort, int completionKey) throws IOException;

// 关闭完成端口,并且释放已经失效的重叠IO结构在本地所占的内存

void implClose() {

synchronized(this) {

if(closed) {

return;

}

closed = true;

}

close0(port);

synchronized(staleIoSet) {

// 遍历所有失效的重叠IO结构

for(Long ov : staleIoSet) {

unsafe.freeMemory(ov);

}

staleIoSet.clear();

}

}

// 关闭完成端口

private static native void close0(long handle);

// 将重叠IO结构中的错误消息包装为IOException后返回

private static IOException translateErrorToIOException(int error) {

String msg = getErrorMessage(error);

if(msg == null) {

msg = "Unknown error: 0x0" + Integer.toHexString(error);

}

return new IOException(msg);

}

// 解析重叠IO结构中的错误消息

private static native String getErrorMessage(int error);

/*▲ 完成端口 ████████████████████████████████████████████████████████████████████████████████┛ */

// 判断通道组是否为空,即该通道组内是否包含通道

@Override

boolean isEmpty() {

keyToChannelLock.writeLock().lock();

try {

return keyToChannel.isEmpty();

} finally {

keyToChannelLock.writeLock().unlock();

}

}

/**

* Invoked when a channel associated with this port is closed before

* notifications for all outstanding I/O operations have been received.

*/

// 将指定的重叠IO结构标记为无效

void makeStale(Long overlapped) {

synchronized(staleIoSet) {

staleIoSet.add(overlapped);

}

}

/**

* Checks if the given OVERLAPPED is stale and if so, releases it.

*/

// 检查指定的重叠IO结构的本地引用,如果该重叠IO结构已经失效了,则释放其所占的本地内存

private void checkIfStale(long ov) {

synchronized(staleIoSet) {

boolean removed = staleIoSet.remove(ov);

if(removed) {

unsafe.freeMemory(ov);

}

}

}

private static native void initIDs();

/** Channels implements this interface support overlapped I/O and can be associated with a completion port. */

// 支持重叠IO结构的通道

interface OverlappedChannel extends Closeable {

/**

* Returns a reference to the pending I/O result.

*/

/*

* 从任务结果映射集移除一条记录,并返回移除掉的重叠IO结构缓存池

* 重叠IO结构被移除下来后,会先尝试将其缓存,缓存池已满时则直接释放重叠IO结构的本地内存

*/

PendingFuture getByOverlapped(long overlapped);

}

/** The handler for consuming the result of an asynchronous I/O operation. */

/*

* 显式设置异步任务的执行结果

*

* 首先会将结果设置给PendingFuture,如果用户显式设置了回调句柄CompletionHandler,

* 则还会进一步讲执行结果设置给CompletionHandler。

*/

interface ResultHandler {

/**

* Invoked if the I/O operation completes successfully.

*/

// 设置任务执行的结果

void completed(int bytesTransferred, boolean canInvokeDirect);

/**

* Invoked if the I/O operation fails.

*/

// 设置任务执行中的异常信息

void failed(int error, IOException ioe);

}

/** Container for data returned by GetQueuedCompletionStatus. */

/*

* IO完成记录,这是对本地(native层)IO完成队列中的元素的包装

*

* 参见:Iocp#getQueuedCompletionStatus()

*/

private static class CompletionStatus {

private int bytesTransferred; // 本次IO操作处理的字节数

private int completionKey; // 完成键,用来关联一个支持重叠IO结构的通道

private long overlapped; // 重叠IO结构的本地引用

private int error; // 异步IO操作中出现的错误信息,如超时、断网等

private CompletionStatus() {

}

int bytesTransferred() {

return bytesTransferred;

}

int completionKey() {

return completionKey;

}

long overlapped() {

return overlapped;

}

int error() {

return error;

}

}

/** Long-running task servicing system-wide or per-file completion port. */

/*

* 轮询任务

*

* 该任务由工作线程执行,通过轮询从"完成端口"获取通知。

* 当没有新的通知时,工作线程陷入阻塞。

*/

private class EventHandlerTask implements Runnable {

/*

* 执行轮询任务,即处理已经完成的IO操作;如果IO操作还未完成,则陷入阻塞

*

* 涉及的完成端口操作:

*【3】Java层与本地(native层)的工作线程已经启动;

*   执行getQueuedCompletionStatus(),尝试从IO完成队列获取已执行完成的IO记录;

*   当本地(native层)的IO完成队列为空时,Java层与本地(native层)的工作线程均陷入阻塞...

*   当有IO操作完成,或者收到模拟IO操作完成的请求时,会向IO完成队列插入一条记录;

*   接下来,唤醒本地(native层)的工作线程(后入先出,优先唤醒最后阻塞的线程),将出队的记录填充到ioResult中;

*   唤醒在Java层的工作线程,对本地(native层)已完成的IO操作做出响应(设置任务结果)。

*

* 注:getQueuedCompletionStatus()方法中的第一个参数是完成端口对象,它指定了工作线程对哪个完成端口感兴趣...

*   只有目标完成端口上收到"IO完成队列有新记录出现"的消息后,才会唤醒阻塞在该完成端口上的工作线程。

*   那么某个IO操作完成后,它如何知道通知哪个完成端口呢?

*   答案是事先会用createIoCompletionPort()方法来完成文件/Socket引用与完成端口的绑定,这也是发挥了createIoCompletionPort()方法的第二个作用。

*   这样的话,作用在某个文件/Socket上的IO操作结束后,其对应的完成端口就能收到通知了。

*

* 参见:AsynchronousChannelGroupImpl#startThreads(Runnable)

*/

public void run() {

// 获取当前线程内的线程局部缓存值:(递归)调用计数器

Invoker.GroupAndInvokeCount myGroupAndInvokeCount = Invoker.getGroupAndInvokeCount();

/*

* 是否允许在当前线程内直接处理回调句柄

*

* 除保底线程外,每个工作线程内都持有的局部缓存。

* 换句话说,myGroupAndInvokeCount不为null的话,说明当前线程不是保底线程。

*

* true : 允许在当前线程内直接处理回调句柄(除保底线程之外的工作线程) -固定 非固定

* false: 不能在当前线程内直接处理回调句柄(保底线程) - 非固定

*/

boolean canInvokeDirect = (myGroupAndInvokeCount != null);

/*

* 构造一条空的IO完成记录。

* 等IO完成队列中有新的元素进来后,会将这些元素出队并填充到这条IO完成记录中。

*/

CompletionStatus ioResult = new CompletionStatus();

/*

* 在当前线程退出时,释放需要重启一个线程来替换当前线程。

* 如果是因为发生了错误/异常而导致当前线程退出,那么往往需要重启一个线程来替换当前线程,即设置replaceMe为true。

* 如果是正常被唤醒后退出,那么不需要再重启别的线程。

*/

boolean replaceMe = false;

try {

for(; ; ) {

/* reset invoke count */

if(myGroupAndInvokeCount != null) {

myGroupAndInvokeCount.resetInvokeCount();

}

/* wait for I/O completion event. A error here is fatal (thread will not be replaced) */

replaceMe = false;

//========================本地(native层)的IO完成队列为空时,当前工作线程将进入阻塞========================//

try {

// 无限阻塞,直到"完成端口"port有新的通知就绪时,获取该通知的内容,并存入ioResult中

getQueuedCompletionStatus(port, ioResult);

} catch(IOException x) {

// should not happen

x.printStackTrace();

return;

}

//========================工作线程醒来后,获取IO完成记录中的信息========================//

// 从IO完成记录中获取到完成键

int completionKey = ioResult.completionKey();

// 从IO完成记录中获取到重叠IO结构的本地引用

long overlapped = ioResult.overlapped();

// 从IO完成记录中获取到传输(读取/写入)的字节数量

int bytesTransferred = ioResult.bytesTransferred();

// 从IO完成记录中获取到异步IO操作中出现的错误信息

int error = ioResult.error();

//========================被模拟IO信号(请求)唤醒========================//

/*

* 当非工作线程向完成端口发送了一个模拟的IO操作之后,这个模拟IO操作在执行结束后,也会进入IO完成队列。

* 这就会触发唤醒工作线程的流程...

* 这种发送模拟IO信号的机制,可以强制让工作线程醒来,并让工作线程执行预设的任务,或者直接让工作线程退出。

*

* 注:发送模拟IO信号的功能是由postQueuedCompletionStatus()方法来完成的。

*

* 参见:Iocp#wakeup()

*/

/* handle wakeup to execute task or shutdown */

if(completionKey == 0 && overlapped == 0L) {

Runnable task = pollTask();

// 如果被模拟信号唤醒,但是没有待执行的任务,说明是收到了"关闭"的指示,此时应当结束当前工作线程

if(task == null) {

return;

}

/* run task (if error/exception then replace thread) */

replaceMe = true;

/*

* 处理任务队列中的task,而这个task通常就是处理回调句柄

*

* 参见:Iocp#executeOnHandlerTask

*/

task.run();

continue;

}

//========================被常规的异步IO操作唤醒========================//

/*

* 每个异步IO操作完成后,会生成一条IO完成记录插入到IO完成队列中。

* 然后,唤醒本地(native层)的工作线程,并取出IO完成队列中的队头元素进行处理。

* 检查队头的IO完成记录,根据其作用的文件/Socket,找到其关联的完成端口。

* 唤醒阻塞在该完成端口上的工作线程(Java层)

*

* 注:工作线程被唤醒遵循后入先出,即最后阻塞的工作线程会被优先唤醒。

*   如果某个IO操作足够耗时的话,会发现总是同一个工作线程被频繁地唤醒,这也是后入先出的表现。

*/

/* map key to channel */

OverlappedChannel channel = null;

// 加锁(读锁)

keyToChannelLock.readLock().lock();

try {

/*

* 通过完成键,找到上述IO完成记录对应的通道。

* 换句话说,就是了解这条IO完成记录中的IO操作是作用在哪个通道上的。

*/

channel = keyToChannel.get(completionKey);

/*

* 如果ch为null,说明keyToChannel中已经把completionKey对应的那条记录移除了。

* 参见:Iocp#disassociate()

*/

if(channel == null) {

// 检查指定的重叠IO结构的本地引用,如果该重叠IO结构已经失效了,则释放其所占的本地内存

checkIfStale(overlapped);

// 继续遍历,从IO完成队列中获取下一个队头元素

continue;

}

} finally {

// 解锁(读锁)

keyToChannelLock.readLock().unlock();

}

/* lookup I/O request */

/*

* 根据重叠IO结构的本地引用,从重叠IO结构的缓存池中检索出与overlapped关联的挂起的任务。

*

* 只有在目标IO操作没成效的情形下,这里才会获取到非null的任务。

* 因为如果IO操作没成效,任务执行结果在当场就被设置完了,没必要等到这里再处理。

*

* 参见Iocp.ResultHandler的实现类

*/

PendingFuture, ?> future = channel.getByOverlapped(overlapped);

// 如果future为null,意味着该重叠IO结构关联的future已被移除了,此处需要考虑清理overlapped

if(future == null) {

/*

* we get here if the OVERLAPPED structure is associated with an I/O operation on a channel that was closed but the I/O operation event wasn't read in a timely manner.

* Alternatively, it may be related to a tryLock operation as the OVERLAPPED structures for these operations are not in the I/O cache.

*/

// 检查指定的重叠IO结构的本地引用,如果该重叠IO结构已经失效了,则释放其所占的本地内存

checkIfStale(overlapped);

continue;

}

/*

* 至此,说明之前的IO操作是有成效的,因此挂起了一个future等待填充执行结果。

* 现在获取到了这个挂起的任务,可以向其填充执行结果了。

*/

/* synchronize on result in case I/O completed immediately and was handled by initiator */

synchronized(future) {

/*

* 如果future已经被提前填充了执行结果,那么后续操作也没必要进行了。

* 例如在限定时间内等不到预想的执行结果时,可以自行填充一个执行结果。

*/

if(future.isDone()) {

continue;

}

// not handled by initiator

}

/* (if error/exception then replace thread) */

replaceMe = true;

// 获取异步IO操作的引用(ReadTask/WriteTask/LockTask/ConnectTask/AcceptTask)

ResultHandler rh = (ResultHandler) future.getContext();

// 如果一切正常,那么可以设置任务执行结果

if(error == 0) {

/*

* bytesTransferred: 本地操作传输的字节数

* canInvokeDirect : 是否允许在当前线程内直接处理回调句柄

*/

rh.completed(bytesTransferred, canInvokeDirect);

// 如果有内部错误发生,则设置任务执行中的异常信息

} else {

IOException ex = translateErrorToIOException(error);

rh.failed(error, ex);

}

} // for(;;)

} finally {

// last thread to exit when shutdown releases resources

int remaining = threadExit(this, replaceMe);

// 如果Java层的工作线程全部退出了,且通道组也已关闭了

if(remaining == 0 && isShutdown()) {

// 接下来要关闭完成端口,并释放那些已经失效的重叠IO结构占用的本地内存

implClose();

}

}

}

}

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值