併發模型html
常見的併發模型通常包括3類,基於線程與鎖的內存共享模型,actor模型和CSP模型,其中尤以線程與鎖的共享內存模型最爲常見。因爲go語言的興起,CSP模型也愈來愈受關注。基於鎖的共享內存模型與後二者的主要區別在於,究竟是經過共享內存來通訊,仍是經過通訊來實現訪問共享內存。因爲actor模型和CSP模型,本人並非特別瞭解,我主要說說最基本的併發模型,基於線程與鎖的內存共享模型。linux
爲何要併發,本質都是爲了充分利用多核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
http://www.javashuo.com/article/p-wsiwtuxy-m.html
https://www.ibm.com/developerworks/cn/linux/l-async/