Redis6:第十二篇-多路复用相关问题

  • Redis的多路复用
    • 什么是IO多路复用
      • 文本事件
    • 同步异步阻塞非阻塞
      • 同步
      • 异步
      • 阻塞
      • 非阻塞
      • 四种组合方式
    • Unix操作系统底层的五种最重要的IO模型
      • Blocking IO
      • NoneBlocking IO
      • IO multiplexing IO
      • signal driven IO
      • asynchronous IO
    • IO多路复用三大函数
      • select方法
      • poll方法
      • epoll方法

本篇文章是来复习Redis的多路复用以及相关知识

Redis的多路复用

什么是IO多路复用

Redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,一次性放到文本事件分派器,事件分派器把事件分给事件处理器

在上图中:用户有三个socket连接,一个是建立连接的socket server,一个设置key的连接,一个是获取key的连接,这三个连接去请求Redis,会先请求到Redis内核里的IO多路复用器(epoll),然后由此放入到Redis事件队列中,然后由事件分派器分派给各个对应处理器。

1.这个IO多路复用把各个事件请求放入到同一个队列中也就解释了Redis为什么是单线程的.
2.IO多路复用(epoll)和事件分派器也就组成了一个由多事件到单处理的转换器。

IO多路复用是一种思想,天上飞的理念必然有落地的实现,所以Linux系统内核提供了一种底层的函数select-poll-epoll来实现Redis的单线程的模式,多IO的访问。

Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的, 但是由于读写操作等待用户输入或输出都是阻塞的,所以I/O操作在一般 情况下往往不能直接返回,这会导致某一文件的1/O阻塞导致整个进程无法对其它客户提供服务,而I/O 多路复用就是为了解决这个问题而出现

所谓 I/O 多路复用机制,就是说通过一种机制,可以监视多个描达符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。这种机制的使用需要select、poll、epoll来配合。多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。
Redis服务采用Reactor的方式来实现文件事件处理器(每一个网络连接其实都对应一个文件描述符)
Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为为文件事件处理器。
它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。
因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型

那么多的理念可能会让你觉得晦涩难懂,那么接下来我会尽量对里面的一些理念进行解析。

文本事件

Redis基于Reactor模式开发了自己的网络事件处理器,这个处理器叫做文本事件处理器(file event handle)。

那么在此来归纳一下IO多路复用
IO:网路IO
多路:多个客户端连接(连接解释套接字描述符,即socket和channel)
复用:复用一个或多个线程,也就是说,一个或一组线程处理多个TCP连接,使用单进程就能实现同时多个客户端的连接
一句话:一个服务端进程可以同时处理多个套接字描述符,其发展可以分为select–poll–epoll三个阶段来描述。

同步异步阻塞非阻塞

我们再来看上面这张图片,事件派发器可以根据事件的类型来分配不同的处理器,但本身并不做请求,这个事件派发器像不像nginx。
其实Redis和nginx底层的牛逼之处就在于epoll操作系统内核函数。

同步

调用者一直等待调用的结果,不出结果不会返回。

异步

被调用方先返回应答,让调用者先回去,然后再计算调用结果,计算完成后就通知并返回给调用者。

同步和异步讨论的是被调用者(服务提供者),重要的是在于获取结果的消息通知方式。

阻塞

调用者一直等待返回结果,当前线程会挂起,什么都不做

非阻塞

调用的请求被接受后不会阻塞该线程,而会立即返回。

阻塞非阻塞的讨论对在于调用者(服务请求者),重点在于等待消息的时候的行为,调用者是否能够做其他的事情。

四种组合方式

1.同步阻塞
2.同步非阻塞
3.异步阻塞
4.异步非阻塞

同步阻塞:服务员–服务提供者说快到你了,先别离开我后台看一眼马上通知你。客户–服务请求者在海底捞火锅前台干等着,啥都不干。
同步非阻塞:服务员说快到你了,先别离开客户在海底捞火锅前台边刷抖音边等着叫号
异步阻塞:服务员说还要再等等你先去逛逛,一会儿通知你。客户怕过号在海底捞火锅前拿着排号小票啥都不干,一直等着店员通知
异步非阻塞:服务员说还要再等等,你先去逛逛,一会儿通知你。客户拿着排号小票刷着抖音,等着店员通知

Unix操作系统底层的五种最重要的IO模型

在Redis中最重要的三个IO模型就是BIO,NIO,IO multiplexing。
本篇以这三个IO模型进行展开。

Blocking IO

阻塞IO:
模型图

recvfrom():就是一个从(已连接)套接口上接收数据,并捕获数据发送源的地址的函数

当用户进程调用了recvfrom这个系统调用, kernel(linux内核)就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。所以,BIO的特点就是在IO执行的两个阶段都被block了。

那么既然阻塞是阻塞在哪里了?我们看下面的代码:

ps:因为Jedis的底层使用的就是Socket那么我就在此使用Socket来进行实验。

public class RedisServer
{public static void main(String[] args) throws IOException{byte[] bytes = new byte[1024];ServerSocket serverSocket = new ServerSocket(6379);while(true){System.out.println("模拟RedisServer启动-----111 等待连接");Socket socket = serverSocket.accept();System.out.println("-----222 成功连接");System.out.println();}}
}
public class RedisClient01
{public static void main(String[] args) throws IOException{System.out.println("------RedisClient01 start");Socket socket = new Socket("127.0.0.1", 6379);}
}

运行结果:

模拟RedisServer启动-----111 等待连接
-----222 成功连接模拟RedisServer启动-----111 等待连接
-----222 成功连接模拟RedisServer启动-----111 等待连接

可以看到Block到了serverSocket.accept()
下面写一下BIO的程序

public class RedisServerBIO
{public static void main(String[] args) throws IOException{ServerSocket serverSocket = new ServerSocket(6379);while(true){System.out.println("-----111 等待连接");Socket socket = serverSocket.accept();//阻塞1 ,等待客户端连接System.out.println("-----222 成功连接");InputStream inputStream = socket.getInputStream();int length = -1;byte[] bytes = new byte[1024];System.out.println("-----333 等待读取");while((length = inputStream.read(bytes)) != -1)//阻塞2 ,等待客户端发送数据{System.out.println("-----444 成功读取"+new String(bytes,0,length));System.out.println("====================");System.out.println();}inputStream.close();socket.close();}}
}
public class RedisClient01
{public static void main(String[] args) throws IOException{Socket socket = new Socket("127.0.0.1",6379);OutputStream outputStream = socket.getOutputStream();//socket.getOutputStream().write("RedisClient01".getBytes());while(true){Scanner scanner = new Scanner(System.in);String string = scanner.next();if (string.equalsIgnoreCase("quit")) {break;}socket.getOutputStream().write(string.getBytes());System.out.println("------input quit keyword to finish......");}outputStream.close();socket.close();}
}

在这个程序里,当客户端一连接的时候客户端二不能连接,发生阻塞。
当我们把这个程旭改写为多线程版本的时候:

public class RedisServerBIOMultiThread
{public static void main(String[] args) throws IOException{ServerSocket serverSocket = new ServerSocket(6379);while(true){//System.out.println("-----111 等待连接");Socket socket = serverSocket.accept();//阻塞1 ,等待客户端连接//System.out.println("-----222 成功连接");new Thread(() -> {try {InputStream inputStream = socket.getInputStream();int length = -1;byte[] bytes = new byte[1024];System.out.println("-----333 等待读取");while((length = inputStream.read(bytes)) != -1)//阻塞2 ,等待客户端发送数据{System.out.println("-----444 成功读取"+new String(bytes,0,length));System.out.println("====================");System.out.println();}inputStream.close();socket.close();} catch (IOException e) {e.printStackTrace();}},Thread.currentThread().getName()).start();System.out.println(Thread.currentThread().getName());}}
}

这样就完成了BIO的多线程版,每一次获得一个socket连接就会新开启一个线程所以并没有发生阻塞。
那么上面的代码存在什么问题呢?
虽然不会再发生阻塞了但是每来一个socket连接就会新创建一个用户线程,那么如果IO量巨大的时候就会创建大量的用户线程那么就会消耗大量的资源。而且这些线程都是由用户态创建的肯定会发生大量的用户态和内核态的上下文切换,所以我们不应该由用户态来创建线程,应该由内核态来创建线程。
现在知道了问题所在那么应该怎么去解决呢?
两种办法
1.引入线程池,但在用户量比较大的情况下,我们并不知道线程池到底有多大,太大了内存吃不消,这种办法也不可行。
2.使用NIO的方式
如果我们想要accept()和read()都不阻塞,这就引入了NIO模型。

另外在Tomcat7之前就是利用BIO来解决多连接

NoneBlocking IO

非阻塞IO

在NIO模式中,一切都是非阻塞的:
accept()方法是非阻塞的,如果没有客户端连接,就返回error(不是停止程序并报错,而是返回一个类似于错误码)
read()方法是非阻塞的,如果read()方法读取不到数据就返回error,如果读取到数据时只阻塞read()方法读数据的时间
在NIO模式中,只有一个线程:当一个客户端与服务端进行连接,这个socket就会加入到一个数组中,隔一段时间遍历一次,看这个socket的read()方法能否读到数据,这样一个线程就能处理多个客户端的连接和读取了

当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error从用户进程角度讲,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。所以,NIO特点是用户进程需要不断的主动询间内核数据准备好了吗,所以说,NIO类似于CAS操作,BIO类似于synchronized.
由于,BIO是从java1.1开始的socket()方法,之后再改进的话原来的代码不太容易改动(ServerSocket),所以又开发了一套API(ServerSocketChannel)
下面的代码就是NIO的使用ServerSocketChannel

public class RedisServerNIO
{static ArrayList<SocketChannel> socketList = new ArrayList<>();static ByteBuffer byteBuffer = ByteBuffer.allocate(1024);public static void main(String[] args) throws IOException{System.out.println("---------RedisServerNIO 启动等待中......");ServerSocketChannel serverSocket = ServerSocketChannel.open();serverSocket.bind(new InetSocketAddress("127.0.0.1",6379));serverSocket.configureBlocking(false);//设置为非阻塞模式while (true){for (SocketChannel element : socketList){int read = element.read(byteBuffer);if(read > 0){System.out.println("-----读取数据: "+read);byteBuffer.flip();byte[] bytes = new byte[read];byteBuffer.get(bytes);System.out.println(new String(bytes));byteBuffer.clear();}}SocketChannel socketChannel = serverSocket.accept();if(socketChannel != null){System.out.println("-----成功连接: ");socketChannel.configureBlocking(false);//设置为非阻塞模式socketList.add(socketChannel);System.out.println("-----socketList size: "+socketList.size());}}}
}

这段代码就是不停的去轮询集合里面的read(),如果返回码大于0说明读取到了内容,如果accept()里面加入了连接,就把它设置为非阻塞模式,并把他放入到List里面,这样accept()和read()都不会阻塞了。

那么NIO里面存在什么问题呢?
NIO成功的解决了BIO需要开启多线程的问题,NIO中一个线程就能解决多个socket,但是还存在2个问题。
问题一:
这个模型在客户端少的时候十分好用,但是客户端如果很多,
比如有1万个客户端进行连接,那么每次循环就要遍历1万个socket.如果一万个socket中只有10个socket有数据,也会遍历一万个socket,就会做很多无用功,每次遍历遇到read返回-1时仍然是一次浪费资源的系统调用。
问题二:
而且这个遍历过程是在用户态进行的,用户态判断socket是否有数据还是调用内核的read()方法实现的,这就涉及到用户态和内核态的切换,每遍历一个就要切换一次,开销很大因为这些问题的存在。
优点:不会阻塞在内核的等待数据过程,每次发起的I/O请求可以立即返回,不用阻塞等待,实时性较好。
缺点:轮询将会不断地询问内核,这将占用大量的CPU时间,系统资源利用率较低,所以一般Web服务器不使用这种1/O模型。
结论:让Linux内核搞定上述需求,我们将一批文件描述符通过一次系统调用传给内核由内核层去遍历,才能真正解决这个问题。IO多路复用应运而生,也即将上述工作直接放进Linux内核,不再两态转换而是直接从内核获得结果,因为内核是非阻塞的。

IO multiplexing IO

O多路复用
也叫
事件驱动型IO

NIO全量遍历,而IO多路复用就是等待socket变成可读,他是一次系统调用+内核层面的遍历这些描述符,解决了NIO的空转问题,也解决了用户态和内核态的切换,节约系统资源。

IO multiplexing就是我们说的select. poll, epoll.
有些地方也称这种IO方式为event driven IO事件驱动IO。
就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
可以基于一个阻塞对象,同时在多个描述符上等待就绪,而不是使用多个线程(每个文件描述符一个线程,每次new一个线程),这样可以大大节省系统资源。
所以, I/O多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态, Select()函数就可以返回。


还记得我上面说过Redis和Nginx很像,nginx也使用epoll来接受请求,nginx会有很多连接进来,epoll会把他们监视起来,然后像拨开关一样,谁有数据就拨向谁,然后调用相应的代码处理。

文件描述符(FileDescriptor)
实际上是一个索引值,官方的话太过于冗长,用人话说就是适用于Unix,Linux系统的一个句柄,引用或者叫连接。

接下来我们简单举个例子回顾一下以上三个模型:
模拟一个tcp服务器处理30个客户socket.
假设你是一个监考老师,让30个学生解答一道竞赛考题,然后负责验收学生答卷,你有下面几个选择:
第一种选择(BIO):按顺序逐个验收,先验收A.然后是B.之后是C、D…。这中间如果有一个学生卡住,全班都会被耽误,你用循环挨个处理
socket,根本不具有并发能力。
第二种选择(NIO):你创建30个分身线程,每个分身线程检查一个学生的答案是否正确。这种类似于为每一个用户创建一个进程或者线程处理连接。
第三种选择(IO多路复用),你站在讲台上等,谁解答完谁举手。这时C、D举手,表示他们解答问题完毕,你下去依次检查C、D的答案,然后继续回到讲台上
等。此时E、A又举手,然后去处理E和A…这种就是IO复用模型。Linux下的select、poll和epoll就是干这个的。

Reator反应模式:也叫Dispather模式,就是IO多路复用统一监听事件,收到事件之后就分发给某进程,是编写高性能网络服务器的必备技术。

signal driven IO

信号驱动IO

这两种IO模型不是Redis的主要模型以后了解之后再补上。

asynchronous IO

异步IO

IO多路复用三大函数

select,poll,epoll都是IO多路复用的具体实现
在上面我写了IO多路复用会一次系统调用然后写入内核,遍历内核描述符,那么它是怎么写入内核的呢,就是用到了这三个函数。

select方法

这是连接linux内核上的一个函数,在1983年实现的。
分析select函数的执行流程:
1.select是一个阻塞函数,当没有数据时,会一直阻塞在sel那一行。
2.当有数据时会将rset中对应的那一位置为1
3. select函数返回,不再阻塞
4.遍历文件描述符数组,判断哪个fd被置位了
5.读取数据,然后处理

select就相当于把NIO循环遍历socket的连接那一个循环由用户态封装到了linux的内核里面。避免了用户态到内核态的转换,节约开销。


这个bitmap就是用来存描述符(socket)的地方,只要与操作就可以得到哪一位上有数据。

其实select()函数也是有一些缺点
select函数的缺点
1.bitmap默认大小为1024,虽然可以调整但还是有限度的
2. rset每次循环都必须重新置位为0,不可重复使用
3.尽管将rset从用户态拷贝到内核态由内核态判断是否有数据,但是还是有拷贝的开销
4.当有数据时select就会返回,但是select函数并不知道哪个文件描述符有数据了,后面还需要再次对文件描述符数组进行遍历。效率比较低

poll方法

redis调用linux的函数
poll的执行流程:
1.将五个fd从用户态拷贝到内核态
2. poll为阻塞方法,执行poll方法,如果有数据会将fd对应的revents置 为POLLIN
3. poll方法返回
4.循环遍历,查找哪个fd被置位为POLLIN了
5.将revents重置为0便于复用
6.对置位的fd进行读取和处理



优点:1. poll使用pollfd数组来代替select中的bitmap.数组没有1024的限制,可以一次管理更多的client。它和select的主要区别就是,去掉了select只能监听1024个文件描述符的限制。
2、当pollfds数组中有事件发生,相应的revents置位为1,遍历的时候又置位回零,实现了pollfd数组的重用
缺点:1. 本质还是拷贝到内核态,依然有开销
2.循环数组依然事件复杂度为o(n)

epoll方法

2002年被Davide Libenzi发明
查看:linux下命令:man epoll

可以看到epoll有三个函数:
1.epoll_creat :创建一个epoll函数的句柄 int epoll_create(int size)
size不是限制epoll能监听的描述符的最大个数,知识对内核的出事的分配的内部数据结构的一个建议
2.epoll_ctl :向内核添加,修改或删除要监控的文件描述符
3.epoll_wait:发起了类似select()的调用。
等待epfb上面的IO事件最多返回maxevents个事件。参数events用来从内核上得到事件的集合,maxevents告知这个内核events有多大

总结
多路复用快的原因在于,操作系统提供了这样的系统调用,使得原来的while循环里多次系统调用,
变成了一次系统调用+内核层遍历这些文件描述符。
epoll是现在最先进的IO多路复用器, Redis.Nginx. linux中的Java NIO都使用的是epoll.
这里“多路”指的是多个网络连接, “复用”指的是复用同一个线程。
1、一个socket的生命周期中只有一次从用户态拷贝到内核态的过程,开销小
2、使用event事件通知机制,每次socket中有数据会主动通知内核,并加入到就绪链表中,不需要遍历所有的socket

在多路复用IO模型中,会有一个内核线程不断地去轮询多个socket的状态,只有当真正读写事件发送时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有真正有读写事件进行时,才会使用IO资源,所以它大大减少来资源占用。多路I/O复用模型是利用select、poll, epoll可以同时监察多个流的I/O事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有1/O事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll是只轮询那些真正发出了事件的流) ,并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。采用多路I/O复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈
在epoll函数里面就没有用户态到内核态的拷贝了,直接是用户态和内核态的公用,这个我以后再展开。
对于nginx和redis要发挥性能,建议用Linux,不要再用windows系统
为啥?就是因为再windows底层使用的还是select函数,而linux用的epoll函数,时间复杂度为o(1)。
如果都找不到就用select函数,时间复杂度为o(n),这也是为什么有了epoll还要保有前面两个函数。

Redis底层多路复用相关推荐

  1. 对于redis底层框架的理解(一)

    近期学习了redis底层框架,好多东西之前都没听说过,算是大开眼界了. 先梳理下redis正常的通讯流程吧 首先服务器启动都有主函数main,这个main函数就在redis.c里 首先是initser ...

  2. redis的多路复用是什么鬼

    有没有人和我一样, 自打知道了redis, 就一直听说什么redis单线程, 使用了多路复用等等. 天真的我以为多路复用是redis实现的技术. 今天才发现, 我被自己骗了, 多路复用是系统来实现的. ...

  3. 保存到redis的字符串类型出现斜杆_深入浅出Redis:这次从Redis底层数据结构开始...

    1.概述 相信使用过Redis 的各位同学都很清楚,Redis 是一个基于键值对(key-value)的分布式存储系统,与Memcached类似,却优于Memcached的一个高性能的key-valu ...

  4. redis底层数据结构之跳跃表

    redis底层数据结构之跳跃表 redis 的zset有序连表为啥选择用跳跃表? 我们要思考一问题,首先多问问自己为什么,才容易理解它,ps:这是个人观点.首先我们选择的数据结构和算法原因有以下几种: ...

  5. Redis底层原理和数据结构-总结篇

    本文数据结构部分内容转自:SmartKeyerror 先了解数据结构,后看具体应用部分,本文着重应用部分的优缺点进行总结!数据结构部分仅仅做初步了解介绍,源码实现具体请参考如下目录自行学习,本文以6. ...

  6. Redis的多路复用机制

    Redis是单线程还是多线程? 通常我们所说的Redis 是单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程.但 Red ...

  7. redis的多路复用原理

    redis服务端对于命令的处理是单线程的,但是在I/O层面却可以同时面对多个客户端并发的提供服务,并发到内部单线程的转化通过多路复用框架实现 一个IO操作的完整流程是数据请求先从用户态到内核态,也就是 ...

  8. 02 Redis 底层数据结构

    一.不同数据类型存储结构 Redis底层数据结构一共有 6 种,分别是简单动态字符串.双向链表.压缩列表.哈希表.跳表和整数数组.它们和数据类型的对应关系如下图所示: 1 数组与链表的区别 数组和链表 ...

  9. Redis底层数据结构详解(一)

    Redis底层数据结构 一.简单动态字符串SDS 1. SDS 2. 为什么Redis没用C语言原生字符串? 2.1 C语言中的字符串 2.2 使用SDS的好处 二.链表linkedlist 三.压缩 ...

最新文章

  1. CodeForces - 813C The Tag Game(拉格朗日乘数法,限制条件求最值)
  2. 明明安装了模块,还是出现 错误 ImportError: No module named ‘pandas‘ 原因LINUX上安装了多个python环境,将脚本中python 改为python3问题解
  3. 使用SharePoint 2007 Web Service上传文件到文档库
  4. 【数学基础】运筹学:拉格朗日乘子法和KKT条件(上)
  5. Boost:bimap双图property地图的测试程序
  6. 好玩的表情包机器人小程序源码_支持直接搜索仿聊天界面获取源码
  7. GCC4.8对new和delete的参数匹配新要求
  8. 分布式理论和分布式一致性协议
  9. 淘宝直通车怎样设置定向推广出价问题总结
  10. 1188: 选票统计(一)(结构体专题)
  11. Python 京东抢购茅台脚本(亲测可用),github脚本24小时内删除
  12. ValueError: multiclass format is not supported
  13. canvas 对图片进行涂抹,涂抹区域保存图片存入本地
  14. Spring系列之一:Spring入门
  15. 化妆品护肤DiY的广告界面 简单的jquery 图片无缝滚动
  16. ASFF的TensorFlow2实现
  17. ffmpeg 多张图片合成h264编码格式的视频 按照指定时间截取 并添加 acc编码格式音乐 IOS可播放
  18. 发国外邮件用什么企业邮箱?在国外用什么邮箱稳定呢?
  19. 五、C语言指针和数组
  20. 求1-1/2+1/3-1/4+……+1/99-1/100 的值

热门文章

  1. 京东前端二面高频面试题
  2. 高通发布802.11ax AP/客户端;美国运营商推出无限流量套餐 | IoT黑板报
  3. systemd实现挂载服务,让你的网站不间断运行。
  4. SpringCloud基本使用教程(一)
  5. xiunobbs 4 mysql_灰狼boss
  6. CentOS查看版本号
  7. 查看服务器内存和硬盘命令,命令查看Linux服务器内存、CPU、显卡、硬盘使用情况...
  8. mysql支不支持fulljoin_MySQL5中是否可以进行完整外连接查询(full join)?解决方法
  9. 线程的创建及多线程之间的交互方式
  10. Day45---Super