【隨筆】異步編程淺析

運營研發團隊程序媛 張晶晶html

背景

  • 1.最近研究redis關於主從複製的功能實現,發現客戶端實時響應slaveof的命令後,把主從複製添加到epoll的時間事件中再進行操做。所以有疑問,redis是如何進行文件和時間事件的調度
  • 2.go的一大特色就是從語言方面支持協程,提供系統的併發性,那麼go語言中是否還須要epoll這種事件驅動模型

基於以上兩個疑問,我進行了事件驅動模型的研究和分析mysql

分析

先明確一點:事件驅動模型的本質是單線程的,由於想要同時處理多個請求,咱們須要換成事件模型的方式重構代碼golang

1.最簡單的模型是單線程

bind()
listen()
while(1) {
    accept() //接收新鏈接
    handle() //處理消息
}

當多個客戶端請求時只能一個個處理,只要等到當前鏈接結束,才能處理下一個鏈接。這種方式處理效率低下,怎麼提升處理效率呢,有兩種方法:多線程處理,或者一個線程處理多個鏈接redis

2.多線程

bind()
listen()
while(1) {
    accept()
    pthread_create()
}

這樣就能夠同時處理多個請求。可是這種方式有幾個很差的地方,sql

  • cpu和內存消耗,上下文切換
  • 線程安全問題
  • 問題定位難等

對於第一個問題,在程序中進行速率的限制,防止客戶端無限連接->線程池編程

3.事件驅動模型

除了線程池,還能夠採用非阻塞式IO,經過fcntl設置套接字。這種方式只能經過不斷的輪詢來檢查是否有請求數據到來。安全

操做系統應該是知道哪一個套接字是準備好了數據的,所以不必逐個掃描。服務器

select

想一想一個線程,有一堆的任務要處理,應該監視哪些東西呢,兩種類型的套接字活動:數據結構

  • 1.accept
  • 2.讀寫事件

儘管這兩種活動在本質上有所區別,咱們仍是要把它們放在一個循環裏,由於只能有一個主循環。循環會包含 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()// 發送消息
        ...
      }
 
 
}

流程圖的處理從圖一變成了圖二

clipboard.png

select 方法會返回須要處理的事件的個數,而後遍歷全部的fd去處理。

可是這種方法依然是有缺陷,第一既然知道了事件的個數,可不能夠知道事件是發生在哪一個fd上。否則每次都須要遍歷全部的fd,限制性能。

此外,FD_SETSIZE是一個編譯期常數,在現在的操做系統中,它的值一般是 1024。它被硬編碼在 glibc 的頭文件裏,而且不容易修改。它把 select 可以監視的文件描述符的數量限制在 1024 之內。曾有些人想要寫出可以處理上萬個併發訪問的客戶端請求的服務器,因此這個問題頗有現實意義。

epoll

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)處理問題。

  • 1.執行epoll_create時,建立了紅黑樹和就緒list鏈表。
  • 2.執行epoll_ctl時,若是增長fd(socket),則檢查在紅黑樹中是否存在,存在當即返回,不存在則添加到紅黑樹上,而後向內核註冊回調函數,用於當中斷事件來臨時向準備就緒list鏈表中插入數據。
  • 3.執行epoll_wait時馬上返回準備就緒鏈表裏的數據便可

又來一個問題,爲何epoll選擇紅黑樹問不是其它avl樹,mysql 選擇b+樹

爲何不用 AVL 樹做爲底層實現, 那是由於 AVL 樹是高度平衡的樹, 而每一次對樹的修改, 都要 rebalance, 這裏的開銷會比紅黑樹大. 紅黑樹插入只要兩次旋轉, 刪除至多三次旋轉. 但不能否認的是, AVL 樹搜索的效率是很是穩定的. 選取紅黑樹, 是一種折中的方案

B+樹是爲磁盤或其餘直接存取的輔助存儲設備而設計的一種數據結構。mysql爲何選取B+樹,本質上是由於mysql數據是存放在外部存儲的

go是否須要epoll

由於協程的高效,在go中處理多客戶端請求,只須要以下這樣寫便可

for{
    accept()
    go handle()
}

是否須要epoll的編程模型呢,在一篇帖子中寫到

go中怎麼找不到像epoll或者iocp這種編程模型
答案很簡單:goroutine底層用的非阻塞+epoll,因此你能夠用同步的方式寫出異
步的程序。

連接:https://grokbase.com/p/gg/gol...

驗證須要查看goroutine的底層實現。(大機率是正確的,這就是go語言的優點所在)

相關文章
相關標籤/搜索