运营研发团队程序媛 张晶晶html
基于以上两个疑问,我进行了事件驱动模型的研究和分析mysql
先明确一点:事件驱动模型的本质是单线程的,由于想要同时处理多个请求,咱们须要换成事件模型的方式重构代码golang
bind() listen() while(1) { accept() //接收新链接 handle() //处理消息 }
当多个客户端请求时只能一个个处理,只要等到当前链接结束,才能处理下一个链接。这种方式处理效率低下,怎么提升处理效率呢,有两种方法:多线程处理,或者一个线程处理多个链接redis
bind() listen() while(1) { accept() pthread_create() }
这样就能够同时处理多个请求。可是这种方式有几个很差的地方,sql
对于第一个问题,在程序中进行速率的限制,防止客户端无限连接->线程池编程
除了线程池,还能够采用非阻塞式IO,经过fcntl设置套接字。这种方式只能经过不断的轮询来检查是否有请求数据到来。安全
操做系统应该是知道哪一个套接字是准备好了数据的,所以不必逐个扫描。服务器
想一想一个线程,有一堆的任务要处理,应该监视哪些东西呢,两种类型的套接字活动:数据结构
尽管这两种活动在本质上有所区别,咱们仍是要把它们放在一个循环里,由于只能有一个主循环。循环会包含 select 的调用。这个 select 的调用会监视上述的两种活动。多线程
while(1) { int nready = select(fdset_max + 1, &readfds, &writefds, NULL, NULL); // 获取事件的个数 //遍历fd for (fd = 0; fd <= fdset_max && nready > 0; fd++) { // Check if this fd became readable. if (FD_ISSET(fd, &readfds)) { //是否为读事件 nready--; if (fd == listener_sockfd) { //是否为请请求到来 fd_status_t status = on_peer_connected(newsockfd, &peer_addr, peer_addr_len); if (status.want_read) { //是否有读事件 FD_SET(newsockfd, &readfds_master); } else { FD_CLR(newsockfd, &readfds_master); } if (status.want_write) { //是否有写事件 FD_SET(newsockfd, &writefds_master); } else { FD_CLR(newsockfd, &writefds_master); } } } else { fd_status_t status = on_peer_ready_recv(fd);//接收数据 if (status.want_read) { FD_SET(fd, &readfds_master); } else { FD_CLR(fd, &readfds_master); } if (status.want_write) { FD_SET(fd, &writefds_master); } else { FD_CLR(fd, &writefds_master); } } } if (FD_ISSET(fd, &writefds)){ //是否有写事件 // write()// 发送消息 ... } }
流程图的处理从图一变成了图二
select 方法会返回须要处理的事件的个数,而后遍历全部的fd去处理。
可是这种方法依然是有缺陷,第一既然知道了事件的个数,可不能够知道事件是发生在哪一个fd上。否则每次都须要遍历全部的fd,限制性能。
此外,FD_SETSIZE是一个编译期常数,在现在的操做系统中,它的值一般是 1024。它被硬编码在 glibc 的头文件里,而且不容易修改。它把 select 可以监视的文件描述符的数量限制在 1024 之内。曾有些人想要写出可以处理上万个并发访问的客户端请求的服务器,因此这个问题颇有现实意义。
epoll 高效的关键之处在于它与内核更好的协做。epoll_wait 用当前准备好的事件填满一个缓冲区。只有准备好的事件添加到了缓冲区,所以没有必要遍历客户端中当前全部 监视的文件描述符。这简化了查找就绪的描述符的过程,把空间复杂度从 select 中的 O(N) 变为了 O(1)。
while(1){ int nready = epoll_wait(epollfd, events, MAXFDS, -1); for ( int i = 0; i < nready; i++) { ... } }
要在 select 里面从新遍历,有明显的差别:若是在监视着 1000 个描述符,只有两个就绪, epoll_waits 返回的是 nready=2,而后修改 events 缓冲区最前面的两个元素,所以咱们只须要“遍历”两个描述符。用 select 咱们就须要遍历 1000 个描述符,找出哪一个是就绪的。所以,在繁忙的服务器上,有许多活跃的套接字时 epoll 比 select 更加容易扩展。
redis为何要实现本身的事件库?
Redis 并无使用 libuv,或者任何相似的事件库,而是它去实现本身的事件库 —— ae,用 ae 来封装 epoll、kqueue 和 select。事实上,Antirez(Redis 的建立者)刚好在 2011 年的一篇文章http://oldblog.antirez.com/po... 中回答了这个问题。他的回答的要点是:ae 只有大约 770 行他理解的很是透彻的代码;而 libuv 代码量很是巨大,也没有提供 Redis 所需的额外功能。
epoll 实现
https://www.cnblogs.com/appre... 这篇文章分析了epoll的实现
一颗红黑树,一张准备就绪fd链表,少许的内核cache,就帮咱们解决了大并发下的fd(socket)处理问题。
又来一个问题,为何epoll选择红黑树问不是其它avl树,mysql 选择b+树
为何不用 AVL 树做为底层实现, 那是由于 AVL 树是高度平衡的树, 而每一次对树的修改, 都要 rebalance, 这里的开销会比红黑树大. 红黑树插入只要两次旋转, 删除至多三次旋转. 但不能否认的是, AVL 树搜索的效率是很是稳定的. 选取红黑树, 是一种折中的方案
B+树是为磁盘或其余直接存取的辅助存储设备而设计的一种数据结构。mysql为何选取B+树,本质上是由于mysql数据是存放在外部存储的
由于协程的高效,在go中处理多客户端请求,只须要以下这样写便可
for{ accept() go handle() }
是否须要epoll的编程模型呢,在一篇帖子中写到
go中怎么找不到像epoll或者iocp这种编程模型
答案很简单:goroutine底层用的非阻塞+epoll,因此你能够用同步的方式写出异
步的程序。
连接:https://grokbase.com/p/gg/gol...
验证须要查看goroutine的底层实现。(大几率是正确的,这就是go语言的优点所在)