九、Golang并发和线程模型
@Author:Runsen
開始前來介紹幾個概念:
- 進程:進程是程序在操作系統中的一次執行過程,系統進行資源分配和調度的一個獨立單位。
- 線程:線程是進程的一個執行實體,是 CPU 調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位。
- 并發:多線程程序在單核心的 cpu 上運行,稱為并發
- 并行:多線程程序在多核心的 cpu 上運行,稱為并行。
- 協程:獨立的棧空間,共享堆空間,調度由用戶自己控制,本質上有點類似于用戶級線程,一個線程上可以跑多個協程,協程是輕量級的線程。
并發與并行并不相同,并發主要由切換時間片來實現“同時”運行,并行則是直接利用多核實現多線程的運行,Go程序可以設置使用核心數,以發揮多核計算機的能力。
線程主要分為用戶線程和內核線程,用戶線程由各語言代碼所支持,而內核線程是由操作系統內核所支持。多線程模型主要就是用戶線程與內核線程的連接方式:
下面我們來探討Go的線程模型,首先我們先來回顧下常見的三種線程模型,然后在介紹Go中獨特的線程模型CSP。
文章目錄
- 三種線程模型
- Goroutine
- CSP
三種線程模型
線程模型主要有三種:1、內核級別線程;2、用戶級別線程;3、混合線程,分別對應的是1:1、N:1、M:N。
內核級別線程:一對一模型(1 : 1):每個用戶級線程映射到一個內核級線程。優點是緩存讀寫快速,缺點是容易阻塞。
用戶級別線程: 多對一模型(M : 1):多個用戶級線程映射到一個內核級線程,線程管理在用戶空間完成。
混合線程:多對多模型(M : N):內核線程和用戶線程的數量比為 M : N,綜合了前兩種的優點
Goroutine
goroutine 是 Go 語言并行設計的核心,有人稱之為 go 程。goroutine 是輕量級線程,goroutine 的調度是由 Golang 運行時進行管理的。在Java中,goroutine就是Thead 。
goroutine 語法格式:go 函數名( 參數列表 )。例如:go f(x, y, z),開啟一個新的 goroutine:f(x, y, z)。
在并發編程中,我們通常想將一個過程切分成幾塊,然后讓每個 goroutine 各自負責一塊工作,當一個程序啟動時,主函數在一個單獨的 goroutine 中運行,我們叫它 `main goroutine。新的 goroutine 會用 go 語句來創建。而 go 語言的并發設計,讓我們很輕松就可以達成這一目的。
我們來看一個示例。
package mainimport ("fmt""time" )func newTask() {i := 0for {i++fmt.Printf("new goroutine: i = %d\n", i)time.Sleep(1*time.Second) //延時1s} }func main() {//創建一個 goroutine,啟動另外一個任務go newTask()i := 0//main goroutine 循環打印 不會暫停for {i++fmt.Printf("main goroutine: i = %d\n", i)time.Sleep(1 * time.Second) //延時1s} }程序運行結果:
如果主 goroutine 退出后,那么其它的工作 goroutine 也會自動退出:
package mainimport ( "fmt" "time" )func newTask() {i := 0for {i++fmt.Printf("new goroutine: i = %d\n", i)time.Sleep(1 * time.Second) //延時1s} }func main() {//創建一個 goroutine,啟動另外一個任務go newTask()fmt.Println("main goroutine exit") }程序運行結果如下,不會一直執行。
new goroutine: i = 1 main goroutine exit上面就是Go實現了并發比較常見的Goroutine方法。
CSP
我們常見的多線程模型一般是通過共享內存實現的(就是Goroutine的原理),但是共享內存就會有很多問題。比如資源搶占的問題、一致性問題等等。為了解決這些問題,我們需要引入多線程鎖、原子操作等等限制來保證程序執行結果的正確性。
這就引出了另外一種是Go語言特有的并發形式,也是Go語言推薦的:CSP(communicating sequential processes)并發模型。
Go的CSP并發模型,是通過goroutine和channel來實現的。
CSP模式中,消息是通過Channel來通訊的,Channel相當于一個消息通訊的中間人,這樣可以讓兩個通訊實體的耦合更松一些
下面Runsen先介紹下Channel,回顧一下基礎知識。通道可用于兩個 goroutine 之間通過傳遞一個指定類型的值來同步運行和通訊。操作符 <-用于指定通道的方向,發送或接收。如果未指定方向,則為雙向通道。
ch <- v // 把 v 發送到通道 ch v := <-ch // 從 ch 接收數據// 并把值賦給 v聲明一個通道很簡單,我們使用chan關鍵字即可,通道在使用前必須先創建:
ch := make(chan int)channel分為兩種,有緩沖channel和無緩沖channel,默認情況下,通道是不帶緩沖區的。我們通過下邊的代碼例子來區分不同的channel種類。
package mainimport ("fmt" )func main() {pipline := make(chan string) //構造無緩沖通道pipline <- "hello world" //發送數據fmt.Println(<-pipline) //讀數據 }運行會拋出錯誤,如下:
fatal error: all goroutines are asleep - deadlock!如果把這個例子改成有緩沖通道還會阻塞嗎?我們繼續看下邊的例子:
package mainimport ("fmt" )func main() {pipline := make(chan string, 1 ) //構造無緩沖通道pipline <- "hello world" //發送數據fmt.Println(<-pipline) //讀數據 }hello world這里運行正常,此時就說明了緩沖和沒有緩沖的區別在于在發送操作是否發生在有接受者時。那么,對于有緩沖通道會發生什么特殊情況呢?
如果這段代碼,通道容量為 1,但是往通道中寫入兩條數據,對于一個協程來說就會造成死鎖。
package mainimport ("fmt" ) func main() {ch1 := make(chan string, 1)ch1 <- "hello world"ch1 <- "hello China"fmt.Println(<-ch1) }//fatal error: all goroutines are asleep - deadlock!每個緩沖通道,都有容量,當通道里的數據量等于通道的容量后,此時再往通道里發送數據,就失造成阻塞,必須等到有人從通道中消費數據后,程序才會往下進行。
下面,我們看goroutine和channel實現CSP并發模型,代碼來自菜鳥教程。
package mainimport "fmt" // 計算數字之和 func sum(s []int, c chan int) {sum := 0for _, v := range s {sum += v}c <- sum // 把 sum 發送到通道 c }func main() {s := []int{7, 2, 8, -9, 4, 0}c := make(chan int)go sum(s[:len(s)/2], c) //-9+4+0go sum(s[len(s)/2:], c) // 7+2+8x, y := <-c, <-c // 從通道 c 中接收fmt.Println(x, y, x+y) //-5 17 12\ }上面示例通過兩個 goroutine 來計算數字之和,在 goroutine 完成計算后,它會計算兩個結果的和。這里的channel是沒有緩沖。
通道可以設置緩沖區,通過 make 的第二個參數指定緩沖區大小。
package mainimport "fmt"func sum(s []int, c chan int) {sum := 0for _, v := range s {sum += v}c <- sum // 把 sum 發送到通道 c }func main() {// 這里我們定義了一個可以存儲整數類型的帶緩沖通道// 緩沖區大小為2,因為存儲了一個數字,所以沒有報錯s := []int{7, 2, 8, -9, 4, 0}c := make(chan int ,2)go sum(s[:len(s)/2], c)go sum(s[len(s)/2:], c)x, y := <-c, <-c // 從通道 c 中接收fmt.Println(x, y, x+y) // -5 17 12 }總結
以上是生活随笔為你收集整理的九、Golang并发和线程模型的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 婚姻保险怎么买
- 下一篇: 八、深入Go 编程语言接口