併發模型 常見的併發模型一般包括3類,基於線程與鎖的記憶體共用模型,actor模型和CSP模型,其中尤以線程與鎖的共用記憶體模型最為常見。由於go語言的興起,CSP模型也越來越受關註。基於鎖的共用記憶體模型與後兩者的主要區別在於,到底是通過共用記憶體來通信,還是通過通信來實現訪問共用記憶體。由於actor模型 ...
併發模型
常見的併發模型一般包括3類,基於線程與鎖的記憶體共用模型,actor模型和CSP模型,其中尤以線程與鎖的共用記憶體模型最為常見。由於go語言的興起,CSP模型也越來越受關註。基於鎖的共用記憶體模型與後兩者的主要區別在於,到底是通過共用記憶體來通信,還是通過通信來實現訪問共用記憶體。由於actor模型和CSP模型,本人並不是特別瞭解,我主要說說最基本的併發模型,基於線程與鎖的記憶體共用模型。
為什麼要併發,本質都是為了充分利用多核CPU資源,提高性能。但併發又不能亂,為了保證正確性,需要通過共用記憶體來協調併發,確保程式正確運轉。無論是多進程併發,還是多線程併發,要麼通過線程間互斥同步(spinlock,rwlock,mutex,condition,信號量),要麼通過進程間通信(共用記憶體,管道,信號量,套接字),本質都是為了協同。多線程和多進程本質類似,尤其是linux環境下的pthread庫,本質是用輕量級進程實現線程。下麵以網路服務為例,簡單討論下多線程模型的演進。
最簡單的模型是單進程單線程模型,來一個請求處理一個請求,這樣效率很低,也無法充分利用系統資源。那麼可以簡單的引入多線程,其中抽出一個線程監聽,每來一個請求就創建一個工作線程服務,多個請求多個線程,這就是多線程併發模型。這種模式下,資源利用率是上去了,但是卻有很多浪費,線程數與請求數成正比,意味著頻繁的創建/銷毀線程開銷,頻繁的上下文切換開銷,這些都是通過系統調用完成,需要應用態到內核態的切換,導致sys-cpu偏高,資源並沒有充分利用在處理請求上。
為了緩解這個問題,引入線程池模型,簡單來說,就是預先創建好一批線程,並且加大線程的復用能力,將線程數控制在一定數目內,緩解上下文切換開銷。以MySQL線程池為例,原來多線程模型是單連接單線程,現在變成單語句單線程,提高了線程復用效率。如果線程在執行過程中遇到等待(鎖等待,IO等待),那麼線程掛起,並減少活躍線程數,告知線程池系統活躍線程可能不夠,需要追加線程,然後等系統空閑時,再減少線程數目,做到根據系統負載平衡線程數目。為了做到極致,更進一步減少上下文切換開銷,引入了協程,協程只是一種用戶態的輕量線程,它運行在用戶空間,不受系統調度。它有自己的調度演算法。在上下文切換的時候,協程在用戶空間切換,而不是陷入內核做線程的切換,減少了開銷。協程的併發,是單線程內控制權的輪轉,相比搶占式調度,協程是主動讓權,實現協作。協程的優勢在於,相比回調的方式,寫的非同步代碼可讀性更強。缺點在於,因為是用戶級線程,利用不了多核機器的併發執行。簡單總結下:
單線程-->(單線程輪詢處理,太慢)
多線程-->(多線程會頻繁地創建、銷毀線程,這對系統也是個不小的開銷。這個問題可以用線程池來解決。)
線程池-->(仍然有多線程上下文切換的問題,調度由內核調度)
協程-->(應用層調度,不touch內核)
I/O模型
linux中所有物理設備對於系統而言都可以抽象成文件,包括網卡,對應的就是套接字,磁碟對應的文件,以及管道等。因此所有對物理設備的讀寫操作都可以抽象為IO操作,典型的IO操作模型分為以下幾類,阻塞IO,非阻塞IO,I/O多路復用,非同步非阻塞IO以及非同步IO等。
IO模型分類
阻塞I/O--> 原生的read/write系統調用,預設導致線程阻塞;
非阻塞I/O -->通過指定系統調用read/write的參數為非阻塞,告知內核fd沒就緒時,不阻塞線程,而是返回一個錯誤碼,應用死迴圈輪詢,直到fd就緒;
I/O多路復用-->(select/poll/epoll),對通知事件堵塞,對於I/O調用不堵塞。
非同步I/O(非同步非阻塞)-->告知內核某個操作(讀寫I/O),並讓內核在整個操作(包括將數據複製到我們的進程緩衝區)完成後通知。
I/O多路復用
常見的I/O多路復用主要用於網路IO場景,主要有select,poll和epoll機制。對比同步I/O,實際上是對I/O請求加了一層代理,由這些代理去監聽通知事件(是否網路包到來),然後再通知用戶去讀寫數據。這種方式也是一種阻塞I/O,代理對通知事件阻塞,這裡的代理一般指監聽線程。對比select,poll提升了最大支持文件描述符數目,從1024提升到65535,MySQL中的半同步複製還因為使用select的這個限制,導致半同步中斷的bug(鏈接)。
對比select和poll機制,epoll通過事件表管理用戶感興趣的事件,無需反覆傳入用戶感興趣事件,處理事件通知的時間複雜度是O(1),而select,poll機制的時間複雜度是O(N)。另外select/poll只能工作在LT模式(水平觸發模式);而epoll不僅支持LT模式,還支持ET模式(邊緣觸發模式)。兩種模式的主要區別是,有數據可讀時,LT模式會不停的通知,直到數據被獲取,這種模式不用擔心通知事件丟失;ET模式只會通知一次,因此對比LT少很多epoll系統調用,效率更高。epoll對編程要求高,需要細緻的處理每個請求,否則容易發生丟失事件的情況。從本質上講,與LT相比,ET模型是通過減少系統調用來達到提高並行效率的。
libev/libeasy
epoll很好用,但是要使用epoll,fd,signal,timer分別要採用不同的機制才能一起工作。libev第一個要做的事情就是把系統資源統一成一種調用方式。因為都需要在讀寫事件就緒後自己負責進行讀寫,也就是讀寫過程是阻塞的。libev的核心是事件處理框架,最常見的是就是一個所謂的Reactor事件處理框架和設計模式。Reactor對象負責實現主迴圈(其中有事件分離器的調用),定義事件處理介面,用戶程式向Reactor註冊事件回調的實現類(從介面繼承),Reactor主迴圈在收到事件的時候調用相應的回調函數。libeasy實現類似libev和libevent的功能,包括HTTP伺服器等,不同的是,它基於libev做了包裝,提供了同一個的資源fd和loop機制,線程池,非同步框架等實現。
AIO
說到AIO,一般是說磁碟的非同步I/O,linux早期的版本並沒有真正的AIO介面,所謂的AIO其實是多線程模擬的,在應用態完成。具體而言就是有一個隊列存儲IO請求,通過一組工作線程提取任務,併發起同步IO,待IO完成後,再通知用戶已經完成了。對於用戶而言,由於是提交IO請求後就直接返回,然後再被通知IO已經完成,所以可以認為是非同步I/O,這種非同步I/O實現機制主要指POXIS AIO,MySQL的InnoDB引擎也實現了一套類似的AIO機制。後面linux內核引入了真正的AIO,主要區別在於發起I/O調用不再是同步調用,IO請求統一在內核層面排隊,並且一次可以提交一批非同步IO請求,然後通過輪詢或者回調的方式接收完成通知即可。相比於POXIS AIO,底層有更多的IO並行,IO和CPU能充分併發,大大提升性能。在使用中,通過-lrt鏈接使用AIO庫是POXIS介面,而通過-laio鏈接使用的AIO庫是linux Native AIO介面。常用介面包括 io_setup,io_destroy,io_submit,io_cacel和io_getevents等。
同步IO:
優點:簡單
缺點:IO阻塞,無法充分利用IO和CPU資源,效率低
Native AIO:
優點:AIO可以支持一次發送多個不連續的非同步IO請求,性能更好(同步IO需要發送多次)
缺陷:需要文件系統支持O_DIRECT選項,如果不支持,io_submit實際上是“退化”成同步操作。
POSIX AIO:
優點:不依賴O_DIRECT選項,有一定的合併能力(相鄰地址的請求,可以做merge)。
缺點:併發的IO請求受限於線程數目;另外就是,可能慢速磁碟,可能導致其它新的請求沒有及時處理(工作線程數不夠了)。
參考文檔
https://cloud.tencent.com/developer/article/1349213
https://my.oschina.net/dclink/blog/287198
https://www.cnblogs.com/lojunren/p/3856290.html
https://www.ibm.com/developerworks/cn/linux/l-async/