您好,登錄后才能下訂單哦!
Go語言中如何分析多進程、多線程與協程的引入,很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細講解,有這方面需求的人可以來學習下,希望你能有所收獲。
為什么要并發編程
在原生 PHP 中并沒有并發的概念,所有的操作都是串行執行的、同步阻塞的,這也是很多人詬病 PHP 性能的原因,但是不支持并發編程的好處也是顯而易見的:保證了 PHP 的簡單性,開發者不必考慮并發引入的線程安全,也不需要在編程時權衡是否需要通過加鎖來保證某個操作的原子性,也沒有線程間通信問題,魚和熊掌不可得兼,你不可能既要上手簡單又要高性能,實際上,90%以上公司的業務和場景根本對性能沒有那么高的要求,傳統的 Nginx + PHP-FPM 完全以勝任了,如果非要在 PHP 中實現異步和并發編程,推薦使用 Swoole 擴展來解決(實際上,Swoole 實現并發編程的協程功能正是借鑒了 Go 語言的協程實現機制)。
下面,我們書歸正傳,來介紹 Go 語言中并發編程的概念和實現。
與并發相對的是串行,即代碼按照順序一行一行執行,當遇到某個耗時的 IO 操作時,比如發送郵件、查詢數據庫等,要等到該 IO 操作完成后才能繼續執行下一行代碼,這在一些要求高并發高性能的業務場景中,顯然是不合適的,從整個操作系統層面來說,多個任務是可以并發執行的,因為 CPU 本身通常是多核的,而且即使是單核 CPU,也可以通過時間分片的方式在多個進程/線程之間切換執行,從用戶角度來說,就好像它們在「同時發生」一樣,比如說,當程序執行到 IO 操作時,我們可以掛起這個任務,把 CPU 時間片出讓給其他任務,然后當這個 IO 操作完成時,通知 CPU 恢復后續代碼的執行,實際上 CPU 大部分時間都是在做這種調度。所以并發編程可以最大限度榨取 CPU 的價值,提高程序的執行效率和性能。
目前,主流的并發編程實現有以下幾種方式:
多進程。多進程是在操作系統層面進行并發的基本模式,同時也是開銷最大的模式。在 Linux 平臺上,很多工具正是采用這種模式在工作,比如 PHP-FPM,它會有專門的主進程負責網絡端口的監聽和連接管理,還會有多個工作進程負責具體的請求處理。這種方法的好處在于簡單、進程間互不影響,壞處在于系統開銷大,因為所有的進程都是由內核管理的,而且不同進程的數據也是相互隔離的。
多線程。多線程在大部分操作系統上都屬于系統層面的并發模式,也是我們使用最多的最有效的一種模式。目前,常見的幾乎所有工具都會使用這種模式,線程比進程輕量級,線程間可以共享數據,開銷要比多進程小很多,但是依舊比較大,且在高并發模式下,效率會有影響,比如 C10K 問題,即支持 1 萬個并發連接需要一萬個線程,這不但對系統資源有較高的要求,還對 CPU 管理這些線程帶來巨大負擔。
基于回調的非阻塞/異步 IO。為了解決 C10K 問題,在很多高并發服務器開發實踐中,都會通過事件驅動的方式使用異步 IO,在這種模式下,一個線程可以維護多個 Socket 連接,從而降低系統開銷,保持服務器的持續運轉,它目前在 Node.js 中得到了很好的實踐,實際上 Nginx 也使用了這種方式。但是使用這種模式,編程比多線程要復雜,通常需要借助 Linux 底層的庫函數來實現。
協程。協程(Coroutine)本質上是一種用戶態線程,你可以把它看作輕量級的線程,不需要操作系統來進行搶占式調度,系統開銷極小,可以有效提高線程的任務并發性,避免多線程的缺點。使用協程的優點是編程簡單,結構清晰;缺點是需要語言級別的支持,如果不支持,則需要用戶在程序中自行實現相應的調度器。目前,原生支持協程的語言還很少,Go 語言就是其中這一,Go 語言中的協程稱作「goroutine」,并且使用語言名稱本身 go
做為協程的關鍵字,足見其在 Go 語言中的舉足輕重。PHP 的 Swoole 擴展也是參考了 Go 協程的實現將其搬到 PHP 中。
接下來我們先詮釋一下傳統并發模型的缺陷,之后再講解 goroutine 并發模型是如何解決這些缺陷的。如果你之前只是熟悉 PHP 編程,沒有接觸過并發編程,那么需要好好消化下這些概念,學習完 Go 并發編程再回去看 Swoole 的協程,就非常駕輕就熟了。由于多進程比較消耗系統資源,且進程間數據隔離,CPU 切換成本高,因此,傳統并發編程多以多線程為主,比如 Java 就是這么做的。下面我們重點探討多線程與協程的對比。
我們之前在 PHP 中編程多是串行思維,串行的事務具有確定性,比如我們想好了123,然后按照這個順序來編寫代碼,代碼會嚴格按照這個設定的順序執行,即使在某一個步驟阻塞了,也會一直等待阻塞代碼執行完畢,再去執行下一步的代碼。
多線程并發模式在這種確定性中引入了不確定性,比如我們原先設定的123,第2步是一個耗時操作,我們啟動了一個新的線程來處理,這個時候就存在了兩個并發的線程,即原來的主線程和第2步啟動的新線程,主線程繼續往后執行,第2、3步的代碼并發執行,這個時候不確定性就來了,我們不知道主線程執行完畢的時候,新線程是否執行完畢了,如果主線程執行完畢退出應用,可能導致新線程的中斷,或者我們在第3步的時候依賴第2步的某個返回結果,我們不知道啥時候能夠返回這個結果,如果第2、3步有相互依賴的變量,甚至可能出現死鎖,以及我們如何在主線程中獲取新線程的異常和錯誤信息并進行相應的處理,等等,這種不確定性給程序的行為帶來了意外和危害,也讓程序變得不可控。
不同的線程好比平行時空,我們需要通過線程間通信來告知不同線程目前各自運行的狀態和結果,以便使程序可控,線程之間通信可以通過共享內存的方式(參考 Swoole 中的 Swoole Table),即在不同線程中操作的是同一個內存地址上存儲的值。為了保證共享內存的有效性,需要采取很多措施,比如加鎖來避免死鎖或資源競爭,還是以上面的主線程和新線程為例,如果我們在第1步獲取了一個中間結果,第2步和第3步都要對這個中間結果進行操作,如果不加鎖保證操作的原子性,很有可能產生臟數據。諸如此類的問題在生產環境極有可能造成重大故障甚至事故,而且不易察覺和調試。
我們可以將線程加共享內存的方式稱為「共享內存系統」。為了解決共享內存系統存在的問題,計算機科學家們又提出了「消息傳遞系統」,所謂「消息傳遞系統」指的是將線程間共享狀態的各種操作都封裝在線程之間傳遞的消息中,這通常要求發送消息時對狀態進行復制,并且在消息傳遞的邊界上交出這個狀態的所有權。從表明上來看,這個操作與「共享內存系統」中執行的通過加鎖實現原子更新操作相同,但從底層實現上來看則不同:一個對同一個內存地址持有的值進行操作,一個是從消息通道讀取數據并處理。由于需要執行狀態復制操作,所以大多數消息傳遞的實現在性能上并不優越,但線程中的狀態管理工作則會變得更加簡單,這就有點像我們在開篇講 PHP 不支持并發編程提到的那樣,如果想讓編碼簡單,性能就要做犧牲,如果想追求性能,代碼編寫起來就比較費勁,這也是我們為什么通常不會直接通過事件驅動的異步 IO 來實現并發編程一樣,因為這涉及到直接調用操作系統底層的庫函數(select、epoll、libevent 等)來實現,非常復雜。
注:最早被廣泛應用的「消息傳遞系統」是由 C. A. R. Hoare 在他的 Communicating Sequential Processes 中提出的,在 CSP 系統中,所有的并發操作都是通過獨立線程以異步運行的方式來實現的。這些線程必須通過在彼此之間發送消息,從而向另一個線程請求信息或者將信息提供給另一個線程。
與傳統的系統級線程和進程相比,協程的最大優勢在于輕量級(可以看作用戶態的輕量級線程),我們可以輕松創建上百萬個協程而不會導致系統資源衰竭,而線程和進程通常最多也不能超過 1 萬個(C10K問題)。多數語言在語法層面并不直接支持協程,而是通過庫的方式支持,比如 PHP 的 Swoole 擴展庫,但用庫的方式支持的功能通常并不完整,比如僅僅提供輕量級線程的創建、銷毀與切換等能力。如果在這樣的輕量級線程中調用一個同步 IO 操作,比如網絡通信、本地文件讀寫,都會阻塞其他的并發執行輕量級線程,從而無法真正達到輕量級線程本身期望達到的目標。
Go 語言在語言級別支持協程,稱之為 goroutine。Go 語言標準庫提供的所有系統調用操作(當然也包括所有同步 IO 操作),都有協程的身影。協程間的切換管理不依賴于系統的線程和進程,也不依賴于 CPU 的核心數量,這讓我們在 Go 語言中通過協程實現并發編程變得非常簡單。
Go 語言的協程系統是基于「消息傳遞系統」實現的,在 Go 語言的編程哲學中,創始人 Rob Pike 推介「Don’t communicate by sharing memory, share memory by communicating(不要通過共享內存來通信,而應該通過通信來共享內存)」,這正是「消息傳遞系統」的精髓所在。Go 語言中的 goroutine 和用于傳遞協程間消息的 channel 一起,共同構筑了 Go 語言協程系統的基石。
go是golang的簡稱,而golang可以做服務器端開發,且golang很適合做日志處理、數據打包、虛擬機處理、數據庫代理等工作。在網絡編程方面,它還廣泛應用于web應用、API應用等領域。
看完上述內容是否對您有幫助呢?如果還想對相關知識有進一步的了解或閱讀更多相關文章,請關注億速云行業資訊頻道,感謝您對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。