三:Redis为什么使用单线程架构

三:Redis为什么使用单线程架构

Redis使用单线程的原因是因为相比多线程速度比较快。速度快体现在两点:

  1. 访问内存的时间小于线程上下文切换的开销。
  2. 多路IO复用,epoll模型速度快。

1.访问内存的时间小于线程上下文切换的开销

从第一篇中我们知道内存的速度大概是100ns,而一次线程上下文切换大概1500ns。线程上下文切换的时间是一次内存访问的15倍,所以Redis使用多线程是得不偿失的。并且多线程相比单线程实现起来考虑到线程安全,需要的数据结构会比较复杂,
单线程可以简化Redis的实现。

那什么时候使用多线程呢?当操作使用的存储慢速的时候,例如使用磁盘存储时,使用多线程时有优势的。我们知道CPU相比磁盘的速度是非常快的,所有计算存储体系中才会有,三级缓存,内存,磁盘。由于磁盘的访问速度慢,如果每写一点数据就直接存储,那么操作的大多数时间都耗费在操作磁盘上了,那么有没有办法提高速度呢?

计算的大多数问题,增加一个中间层就能很好的解决问题。

如果总共写入1M数据,每1Kb数据都写入磁盘,那么大多数时间都耗费在磁盘的寻址,多次重复的写耗费的时间,如果我添加一个Buffer,当Buffer快满时,我再统一写入磁盘,那么我只需要一次寻址,一次写操作。没错java里的IO就是这样干的~

	每次写1KB数据:
		时间 = 1024次寻址 + 1024次写时间
	使用buffer:	
		时间 = 1次寻址 + 1次写时间

2.多路IO复用,epoll模型速度快

IO多路复用是用来解决对多个I/O监听的。我们先来整体看下Linux系统中的5大IO模型。

  • IO阻塞模型
  • IO非阻塞模型
  • IO多路复用模型
  • 信号驱动式IO模型
  • 异步IO模型

当进行一次网络IO(假设为读操作),会涉及两个系统对象:1.这个IO的进程/线程。2.系统内核(kernel)。它会经历两个阶段:

  1. 等待数据准备。
  2. 数据准备完成,将数据从内核拷贝到内存。

上面的IO模型的区别就是在这两个阶段有所不同。

IO阻塞模型

当前IO发起recvfrom系统调用,内核开始第一个阶段准备数据,此时IO线程会阻塞一直等待内核准备完数据,
当内核准备好数据,将数据复制到内存,此时IO线程解除阻塞,从内存读取数据。
从上面的过程中我们看到了,在两个阶段都是阻塞的。

IO非阻塞模型

当前IO发去recvfrom系统调用,内核开始第一个阶段的数据准备,此时IO线程并不会被内核阻塞而是直接返回
一个Error,当IO线程判断结果是Error时,再次发送请求给内核,知道内核数据准备好,复制到内存,此时IO线程
再去操作数据。所以IO非阻塞模型下,IO线程需要不断的主动询问内核数据是否准备好。

多路IO复用模型

IO非阻塞下,我们可以看到IO线程一直做检查,为什么不检查多个IO请求呢?于是多路IO复用诞生。
多个IO线程注册到选择器上,选择器一直轮询,判断其中是否有IO线程数据准备好,当任一线程数据准备
好则开始处理数据。

Linux的IO多路复用模型有三种实现:

  1. select实现
    无差别轮询,单个进程能够监视的文件描述符数量存在最大限制,一般是1024
  2. poll实现
    相比select,由于使用链表存储,文件描述符的数量没有限制
  3. epoll实现
    效率比select/pollh高很多,使用红黑树存储,查增改的效率比数组和链表高很多。

如果我们要实现100万的并发连接,select每一个进程支持1024个连接,那么我们需要开辟1k个进程。
poll,我们需要一个进程可以,但是每扫描一次的时间时O(n)= O(100万)。
epoll事件驱动,epoll_wait()系统调用,通过此调用收集在epoll监控中已经发生的事件,时间是O(1)。

信号驱动式IO模型

当IO线程发起调用,会向内核注册一个信号处理函数,然后线程返回不阻塞;当内核数据准备好后,发送一个信号给IO线程,此时IO线程调用recvfrom系统调用开始读取数据进行处理。

异步IO模型

IO线程告知内核启动某个操作,并让内核在整个操作完成后通知应用程序。与信号量驱动IO的区别时,
信号量是内核通知线程何时启动一个IO操作(recvfrom调用),异步IO则是有内核通知线程操作何时完成。

IO多路复用模型epoll实现

使用epoll的过程是三个步骤

  1. 调用epoll_create()创建一个epoll对象。
  2. 调用epoll_ctl向epoll对象中添加连接的socket
  3. epoll_wait收集发生的事件的连接。
	int s = socket(AP_INET, SOCKET_STREAM, 0);
	bind(s,...)
	listen(s,...)
	//创建epoll对象
	int epfd = epoll_create(...);
	//将所有需要监听的socket添加到epfd中
	epoll_ctl(epfd,...);
	whiie(1){
		//如果没有就绪的socket(读取到数据)阻塞在这里
		int n = epoll_wait(...)
		for(接受到数据的socket) {
			//处理数据
		}
	}
	struct eventpoll{
		.....	
		//红黑树的根节点(每一个节点是Epitem),整个树存储所有的监听的socket,每个节点存储一个socket
		struct rb_root rbr;
		//双向链表存储就绪列表,并非直接引用socket,而是通过Epitem间接引用
		struct list_head rdlist;	
		.....
	}
	struct epitem {
		rbn;
		rdlink;
		next;
		ffd;
		nwait;
		pwqlist;
		ep;
		fllink;	
		event;
	}

现在我们来梳理下epoll的流程:

  1. 进程调用epoll_create,内核创建eventpoll对象,这个对象中主要包含rbr(红黑树)所有监视的socket,
    rdlist(双向链表)
  2. socket1,socket2,socket3被监听,注册(添加)到eventpoll对象的rbr监视树上。
  3. 此时socket1接收到数据,中断程序会给eventpoll的就绪列表rdlist添加socket1的引用
  4. 当执行到epoll_wait,检测到rdlist不为空,获取到socketk开始处理。

参考

https://segmentfault.com/a/1190000021163843?utm_source=tag-newest
https://blog.csdn.net/shenya1314/article/details/73691088
https://blog.csdn.net/historyasamirror/article/details/5778378
https://blog.csdn.net/tjiyu/article/details/52959418?utm_source=distribute.pc_relevant.none-task
https://blog.csdn.net/bird73/article/details/79792548
https://www.cnblogs.com/aspirant/p/9166944.html
https://blog.csdn.net/armlinuxww/article/details/92803381

发布了121 篇原创文章 · 获赞 56 · 访问量 167万+

猜你喜欢

转载自blog.csdn.net/u013565163/article/details/104398144