转载请声明出处哦~,本篇文章发布于luozhiyun的博客:https://www.luozhiyun.com

本文使用的go的源码15.7

可以从 Go 源码目录结构和对应代码文件了解 Go 在不同平台下的网络 I/O 模式的实现。比如,在 Linux 系统下基于 epoll,freeBSD 系统下基于 kqueue,以及 Windows 系统下基于 iocp。

因为我们的代码都是部署在Linux上的,所以本文以epoll封装实现为例子来讲解Go语言中I/O多路复用的源码实现。

介绍

I/O多路复用

所谓 I/O 多路复用指的就是 select/epoll 这一系列的多路选择器:支持单一线程同时监听多个文件描述符(I/O 事件),阻塞等待,并在其中某个文件描述符可读写时收到通知。以防很多同学对select或epoll不那么熟悉,所以下面先来讲讲这两个选择器。

首先我们先说一下什么是文件描述符(File descriptor),根据它的英文首字母也简称FD,它是一个用于表述指向文件的引用的抽象化概念。它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。

select

int select(int nfds,
fd_set *restrict readfds,
fd_set *restrict writefds,
fd_set *restrict errorfds,
struct timeval *restrict timeout);

writefds、readfds、和exceptfds是三个文件描述符集合。select会遍历每个集合的前nfds个描述符,分别找到可以读取、可以写入、发生错误的描述符,统称为就绪的描述符。

timeout参数表示调用select时的阻塞时长。如果所有文件描述符都未就绪,就阻塞调用进程,直到某个描述符就绪,或者阻塞超过设置的 timeout 后,返回。如果timeout参数设为 NULL,会无限阻塞直到某个描述符就绪;如果timeout参数设为 0,会立即返回,不阻塞。

当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。

select的缺点也列举一下:

  1. select最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FD_SETSIZE设置,默认值是1024;
  2. 每次调用 select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大;
  3. 每次 kernel 都需要线性扫描整个 fd_set,所以随着监控的描述符 fd 数量增长,其 I/O 性能会线性下降;

epoll

epoll是selec的增强版本,避免了“性能开销大”和“文件描述符数量少”两个缺点。

为方便理解后续的内容,先看一下epoll的用法:

int listenfd = socket(AF_INET, SOCK_STREAM, 0);
bind(listenfd, ...)
listen(listenfd, ...) int epfd = epoll_create(...);
epoll_ctl(epfd, ...); //将所有需要监听的fd添加到epfd中 while(1){
int n = epoll_wait(...)
for(接收到数据的socket){
//处理
}
}

先用epoll_create创建一个epoll对象实例epfd,同时返回一个引用该实例的文件描述符,返回的文件描述符仅仅指向对应的epoll实例,并不表示真实的磁盘文件节点。

epoll实例内部存储:

  • 监听列表:所有要监听的文件描述符,使用红黑树;
  • 就绪列表:所有就绪的文件描述符,使用链表;

再通过epoll_ctl将需要监视的fd添加到epfd中,同时为fd设置一个回调函数,并监听事件event,并添加到监听列表中。当有事件发生时,会调用回调函数,并将fd添加到epoll实例的就绪队列上。

最后调用epoll_wait阻塞监听 epoll 实例上所有的fd的 I/O 事件。当就绪列表中已有数据,那么epoll_wait直接返回,解决了select每次都需要轮询一遍的问题。

epoll的优点:

epoll的监听列表使用红黑树存储,epoll_ctl 函数添加进来的 fd 都会被放在红黑树的某个节点内,而红黑树本身插入和删除性能比较稳定,时间复杂度 O(logN),并且可以存储大量的的fd,避免了只能存储1024个fd的限制;

epoll_ctl 中为每个文件描述符指定了回调函数,并在就绪时将其加入到就绪列表,因此不需要像select一样遍历检测每个文件描述符,只需要判断就绪列表是否为空即可;

解析

netpoll本质上是对 I/O 多路复用技术的封装,所以自然也是和epoll一样脱离不了下面几步:

  1. netpoll创建及其初始化;
  2. 向netpoll中加入待监控的任务;
  3. 从netpoll获取触发的事件;

在go中对epoll提供的三个函数进行了封装:

func netpollinit()
func netpollopen(fd uintptr, pd *pollDesc) int32
func netpoll(delay int64) gList

netpollinit函数负责初始化netpoll;

netpollopen负责监听文件描述符上的事件;

netpoll会阻塞等待返回一组已经准备就绪的 Goroutine;

下面是Go语言中编写的一个TCP server:

func main() {
listen, err := net.Listen("tcp", ":8888")
if err != nil {
fmt.Println("listen error: ", err)
return
}
for {
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept error: ", err)
break
}
// 创建一个goroutine来负责处理读写任务
go HandleConn(conn)
}
}

下面我们跟着这个TCP server的源码一起看看是在哪里使用了netpoll来完成epoll的调用。

net.Listen

这个TCP server中会调用net.Listen创建一个socket同时返回与之对应的fd,该fd用来初始化listener的netFD(go层面封装的网络文件描述符),接着调用 netFD的listenStream方法完成对 socket 的 bind&listen和netFD的初始化。

调用过程如下:

func socket(ctx context.Context, net string, family, sotype, proto int, ipv6only bool, laddr, raddr sockaddr, ctrlFn func(string, string, syscall.RawConn) error) (fd *netFD, err error) {
// 创建一个socket
s, err := sysSocket(family, sotype, proto)
if err != nil {
return nil, err
}
...
// 创建fd
if fd, err = newFD(s, family, sotype, net); err != nil {
poll.CloseFunc(s)
return nil, err
}
if laddr != nil && raddr == nil {
switch sotype {
case syscall.SOCK_STREAM, syscall.SOCK_SEQPACKET:
// 调用 netFD的listenStream方法完成对 socket 的 bind&listen和netFD的初始化
if err := fd.listenStream(laddr, listenerBacklog(), ctrlFn); err != nil {
fd.Close()
return nil, err
}
return fd, nil
case syscall.SOCK_DGRAM:
...
}
}
...
return fd, nil
} func newFD(sysfd syscall.Handle, family, sotype int, net string) (*netFD, error) {
ret := &netFD{
pfd: poll.FD{
Sysfd: sysfd,
IsStream: sotype == syscall.SOCK_STREAM,
ZeroReadIsEOF: sotype != syscall.SOCK_DGRAM && sotype != syscall.SOCK_RAW,
},
family: family,
sotype: sotype,
net: net,
}
return ret, nil
}

sysSocket方法会发起一个系统调用创建一个socket,newFD会创建一个netFD,然后调用netFD的listenStream方法进行bind&listen操作,并对netFD进行init。

netFD是一个文件描述符的封装,netFD中包含一个FD数据结构,FD中包含了Sysfd 和pollDesc两个重要的数据结构,Sysfd是sysSocket返回的socket系统文件描述符,pollDesc用于监控文件描述符的可读或者可写。

我们继续看listenStream:

func (fd *netFD) listenStream(laddr sockaddr, backlog int, ctrlFn func(string, string, syscall.RawConn) error) error {
...
// 完成绑定操作
if err = syscall.Bind(fd.pfd.Sysfd, lsa); err != nil {
return os.NewSyscallError("bind", err)
}
// 进行监听操作
if err = listenFunc(fd.pfd.Sysfd, backlog); err != nil {
return os.NewSyscallError("listen", err)
}
// 初始化fd
if err = fd.init(); err != nil {
return err
}
lsa, _ = syscall.Getsockname(fd.pfd.Sysfd)
fd.setAddr(fd.addrFunc()(lsa), nil)
return nil
}

listenStream方法会调用Bind方法完成fd的绑定操作,然后调用listenFunc进行监听,接着调用fd的init方法,完成FD、pollDesc初始化。

func (pd *pollDesc) init(fd *FD) error {
// 调用到runtime.poll_runtime_pollServerInit
serverInit.Do(runtime_pollServerInit)
// 调用到runtime.poll_runtime_pollOpen
ctx, errno := runtime_pollOpen(uintptr(fd.Sysfd))
...
return nil
}

runtime_pollServerInit用Once封装保证只能被调用一次,这个函数在Linux平台上会创建一个epoll文件描述符实例;

poll_runtime_pollOpen调用了netpollopen会将fd注册到 epoll实例中,并返回一个pollDesc;

netpollinit初始化

func poll_runtime_pollServerInit() {
netpollGenericInit()
} func netpollGenericInit() {
if atomic.Load(&netpollInited) == 0 {
lock(&netpollInitLock)
if netpollInited == 0 {
netpollinit()
atomic.Store(&netpollInited, 1)
}
unlock(&netpollInitLock)
}
}

netpollGenericInit会调用平台上特定实现的netpollinit,在Linux中会调用到netpoll_epoll.go的netpollinit方法:

var (
epfd int32 = -1 // epoll descriptor
) func netpollinit() {
// 创建一个新的 epoll 文件描述符
epfd = epollcreate1(_EPOLL_CLOEXEC)
...
// 创建一个用于通信的管道
r, w, errno := nonblockingPipe()
...
ev := epollevent{
events: _EPOLLIN,
}
*(**uintptr)(unsafe.Pointer(&ev.data)) = &netpollBreakRd
// 将读取数据的文件描述符加入监听
errno = epollctl(epfd, _EPOLL_CTL_ADD, r, &ev)
...
netpollBreakRd = uintptr(r)
netpollBreakWr = uintptr(w)
}

调用epollcreate1方法会创建一个epoll文件描述符实例,需要注意的是epfd是一个全局的属性。然后创建一个用于通信的管道,调用epollctl将读取数据的文件描述符加入监听。

netpollopen加入事件监听

下面再看看poll_runtime_pollOpen方法:

func poll_runtime_pollOpen(fd uintptr) (*pollDesc, int) {
pd := pollcache.alloc()
lock(&pd.lock)
if pd.wg != 0 && pd.wg != pdReady {
throw("runtime: blocked write on free polldesc")
}
if pd.rg != 0 && pd.rg != pdReady {
throw("runtime: blocked read on free polldesc")
}
pd.fd = fd
pd.closing = false
pd.everr = false
pd.rseq++
pd.rg = 0
pd.rd = 0
pd.wseq++
pd.wg = 0
pd.wd = 0
pd.self = pd
unlock(&pd.lock) var errno int32
errno = netpollopen(fd, pd)
return pd, int(errno)
} func netpollopen(fd uintptr, pd *pollDesc) int32 {
var ev epollevent
ev.events = _EPOLLIN | _EPOLLOUT | _EPOLLRDHUP | _EPOLLET
*(**pollDesc)(unsafe.Pointer(&ev.data)) = pd
return -epollctl(epfd, _EPOLL_CTL_ADD, int32(fd), &ev)
}

poll_runtime_pollOpen方法会通过pollcache.alloc初始化总大小约为 4KB的pollDesc结构体。然后重置pd的属性,调用netpollopen向epoll实例epfd加入新的轮询事件监听文件描述符的可读和可写状态。

下面我们再看看pollCache是如何初始化pollDesc的。

type pollCache struct {
lock mutex
first *pollDesc
} const pollBlockSize = 4 * 1024 func (c *pollCache) alloc() *pollDesc {
lock(&c.lock)
// 初始化首节点
if c.first == nil {
const pdSize = unsafe.Sizeof(pollDesc{})
n := pollBlockSize / pdSize
if n == 0 {
n = 1
}
mem := persistentalloc(n*pdSize, 0, &memstats.other_sys)
// 初始化pollDesc链表
for i := uintptr(0); i < n; i++ {
pd := (*pollDesc)(add(mem, i*pdSize))
pd.link = c.first
c.first = pd
}
}
pd := c.first
c.first = pd.link
lockInit(&pd.lock, lockRankPollDesc)
unlock(&c.lock)
return pd
}

pollCache的链表头如果为空,那么初始化首节点,首节点是一个pollDesc的链表头,每次调用该结构体都会返回链表头还没有被使用的pollDesc。

到这里就完成了net.Listen的分析,下面我们看看listen.Accept。

Listener.Accept

Listener.Accept方法最终会调用到netFD的accept方法中:

func (fd *netFD) accept() (netfd *netFD, err error) {
// 调用netfd.FD的Accept接受新的 socket 连接,返回 socket 的 fd
d, rsa, errcall, err := fd.pfd.Accept()
...
// 构造一个新的netfd
if netfd, err = newFD(d, fd.family, fd.sotype, fd.net); err != nil {
poll.CloseFunc(d)
return nil, err
}
// 调用 netFD 的 init 方法完成初始化
if err = netfd.init(); err != nil {
netfd.Close()
return nil, err
}
lsa, _ := syscall.Getsockname(netfd.pfd.Sysfd)
netfd.setAddr(netfd.addrFunc()(lsa), netfd.addrFunc()(rsa))
return netfd, nil
}

这个方法首先会调用到FD的Accept接受新的 socket 连接,并返回新的socket对应的fd,然后调用newFD构造一个新的netfd,并通过init 方法完成初始化。

init方法上面我们已经看过了,下面我们来看看Accept方法:

func (fd *FD) Accept() (int, syscall.Sockaddr, string, error) {
...
for {
// 使用 linux 系统调用 accept 接收新连接,创建对应的 socket
s, rsa, errcall, err := accept(fd.Sysfd)
if err == nil {
return s, rsa, "", err
}
switch err {
case syscall.EINTR:
continue
case syscall.EAGAIN:
if fd.pd.pollable() {
// 如果当前没有发生期待的 I/O 事件,那么 waitRead 会通过 park goroutine 让逻辑 block 在这里
if err = fd.pd.waitRead(fd.isFile); err == nil {
continue
}
}
case syscall.ECONNABORTED:
continue
}
return -1, nil, errcall, err
}
}

FD.Accept方法会使用 linux 系统调用 accept 接收新连接,创建对应的 socket,如果没有可读的消息,waitRead会被阻塞。这些被park住的goroutine会在goroutine的调度中调用runtime.netpoll被唤醒。

pollWait事件等待

pollDesc.waitRead实际上是调用了runtime.poll_runtime_pollWait

func poll_runtime_pollWait(pd *pollDesc, mode int) int {
...
// 进入 netpollblock 并且判断是否有期待的 I/O 事件发生
for !netpollblock(pd, int32(mode), false) {
...
}
return 0
} func netpollblock(pd *pollDesc, mode int32, waitio bool) bool {
gpp := &pd.rg
if mode == 'w' {
gpp = &pd.wg
}
// 这个 for 循环是为了等待 io ready 或者 io wait
for {
old := *gpp
// gpp == pdReady 表示此时已有期待的 I/O 事件发生,
// 可以直接返回 unblock 当前 goroutine 并执行响应的 I/O 操作
if old == pdReady {
*gpp = 0
return true
}
if old != 0 {
throw("runtime: double wait")
}
// 如果没有期待的 I/O 事件发生,则通过原子操作把 gpp 的值置为 pdWait 并退出 for 循环
if atomic.Casuintptr(gpp, 0, pdWait) {
break
}
}
if waitio || netpollcheckerr(pd, mode) == 0 {
// 让出当前线程,将 Goroutine 转换到休眠状态并等待运行时的唤醒
gopark(netpollblockcommit, unsafe.Pointer(gpp), waitReasonIOWait, traceEvGoBlockNet, 5)
}
// be careful to not lose concurrent pdReady notification
old := atomic.Xchguintptr(gpp, 0)
if old > pdWait {
throw("runtime: corrupted polldesc")
}
return old == pdReady
}

poll_runtime_pollWait会用for循环调用netpollblock函数判断是否有期待的 I/O 事件发生,直到netpollblock返回true表示io ready才会走出循环。

netpollblock方法会判断当前的状态是不是处于pdReady,如果是那么直接返回true;如果不是,那么将gpp通过CAS设置为pdWait并退出 for 循环。通过gopark 把当前 goroutine 给 park 住,直到对应的 fd 上发生可读/可写或者其他I/O 事件为止。

这些被park住的goroutine会在goroutine的调度中调用runtime.netpoll被唤醒。

netpoll轮询等待

runtime.netpoll的核心逻辑是: 根据入参 delay设置调用 epoll_wait 的 timeout 值,调用 epoll_wait 从 epoll 的 eventpoll.rdllist双向列表中获取IO就绪的fd列表,遍历epoll_wait 返回的fd列表, 根据调用epoll_ctl注册fd时封装的上下文信息组装可运行的 goroutine 并返回。

执行完 netpoll 之后,会返回一个就绪 fd 列表对应的 goroutine 列表,接下来将就绪的 goroutine 加入到调度队列中,等待调度运行。

func netpoll(delay int64) gList {
if epfd == -1 {
return gList{}
}
var waitms int32
// 因为传入delay单位是纳秒,下面将纳秒转换成毫秒
if delay < 0 {
waitms = -1
} else if delay == 0 {
waitms = 0
} else if delay < 1e6 {
waitms = 1
} else if delay < 1e15 {
waitms = int32(delay / 1e6)
} else {
// An arbitrary cap on how long to wait for a timer.
// 1e9 ms == ~11.5 days.
waitms = 1e9
}
var events [128]epollevent
retry:
// 等待文件描述符转换成可读或者可写
n := epollwait(epfd, &events[0], int32(len(events)), waitms)
// 返回负值,那么重新调用epollwait进行等待
if n < 0 {
...
goto retry
}
var toRun gList
// 意味着被监控的文件描述符出现了待处理的事件
for i := int32(0); i < n; i++ {
ev := &events[i]
if ev.events == 0 {
continue
}
...
// 判断发生的事件类型,读类型或者写类型
var mode int32
if ev.events&(_EPOLLIN|_EPOLLRDHUP|_EPOLLHUP|_EPOLLERR) != 0 {
mode += 'r'
}
if ev.events&(_EPOLLOUT|_EPOLLHUP|_EPOLLERR) != 0 {
mode += 'w'
}
if mode != 0 {
// 取出保存在 epollevent 里的 pollDesc
pd := *(**pollDesc)(unsafe.Pointer(&ev.data))
pd.everr = false
if ev.events == _EPOLLERR {
pd.everr = true
}
// 调用 netpollready,传入就绪 fd 的 pollDesc
netpollready(&toRun, pd, mode)
}
}
return toRun
}

netpoll会调用epollwait获取就绪的 fd 列表,对应的epoll函数是epoll_wait。toRun是一个 g 的链表,存储要恢复的 goroutines,最后返回给调用方。如果epollwait返回的n大于零,那么表示被监控的文件描述符出现了待处理的事件,那么需要调用for循环进行处理。循环里面会根据时间类型设置mode,然后拿出对应的pollDesc,调用netpollready方法。

下面我们再看一下netpollready:

func netpollready(toRun *gList, pd *pollDesc, mode int32) {
var rg, wg *g
// 获取对应的g的指针
if mode == 'r' || mode == 'r'+'w' {
rg = netpollunblock(pd, 'r', true)
}
if mode == 'w' || mode == 'r'+'w' {
wg = netpollunblock(pd, 'w', true)
}
// 将对应的g加入到toRun列表中
if rg != nil {
toRun.push(rg)
}
if wg != nil {
toRun.push(wg)
}
} func netpollunblock(pd *pollDesc, mode int32, ioready bool) *g {
gpp := &pd.rg
// 根据传入的mode判断事件类型
if mode == 'w' {
gpp = &pd.wg
} for {
// 取出 gpp 存储的 g
old := *gpp
if old == pdReady {
return nil
}
if old == 0 && !ioready {
return nil
}
var new uintptr
if ioready {
new = pdReady
}
// cas 将读或者写信号量转换成 pdReady
if atomic.Casuintptr(gpp, old, new) {
if old == pdWait {
old = 0
}
// 返回对应的 g指针
return (*g)(unsafe.Pointer(old))
}
}
}

讲完了runtime.netpoll的源码有个需要注意的地方,调用runtime.netpoll的地方有两处:

  • 在调度器中执行runtime.schedule(),该方法中会执行runtime.findrunable(),在runtime.findrunable()中调用了runtime.netpoll获取待执行的goroutine;
  • Go runtime 在程序启动的时候会创建一个独立的sysmon监控线程,sysmon 每 20us~10ms 运行一次,每次运行会检查距离上一次执行netpoll是否超过10ms,如果是则会调用一次runtime.netpoll

这些入口的调用感兴趣的可以自己去看看。

总结

本文从I/O多路复用开始讲解select以及epoll,然后再回到go语言中去看它是如何实现多路复用这样的结构的。通过追踪源码可以发现,其实go也是根据epoll来封装自己的函数:

func netpollinit()
func netpollopen(fd uintptr, pd *pollDesc) int32
func netpoll(block bool) gList

通过这三个函数来实现对epoll的创建实例、注册、事件等待操作。

对于I/O多路复用不是很了解的同学也可以借此机会多多的去学习一下网络编程方面的知识,扩充一下知识面。

Reference

https://www.infoq.cn/article/boeavgkiqmvcj8qjnbxk

https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-netpoller/#66-网络轮询器

https://zhuanlan.zhihu.com/p/64138532

https://imageslr.github.io/2020/02/27/select-poll-epoll.html

http://singlecool.com/2020/12/13/golang-netpoll/

详解Go语言I/O多路复用netpoller模型的更多相关文章

  1. 详解Go语言调度循环源码实现

    转载请声明出处哦~,本篇文章发布于luozhiyun的博客: https://www.luozhiyun.com/archives/448 本文使用的go的源码15.7 概述 提到"调度&q ...

  2. 详解go语言的array和slice 【二】

    上一篇已经讲解过,array和slice的一些基本用法,使用array和slice时需要注意的地方,特别是slice需要注意的地方比较多.上一篇的最后讲解到创建新的slice时使用第三个索引来限制sl ...

  3. 详解 Go 语言中的 time.Duration 类型

    swardsman详解 Go 语言中的 time.Duration 类型swardsman · 2018-03-17 23:10:54 · 5448 次点击 · 预计阅读时间 5 分钟 · 31分钟之 ...

  4. Rserve详解,R语言客户端RSclient【转】

    R语言服务器程序 Rserve详解 http://blog.fens.me/r-rserve-server/ Rserve的R语言客户端RSclient https://blog.csdn.net/u ...

  5. (转)详解LVS负载均衡之三种工作模型原理和10种调度算法

    原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://linuxnx.blog.51cto.com/6676498/1195379 LV ...

  6. 详解C语言的类型转换

    1.自动类型转换 字符型变量的值实质上是一个8位的整数值,因此取值范围一般是-128-127,char型变量也可以加修饰符unsigned,则unsigned char 型变量的取值范围是0-255( ...

  7. for循环使用详解(c语言版)

    说明:c语言的很多语法部分都和JAVA,AS等相同 特别是for的用法. c语言中的for循环语句使用最为灵活,不仅可以用于循环次数已经确定的情况,而且可以用于循环次数不确定而只给出循环结束条件的情况 ...

  8. 详解C语言的main函数

    如图所示:#include<stdio.h>这是一个头文件,包含的是C程序运行的C语言的库函数,只有包含了相关的头文件,在程序中才能调用.stdio表示输入输出控制.printf():就是 ...

  9. malloc函数详解 C语言逻辑运算符

    今天写线性表的实现,又遇到了很多的难题,C语言的指针真的没学扎实.很多基础都忘了. 一是 :malloc 函数的使用. 二是:C语言逻辑运算符. 一.原型:extern void *malloc(un ...

随机推荐

  1. 【Linux】history用法

    通过history命令可以查看我们在系统中输入过的命令 history命令的一些常用参数 -c  清空内存中命令历史 -d #  删除指定的历史命令,比如 history -d 100 ,就是删除第1 ...

  2. SDUST数据结构 - chap2 线性表

    一.判断题: 二.选择题: 三.编程题: 7-1 jmu-ds-顺序表区间元素删除 : 输入样例: 10 5 1 9 10 67 12 8 33 6 2 3 10 输出样例: 1 67 12 33 2 ...

  3. oracle RAC和RACOneNode之间的转换

    Convert RAC TO RACOneNode 1.查看资源状态 [grid@rac01 ~]$ crsctl status res -t 从这里看到,数据库的名字叫racdb 2.查看实例 [o ...

  4. Table controls and tabstrip controls

    本文转载自http://www.cnblogs.com/clsoho/archive/2010/01/21/1653268.html ONTROLS Syntax Forms Declaration ...

  5. [Usaco2016 Dec]Counting Haybales

    原题链接https://www.lydsy.com/JudgeOnline/problem.php?id=4747 先将原数组排序,然后二分查找即可.时间复杂度\(O((N+Q)logN)\). #i ...

  6. STL_map和multimap容器

    一.map/multimap的简介 map是标准的关联式容器,一个map是一个键值对序列,即(key,value)对.它提供基于key的快速检索能力. map中key值是唯一的.集合中的元素按一定的顺 ...

  7. 从ReentrantLock源码入手看锁的实现

    写这篇确实挺伤脑筋的,是按部就班一行一行读,但是我想这么写估计很多没有接触过的可能就劝退了,很容易出现的一种现象就是看了后面忘了前面,而且很容易看了一行代码就一层层往下钻,这样不仅容易打击看源码的积极 ...

  8. JAVA SSM整合流程以及注意点

    1.搭建整合环境 整合说明:SSM整合可以使用多种方式,咱们会选择XML + 注解的方式 先搭建整合的环境 先把Spring的配置搭建完成 再使用Spring整合SpringMVC框架 最后使用Spr ...

  9. HA工作机制及namenode向QJM写数据流程

    HA工作机制 (配置HA高可用传送门:https://www.cnblogs.com/zhqin/p/11904317.html) HA:高可用(7*24小时不中断服务) 主要的HA是针对集群的mas ...

  10. 类型检查和鸭子类型 Duck typing in computer programming is an application of the duck test 鸭子测试 鸭子类型 指示编译器将类的类型检查安排在运行时而不是编译时 type checking can be specified to occur at run time rather than compile time.

    Go所提供的面向对象功能十分简洁,但却兼具了类型检查和鸭子类型两者的有点,这是何等优秀的设计啊! Duck typing in computer programming is an applicati ...