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