併發總結


 

併發總結

併發

高併發(High Concurrency)是互聯網分佈式系統架構設計中必須考慮的因素之一,它通常是指,通過設計保證系統能夠同時並行處理很多請求。

高併發相關常用的一些指標有響應時間(Response Time),吞吐量(Throughput),每秒查詢QPS(Query Per Second),併發用戶數等。

併發的手段主要有:垂直擴展(Scale Up)與水平擴展(Scale Out)。垂直擴展可以通過提升單機硬件性能,或者提升單機架構性能,來提高併發性,但單機性能總是有極限的,因此,水平擴展也是必要的。

水平擴展的方法大致有:

  1. CDN,通過DNS的方式進行擴展,增加節點

  2. Nginx,單個節點通過Nginx負載均衡配置多個設備

  3. 數據庫,將原本存儲在一臺服務器上的數據(緩存,數據庫),用hash的方法拆分到不同服務器上去

  4. 當然還可以在客戶端、應用層做一些處理,提高併發能力

    垂直擴展,主要是通過系統架構來實現高併發。

任務類型主要分爲IO密集型和CPU密集型。IO密集型任務主要佔用IO,計算消耗很少,因此對於這類任務需要將其IO佔用與CPU佔用分開,否則將浪費CPU的時間。CPU密集型任務,主要是消耗CPU進行計算,因此必要時需要將計算分散到多個CPU進行,這樣纔可以減少時間。

目前網絡任務大多是IO密集型任務,瓶頸一般在網絡IO上。因此本文主要討論這類問題。

對於單機來說,實現高併發主要是通過:多進程、多線程、協程、IO多路複用。

多進程

進程創建:

一般來說,一個程序就是一個進程,它有自己的虛擬地址空間,進程表示一個運行的程序,程序的代碼段,數據段這些都是存放在磁盤中的,在運行時加載到內存中。所以虛擬內存面向的是磁盤,虛擬頁是對磁盤文件的分配,然後被緩存到物理內存的物理頁中。

所以存儲資源是操作系統由虛擬內存機制來管理和分配的。進程是操作系統分配存儲資源的最小單元。

因此每個進程之間都是獨立的。Unix/Linux操作系統提供了一個fork()系統調用,產生新的進程,新的進程會複製子進程產生之前的數據,以及所有的代碼。一個進程至少有一個線程。

進程池:

如果要啓動大量的子進程,可以用進程池的方式批量創建子進程,如果需要,直接從進程池中獲取一個進程使用即可。

進程間通信:

進程間通信方式有很多,通常有管道(包括無名管道和命名管道)、消息隊列、信號量、Socket、Streams等。其中 Socket和Streams支持不同主機上的兩個進程通信。

進程調度:

進程創建、管理、調度都是由os自動操作的,一個進程一般只能佔用一個CPU。

多線程

Linux的線程本質上是一種輕量級的進程,是通過clone系統調用來創建的。進程是操作系統分配存儲資源的最小單元,那麼線程就是操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一個進程下的所有線程共享該進程的資源。因此不管是單進程多線程 還是 多進程單線程,其實調度的都是線程。

C語言利用了Pthreads庫來真正創建了線程這個數據結構。Linux採用了1:1的模型,即C語言的Pthreads庫創建的線程實體1:1對應着內核創建的一個KSE(Kernal Scheduling Entry, 內核調度實體)。Pthreads運行在用戶空間,KSE運行在內核空間。

在程序中可以將線程綁定到具體的CPU上。

在Python中,多線程並不能做到多核,因爲Python有GIL鎖機制。

線程也可以構建類似於進程池的線程池。

線程間通信:

鎖、信號量、條件變量、共享內存等

 

協程

Coroutine,翻譯成”協程“,Coroutine是編譯器級的,Process和Thread是操作系統級的。通過插入相關的代碼使得代碼段能夠實現分段式的執行,重新開始的地方是yield關鍵字指定的,一次一定會跑到一個yield對應的地方。

協程是輕量級線程,擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧。與多線程有些類似,但協程調用是在一個線程內進行的,是單線程,切換的開銷小,因此效率上略高於多線程。線程之間需要使用同步機制來避免產生全局資源的竟態,這就不可避免產生了休眠、調度、切換上下文一類的系統開銷,而且線程調度還會產生時序上的不確定性。

同時可以避免線程同步帶來的各種問題:競爭、死鎖等

協程間的調用是邏輯上可控的,時序上確定的,可謂一切盡在掌握中。

 

IO多路複用

進程、線程、協程,這三種方法只是三種工具,爲不同問題提供不同的解決方法,不同的問題可能需要用到不同的方法去解決。

網絡任務中,IO多路複用被經常採用,對於高併發的網絡任務,一般採用異步非阻塞的方式處理,因爲IO耗時遠遠大於CPU,而IO請求個數非常頻繁。

傳統的網絡服務器(如nginx、squid等)都採用了 EDSM (event-driven state machine,事件驅動狀態機) 機制併發處理請求,這是一種異步處理的方式,通過使用callback 方法避免阻塞線程。

EDSM最常見的方式就是I/O事件的異步回調。基本上都會有一個叫做dispatcher的單線程主循環(又叫event loop),用戶通過向dispatcher註冊回調函數(又叫event handler)來實現異步通知,從而不必在原地空耗資源乾等。在dispatcher主循環中通過select()/epoll()等系統調用來等待各種I/O事件的發生,當內核檢測到事件觸發並且數據可達或可用時,select()/epoll()會返回從而使dispatcher調用相應的回調函數來對處理用戶的請求。

如果採用進程、線程的方式對收到的請求進行回調,那麼需要創建進程池或者線程池,當有回調的時候,從進程池或者線程池中獲取一個進程或者線程去處理回調的事件。

如果採用協程的方式對請求進行 “回調”。那麼,整個過程都是單線程的。這種處理本質上就是將一堆相互獨立(disjoint)的回調實現同步控制,就像串聯在一個順序鏈表上,不存在進程/線程的切換。

協程是在單線程中使用同步編程思想來實現異步的處理流程,從而實現單線程能併發處理成百上千個請求,而且每個請求的處理過程是線性的,邏輯上可控的,時序上確定的。

 

對比:

  1. 線程執行開銷小,但不利於資源的管理和保護;而進程正相反。進程上下文切換要保存頁表,文件描述符表,信號控制數據和進程信息等數據。線程上下文切換是很輕量級的。

  2. 線程適合於在SMP(多核處理機)機器上運行,而進程則可以跨機器遷移。

  3. 進程有獨立的地址空間,一個進程崩潰後,在保護模式下不會對其它進程產生影響,而線程只是一個進程中的不同執行路徑。線程有自己的堆棧和局部變量,但線程之間沒有單獨的地址空間,一個線程死掉就等於整個進程死掉,所以多進程的程序要比多線程的程序健壯,

  4. 進程在執行過程中擁有獨立的內存單元,而多個線程共享內存,從而極大地提高了程序的運行效率。

 

總結:

每個進程擁有獨立的虛擬內存地址空間,會真正地擁有獨立與父進程之外的物理內存。並且由於進程擁有獨立的內存地址空間,導致了進程之間無法利用直接的內存映射進行進程間通信。

併發的本質是同時運行多個任務,併發非常重要的問題之一就是:共享資源的問題。而進程恰恰很難在邏輯上表示共享資源,需要通過消耗較大的其他方式進行資源的同步,如:管道、消息隊列、信號量、Socket等。

而線程可以很簡單地表示共享資源的問題,一個進程的所有線程都是共享這個進程的同一個虛擬地址空間的,也就是說從線程的角度來說,它們看到的物理資源都是一樣的,這樣就可以通過共享變量的方式來表示共享資源,也就是直接共享內存的方式解決了線程通信的問題。而線程也表示一個獨立的邏輯流,這樣就完美解決了進程的一個大難題。但是,線程同樣需要用鎖或者其他方式去解決資源的同步問題,這也會帶來額外的消耗。

協程不存在資源同步問題,因爲協程運行在一個線程中,只要邏輯正確,就不需要用到鎖。但是由於協程是在一個線程中的,而線程是OS調度的最小粒度,因此只靠協程沒法利用多核的優勢。協程的作用主要是用於異步IO,讓線程不用等待IO返回,或者不用掛起線程,減少線程切換的次數。所以,一般來說需要協程與進程/線程配合起來使用,達到更好的效果

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章