一文帶你了解Golang中的并發(fā)性
并發(fā)是一個很酷的話題,一旦你掌握了它,就會成為一筆巨大的財富。說實話,我一開始很害怕寫這篇文章,因為我自己直到最近才對并發(fā)性不太適應(yīng)。我已經(jīng)掌握了基礎(chǔ)知識,所以我想幫助其他初學(xué)者學(xué)習(xí)Go的并發(fā)性。這是眾多并發(fā)性教程中的第一篇,請繼續(xù)關(guān)注更多的教程。
什么是并發(fā)性,為什么它很重要
并發(fā)是指在同一時間運行多個事物的能力。你的電腦有一個CPU。一個CPU有幾個線程。每個線程通常一次運行一個程序。當(dāng)我們通常寫代碼時,這些代碼是按順序運行的,也就是說,每項工作都是背對背運行的。在并發(fā)代碼中,這些工作是由線程同時運行的。
一個很好的比喻是對一個家庭廚師的比喻。我還記得我第一次嘗試煮意大利面的時候。我按照菜譜一步步地做。我切了蔬菜,做了醬汁,然后煮了意大利面條,再把兩者混合起來。在這里,每一步都是按順序進行的,所以下一項工作必須等到當(dāng)前工作完成后才能進行。
快進到現(xiàn)在,我在烹飪意大利面條方面變得更有經(jīng)驗。我現(xiàn)在先開始做意大利面,然后在這期間進行醬汁的制作。烹飪時間幾乎減少到一半,因為烹飪意大利面條和醬汁是同時進行的。
并發(fā)性與平行性
并發(fā)性與并行性有些不同。并行性與并發(fā)性類似,即同時發(fā)生多項工作。然而,在并行性中,多個線程分別在進行不同的工作,而在并發(fā)性中,一個線程在不同的工作之間游走。
因此,并發(fā)性和并行性是兩個不同的概念。一個程序既可以并發(fā)地運行,也可以并行地運行。你的代碼可以按順序?qū)?,也可以按并發(fā)寫。該代碼可以在單核機器或多核機器上運行。把并發(fā)性看作是你的代碼的一個特征,而把并行性看作是執(zhí)行的一個特征。
Goroutines, the worker Mortys
Go使編寫并發(fā)代碼變得非常簡單。每個并發(fā)的工作都由一個goroutine來表示。你可以通過在函數(shù)調(diào)用前使用go關(guān)鍵字來啟動一個goroutine。看過《瑞克和莫蒂》嗎?想象一下,你的主函數(shù)是一個Rick,他把任務(wù)委托給goroutine Mortys。
讓我們從一個連續(xù)的代碼開始。
package main import ( "fmt" "time" ) func main() { simple() } func simple() { fmt.Println(time.Now(), "0") time.Sleep(time.Second) fmt.Println(time.Now(), "1") time.Sleep(time.Second) fmt.Println(time.Now(), "2") time.Sleep(time.Second) fmt.Println("done") }
2022-08-14 16:22:46.782569233 +0900 KST m=+0.000033220 0
2022-08-14 16:22:47.782728963 +0900 KST m=+1.000193014 1
2022-08-14 16:22:48.782996361 +0900 KST m=+2.000460404 2
done
上面的代碼打印出當(dāng)前時間和一個字符串。每條打印語句的運行時間為一秒??偟膩碚f,這段代碼大約需要三秒鐘的時間來完成。
現(xiàn)在讓我們把它與一個并發(fā)的代碼進行比較。
func main() { simpleConc() } func simpleConc() { for i := 0; i < 3; i++ { go func(index int) { fmt.Println(time.Now(), index) }(i) } time.Sleep(time.Second) fmt.Println("done") }
2022-08-14 16:25:14.379416226 +0900 KST m=+0.000049175 2
2022-08-14 16:25:14.379446063 +0900 KST m=+0.000079012 0
2022-08-14 16:25:14.379450313 +0900 KST m=+0.000083272 1
done
上面的代碼啟動了三個goroutines,分別打印當(dāng)前時間和i。這段代碼花了大約一秒鐘完成。這比順序版本快了三倍左右。
"等一下,"我聽到你問。"為什么要等整整一秒?難道我們不能刪除這一行以使程序盡可能快地運行嗎?"好問題!讓我們看看會發(fā)生什么。
func main() { simpleConcFail() } func simpleConcFail() { for i := 0; i < 3; i++ { go func(index int) { fmt.Println(time.Now(), index) }(i) } fmt.Println("done") }
done
嗯......。程序確實在沒有任何慌亂的情況下退出了,但我們?nèi)鄙賮碜詆oroutines的輸出。為什么它們被跳過?
這是因為在默認(rèn)情況下,Go并不等待goroutine的完成。你知道m(xù)ain也是在goroutine里面運行的嗎?主程序通過調(diào)用simpleConcFail來啟動工作程序,但它在工作程序完成工作之前就退出了。
讓我們回到烹飪的比喻上。想象一下,你有三個廚師,他們分別負(fù)責(zé)烹飪醬料、意大利面和肉丸。現(xiàn)在,想象一下,如果戈登-拉姆齊命令廚師們做一盤意大利面條和肉丸子。這三位廚師將努力工作,烹制醬汁、意大利面條和肉丸。但是,在廚師們還沒有完成的時候,戈登就按了鈴,命令服務(wù)員上菜。很明顯,食物還沒有準(zhǔn)備好,顧客只能得到一個空盤子。
這就是為什么我們在退出節(jié)目前等待一秒鐘。我們并不總是確定每項工作都會在一秒鐘內(nèi)完成。有一個更好的方法來等待工作的完成,但我們首先需要學(xué)習(xí)另一個概念。
總結(jié)一下,我們學(xué)到了這些東西:
- 工作被委托給goroutines。
- 使用并發(fā)性可以提高你的性能。
- 主goroutine默認(rèn)不等待工作goroutine完成。
- 我們需要一種方法來等待每個goroutine完成。
Channels, the green portal
goroutines之間是如何交流的?當(dāng)然是通過通道。通道的作用類似于門戶。你可以通過通道發(fā)送和接收數(shù)據(jù)。下面是你如何在Go中制作一個通道。
ch := make(chan int)
每個通道都是強類型的,并且只允許該類型的數(shù)據(jù)通過。讓我們看看我們?nèi)绾问褂眠@個。
func main() { unbufferedCh() } func unbufferedCh() { ch := make(chan int) go func() { ch <- 1 }() res := <-ch fmt.Println(res) }
1
很簡單,對嗎?我們做了一個名為ch的通道。我們有一個goroutine,向ch發(fā)送1,我們接收該數(shù)據(jù)并將其保存到res。
你問,為什么我們在這里需要一個goroutine?因為不這樣做會導(dǎo)致死鎖。
func main() { unbufferedChFail() } func unbufferedChFail() { ch := make(chan int) ch <- 1 res := <-ch fmt.Println(res) }
fatal error: all goroutines are asleep - deadlock!
我們碰到了一個新詞。什么是死鎖?死鎖就是你的程序被卡住了。為什么上面的代碼會卡在死鎖中?
為了理解這一點,我們需要知道通道的一個重要特性。我們創(chuàng)建了一個無緩沖的通道,這意味著在某一特定時間內(nèi)沒有任何東西可以被存儲在其中。這意味著發(fā)送方和接收方都必須同時準(zhǔn)備好,才能在通道上傳輸數(shù)據(jù)。
在失敗的例子中,發(fā)送和接收的動作依次發(fā)生。我們發(fā)送1到ch,但在那個時候沒有人接收數(shù)據(jù)。接收發(fā)生在稍后的一行,這意味著在接收行運行之前,1不能被發(fā)送??杀氖牵?不能先被發(fā)送,因為ch是沒有緩沖的,沒有空間來容納任何數(shù)據(jù)。
在這個工作例子中,發(fā)送和接收的動作同時發(fā)生。主函數(shù)啟動了goroutine,并試圖從ch中接收,此時goroutine正在向ch發(fā)送1。
另一種從通道接收而不發(fā)生死鎖的方法是先關(guān)閉通道。
func main() { unbufferedCh() } func unbufferedCh() { ch2 := make(chan int) close(ch2) res2 := <-ch2 fmt.Println(res2) }
0
關(guān)閉通道意味著不能再向它發(fā)送數(shù)據(jù)。我們?nèi)匀豢梢詮脑撏ǖ乐薪邮账τ谖淳彌_的通道,從一個關(guān)閉的通道接收將返回一個通道類型的零值。
總結(jié)一下,我們學(xué)到了這些東西:
- 通道是goroutines之間相互交流的方式。
- 你可以通過通道發(fā)送和接收數(shù)據(jù)。
- 通道是強類型的。
- 沒有緩沖的通道沒有空間來存儲數(shù)據(jù),所以發(fā)送和接收必須同時進行。否則,你的代碼就會陷入死鎖。
- 一個封閉的通道將不接受任何數(shù)據(jù)。
- 從一個封閉的非緩沖通道接收數(shù)據(jù)將返回一個零值。
如果通道能保持?jǐn)?shù)據(jù)一段時間,那不是很好嗎?這里就是緩沖通道發(fā)揮作用的地方。
Buffered channels, the portal that is somehow cylindrical?
緩沖通道是帶有緩沖器的通道。數(shù)據(jù)可以存儲在其中,所以發(fā)送和接收不需要同時進行。
func main() { bufferedCh() } func bufferedCh() { ch := make(chan int, 1) ch <- 1 res := <-ch fmt.Println(res) }
1
在這里,1被儲存在ch里面,直到我們收到它。
很明顯,我們不能向一個滿了緩沖區(qū)的通道發(fā)送更多的信息。你需要在緩沖區(qū)內(nèi)有空間才能發(fā)送更多。
func main() { bufferedChFail() } func bufferedChFail() { ch := make(chan int, 1) ch <- 1 ch <- 2 res := <-ch fmt.Println(res) }
fatal error: all goroutines are asleep - deadlock!
你也不能從一個空的緩沖通道接收。
func main() { bufferedChFail2() } func bufferedChFail2() { ch := make(chan int, 1) ch <- 1 res := <-ch res2 := <-ch fmt.Println(res, res2) }
fatal error: all goroutines are asleep - deadlock!
如果一個通道已滿,發(fā)送操作將等待,直到有可用的空間。這在這段代碼中得到了證明。
func main() { bufferedCh2() } func bufferedCh2() { ch := make(chan int, 1) ch <- 1 go func() { ch <- 2 }() res := <-ch fmt.Println(res) }
1
我們接收一次是為了取出1,這樣goroutine就可以發(fā)送2到通道。我們沒有從ch接收兩次,所以只接收1。
我們也可以從封閉的緩沖通道接收。在這種情況下,我們可以在封閉的通道上設(shè)置范圍來迭代里面的剩余項目。
func main() { bufferedChRange() } func bufferedChRange() { ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 close(ch) for res := range ch { fmt.Println(res) } // you could also do this // fmt.Println(<-ch) // fmt.Println(<-ch) // fmt.Println(<-ch) }
1
2
3
在一個開放的通道上測距將永遠不會停止。這意味著在某些時候,通道將是空的,測距循環(huán)將試圖從一個空的通道接收,從而導(dǎo)致死鎖。
總結(jié)一下:
- 緩沖通道是有空間容納項目的通道。
- 發(fā)送和接收不一定要同時進行,與非緩沖通道不同。
- 向一個滿的通道發(fā)送和從一個空的通道接收將導(dǎo)致一個死鎖。
- 你可以在一個封閉的通道上進行迭代,以接收緩沖區(qū)內(nèi)的剩余值。
等待戈多...我的意思是,goroutines來完成,使用通道
通道可以用來同步goroutines。還記得我告訴過你,在通過無緩沖通道傳輸數(shù)據(jù)之前,發(fā)送方和接收方必須都準(zhǔn)備好了嗎?這意味著接收方將等待,直到發(fā)送方準(zhǔn)備好。我們可以說,接收是阻斷的,意思是接收方將阻斷其他代碼的運行,直到它收到東西。讓我們用這個巧妙的技巧來同步我們的goroutines。
func main() { basicSyncing() } func basicSyncing() { done := make(chan struct{}) go func() { for i := 0; i < 5; i++ { fmt.Printf("%s worker %d start\n", fmt.Sprint(time.Now()), i) time.Sleep(time.Duration(rand.Intn(5)) * time.Second) } close(done) }() <-done fmt.Println("exiting...") }
我們做了一個done通道,負(fù)責(zé)阻斷代碼,直到goroutine完成。done可以是任何類型,但struct{}經(jīng)常被用于這些類型的通道。它的目的不是為了傳輸結(jié)構(gòu),所以它的類型并不重要。
一旦工作完成,worker goroutine 將關(guān)閉 done。此時,我們可以從 done 中接收,它將是一個空結(jié)構(gòu)。接收動作解除了代碼的阻塞,使其可以退出。
這就是我們使用通道等待goroutine完成的方式。
總結(jié)
并發(fā)可能看起來是一個令人生畏的話題。我當(dāng)然認(rèn)為是這樣的。然而,在了解了基礎(chǔ)知識之后,我認(rèn)為實現(xiàn)起來真的很美。希望你們能從這個教程中有所收獲我們僅僅是觸及了表面,Go為我們提供的東西還有很多。下一次我們將在更多的并發(fā)性教程中見面。再見!
到此這篇關(guān)于一文帶你了解Golang中的并發(fā)性的文章就介紹到這了,更多相關(guān)Golang并發(fā)性內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang對sqlite3數(shù)據(jù)庫進行操作實踐記錄
sqlite是嵌入式關(guān)系型數(shù)據(jù)庫引擎,官方描述為自包含的、無服務(wù)的、零配置并支持事務(wù)的關(guān)系型數(shù)據(jù)庫引擎,下面這篇文章主要給大家介紹了關(guān)于Golang對sqlite3數(shù)據(jù)庫進行操作的相關(guān)資料,需要的朋友可以參考下2024-03-03搭建Go語言的ORM框架Gorm的具體步驟(從Java到go)
很多朋友不知道如何使用Goland軟件,搭建一個ORM框架GORM,今天小編給大家分享一篇教程關(guān)于搭建Go語言的ORM框架Gorm的具體步驟(從Java到go),感興趣的朋友跟隨小編一起學(xué)習(xí)下吧2022-09-09