review
stay Last section We have a general understanding of the creation process and function of threadpertakexcutor, but this is just to build an executor, which has not been really used yet, because before using, we need to build NioEventLoop, and the newChild method in this section is to do this.
Also, for the convenience of lazy people, I'll paste the coordinates again. You can see the forgotten ones Section I:
Here [coordinate 1]
- There is the newChild method in the for loop.
- The newChild method is to generate the NioEventLoop and save it in an array.
Netty Version: 4.1.6
The general process of newChild
- newChild
- new NioEventLoop
- Save the threadpertakexcutor created previously
- Create MpscQueue (task queue)
- Create selector
- new NioEventLoop
Follow up the newchill method
Find the newChild method from the above [signpost 1] and follow it (implementation class: NioEventLoopGroup):
io.netty.channel.nio.NioEventLoopGroup#newChild
protected EventLoop newChild(Executor executor, Object... args) throws Exception { return new NioEventLoop(this, executor, (SelectorProvider) args[0], ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]); }
- It can be found that it is new NioEventLoop
Follow up the constructor of NioEventLoop, where [coordinate 2]:
io.netty.channel.nio.NioEventLoop#NioEventLoop
NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider, SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) { super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler); if (selectorProvider == null) { throw new NullPointerException("selectorProvider"); } if (strategy == null) { throw new NullPointerException("selectStrategy"); } provider = selectorProvider; selector = openSelector(); selectStrategy = strategy; }
- The selector is created here, and it can be seen that each NioEventLoop is bound with a selector, which is produced by the provider.
The following is to follow up the optimization of selector, and know that you can skip to [coordinate 3] below
Here, we will not follow up super first, but look at the openSelector method. Because the code is relatively long, we will explain it in sections as follows:
io.netty.channel.nio.NioEventLoop#openSelector
selector = provider.openSelector();
- Call the api of jdk to create a selector
if (DISABLE_KEYSET_OPTIMIZATION) { return selector; }
- If the parameter is false, it means that the selector needs to be optimized (hashset will emit variable array), otherwise it will return directly.
final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();
The above SelectedSelectionKeySet is the optimized data structure. Follow up the constructor to see:
Basic attributes
SelectionKey[] keys; int size;
- It's not hard to see that the bottom layer of "set" is actually the well-known array, which is used to store the selectionKey (selectedKeys).
- Note that the above code is version 4.1.42. If it is 4.1.6, it is a double array. The implementation is a little more complex, but the principle is similar.
Add element (version: 4.1.42)
public boolean add(SelectionKey o) { if (o == null) { return false; } keys[size++] = o; if (size == keys.length) { increaseCapacity(); } return true; }
- The time complexity of inserting elements here is O(1), which is faster than HashSet.
Array expansion (version)
private void increaseCapacity() { SelectionKey[] newKeys = new SelectionKey[keys.length << 1]; System.arraycopy(keys, 0, newKeys, 0, size); keys = newKeys; }
- Double each time
In fact, the main purpose of optimizing into arrays is to reduce the time complexity of operations. Here [coordinate 3]
Get the Class object of selector through reflection
Object maybeSelectorImplClass = AccessController.doPrivileged(new PrivilegedAction<Object>() { @Override public Object run() { try { return Class.forName( "sun.nio.ch.SelectorImpl", false, PlatformDependent.getSystemClassLoader()); } catch (ClassNotFoundException e) { return e; } catch (SecurityException e) { return e; } } });
For the above code, we can continue to follow up SelectorImpl and find the following code:
protected Set<SelectionKey> selectedKeys = new HashSet();
- This proves that the data structure of selectedKeys is HashSet before Netty optimizes through reflection.
See how to replace with reflection
final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet(); // ... the process of obtaining, verifying and method body is omitted. Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys"); Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys"); selectedKeysField.setAccessible(true); publicSelectedKeysField.setAccessible(true); selectedKeysField.set(selector, selectedKeySet); publicSelectedKeysField.set(selector, selectedKeySet);
- From the above reflection code, replace the HashSet of the original selectedKeys with the SelectedSelectionKeySet
Now let's go back to the method of [coordinate 2] and go to super, where [coordinate 3]:
io.netty.channel.SingleThreadEventLoop#SingleThreadEventLoop(EventLoopGroup, Executor, boolean, int, RejectedExecutionHandler)
protected SingleThreadEventLoop(EventLoopGroup parent, Executor executor, boolean addTaskWakesUp, int maxPendingTasks, RejectedExecutionHandler rejectedExecutionHandler) { super(parent, executor, addTaskWakesUp, maxPendingTasks, rejectedExecutionHandler); tailTasks = newTaskQueue(maxPendingTasks); }
- The new task queue here can also be seen when entering super.
Advanced access to super:
protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor, boolean addTaskWakesUp, int maxPendingTasks, RejectedExecutionHandler rejectedHandler) { super(parent); this.addTaskWakesUp = addTaskWakesUp; this.maxPendingTasks = Math.max(16, maxPendingTasks); this.executor = ObjectUtil.checkNotNull(executor, "executor"); taskQueue = newTaskQueue(this.maxPendingTasks); rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, "rejectedHandler"); }
- You can see that the executor is saved here, which will be used in io.netty.util.concurrent.singlethreadeventexecutor and dostartthread.
- The mpsc queue is also created here, and the next step is to drop to.
Enter the newTaskQueue method (implementation class: NioEventLoop):
io.netty.channel.nio.NioEventLoop#newTaskQueue
protected Queue<Runnable> newTaskQueue(int maxPendingTasks) { // This event loop never calls takeTask() return PlatformDependent.newMpscQueue(maxPendingTasks); }
- Here I call the api of jdk to create MpscQueue. I set a breakpoint and finally return MpscChunkedArrayQueue.
As for mpscQueue, its full name is multiple producers (different threads) and a single consumer (one thread!). The detailed information given in the document is that mpscQueue is thread safe for [multi producer single consumer] mode, and the main function here is to store task queue.
Summary
- newChild is actually creating NioEventLoop.
- When NioEventLoop is created, the same threadpertakexcutor will be saved for NioEventLoop in each NioEventLoopGroup.
- During the construction of each NioEventLoop, a corresponding selector and mpscQueue will also be created.
- Each NioEventLoop corresponds to a selector.
- The mpscQueue of newChild is thread safe under specific (current) conditions, and the queue is provided by jdk, and Netty is not further encapsulated.
- Each selector has a selectorceys. By default, the HashSet stores the selectionKeys. But by default, it is replaced by the array by Netty. The advantage is to reduce the complexity of the operation elements.
Finally, we add a little bit of a doll: we know that there are multiple selectedkeys in a selector, but in fact, the selectedKey stores the selector, channel (saved as type Object), event set, etc.
The above sounds like "save each other". It's a bit convoluted. I found a good basis on the Internet for easy understanding: selectionKey can be seen as a bridge between selector and channel.