服務器的兩種併發原理

衆所周知,如今的服務器能夠處理多個socket鏈接,背後併發的實現主要有兩種途徑。php

  1. 多線程同步阻塞
  2. I/O多路複用

socket的創建

聊到socket,就不得不提到socket的創建的流程。祭出經典的老圖:
jpgnode

服務器依次使用socket,bind,listen以後就會監聽對應的地址,此時accept會一直阻塞直到有鏈接創建,若是客戶端和服務器創建了鏈接,那麼accept就會返回一個鏈接句柄,能夠對鏈接進行讀數據或者寫數據。nginx

同步阻塞

那麼問題來了,服務器若是不作特殊處理的話,一次只能處理一個鏈接,新的鏈接來是須要等待上一個鏈接結束才能鏈接成功,這就是最開始的服務器同步阻塞方法。redis

同步阻塞:進程發起IO系統調用後,進程被阻塞,轉到內核空間處理,整個IO處理完畢後返回進程。操做成功則進程獲取到數據。segmentfault

多線程併發

可能之前的擁有的電腦人很少,這種方式一次只能鏈接一個倒也沒有問題,以後訪問的人開始多起來,設計者以爲這樣下去不行,就設計了多線程同步阻塞的方法。每次accept得到一個句柄,就建立一個線程去處理鏈接,這下子就能同時處理多個鏈接呢。這就是經典的多線程同步阻塞的方法。服務器

典型的多線程(進程)併發模型就是cgi網絡

特色

服務器和客戶端之間的併發,有如下特色:多線程

  1. 外部鏈接不少,但不少鏈接是不活躍的鏈接,典型的如聊天的im系統。
  2. 少許的CPU消耗。
  3. 大部分的時間耗費在I/O阻塞和其餘網絡服務。
  4. 外部網絡不穩定,客戶端收發數據慢不少。
  5. 對業務的請求處理很快,大部分時候毫秒級就能夠完成。

問題

根據以上特色,咱們能夠得知服務器有如下問題:
若是採用多線程同步阻塞,1個tcp鏈接須要創建1個線程,10k個鏈接須要創建10k個鏈接,然而大部分鏈接是不活躍,即使是須要處理業務邏輯,也能夠快速返回結果,大部分時間也是處於I/O阻塞或網絡等待。這就使得多個線程的建立很耗費資源,且線程的切換也是極其耗費CPU,這就極可能致使了CPU處理業務的消耗的資源很少,可是卻花了不少資源在進程切換上面。併發

I/O多路複用

多線程併發的問題是大部分socket都是閒置的狀態或者是處於IO阻塞的狀態,那能不能把阻塞的socket先扔到一邊去處理其餘事情,來避免等待所帶來的資源耗費,也就是非阻塞IO的概念。異步

非阻塞IO

當用戶進程發出read操做時,若是kernel中的數據尚未準備好,那麼它並不會block用戶進程,而是馬上返回一個error。從用戶進程角度講 ,它發起一個read操做後,並不須要等待,而是立刻就獲得了一個結果。用戶進程判斷結果是一個error時,它就知道數據尚未準備好,因而它能夠再次發送read操做。一旦kernel中的數據準備好了,而且又再次收到了用戶進程的system call,那麼它立刻就將數據拷貝到了用戶內存,而後返回。

所以:使用非阻塞IO是須要不斷輪詢IO數據是否好了。

IO多路複用原理就是不斷輪詢多個socket,當其中的某個socket準備好了數據就返回,不然整個進程繼續阻塞,就可讓一個進程在不太耗費資源的狀況下處理多個鏈接,可是這個輪詢的操做是交給內核態去完成,也就避免了內核態和用戶態的切換的問題。

而目前的實現方法有select, poll, epoll,其中epoll的性能最好,用的也是最普遍。

優勢

  1. 避免了建立多個線程所耗費的資源以及時間。
  2. 對socket的輪詢是內核態的完成,不須要像多線程那樣切換須要耗費資源。

而epoll的實現能夠作到性能幾乎不受鏈接數(單單是鏈接而沒有其餘的操做)的影響。

固然多路複用IO也有本身的問題,也就是自己不支持多核的使用,須要另外解決多核的利用。

其中使用enroll的成熟程序有nginx,redis,nodej等。

服務器的發展

根據知乎大佬的介紹,服務器通過發展能夠分爲兩階段:

第一代服務器模型

把傳輸層的tcp併發的鏈接放到IO多路複用去處理,應用層繼續使用多線程併發模型去作。這樣就能夠大幅度減小線程的建立切換的資源耗費。
如:nginx + php-fpm(實際上是php-fpm是多進程)

第二代服務器模型

第二代服務器模型是把應用層也使用IO多路複用去處理,減小應用層的等待外部接口調用阻塞等待,通常是大廠大流量併發須要用到。
如:

  1. nodejs的異步回調
  2. Go的goroutine

參考資料:
[1]:許懷遠的知乎回答
[2]:Linux IO模式及 select、poll、epoll詳解

相關文章
相關標籤/搜索