Go(又稱Golang)是Google開發(fā)的一種靜態(tài)強類型、編譯型、并發(fā)型,并具有垃圾回收功能的編程語言。Go于2009年正式推出,國內(nèi)各大互聯(lián)網(wǎng)公司都有使用,尤其是七牛云,基本都是golang寫的,
傳聞Go是為并發(fā)而生的語言,運行速度僅比c c++慢一點,內(nèi)置協(xié)程(輕量級的線程),說白了協(xié)程還是運行在一個線程上,由調(diào)度器來調(diào)度線程該運行哪個協(xié)程,也就是類似于模擬了一個操作系統(tǒng)調(diào)度線程,我們也知道,其實多線程說白了也是輪流占用cpu,其實還是順序執(zhí)行的,協(xié)程也是一樣,他也是輪流獲取執(zhí)行機會,只不過他獲取的是線程,但是如果cpu是多核的話,多線程就能真正意義上的實現(xiàn)并發(fā)同時,如果GO執(zhí)行過程中有多個線程的話,協(xié)程也能實現(xiàn)真正意義上的并發(fā)執(zhí)行,所以,最理想的情況,根據(jù)cpu核數(shù)開辟對應(yīng)數(shù)量的線程,通過這些線程,來為協(xié)程提供執(zhí)行環(huán)境
當(dāng)我們在開發(fā)網(wǎng)絡(luò)應(yīng)用程序時,遇到的瓶頸總是在io上,由此出現(xiàn)了多進(jìn)程,多線程,異步io的解決方案,其中異步io最為優(yōu)秀,因為他們在不占用過多的資源情況下完成高性能io操作,但是異步io會導(dǎo)致一個問題,那就是回調(diào)地獄,node js之前深受詬病的地方就在于此,后來出現(xiàn)了async await這種方案,真正的實現(xiàn)了同步式的寫異步,其實Go的協(xié)程也是這樣,有人把goroutine叫做纖程,認(rèn)為node js的async await才是真正的協(xié)程,對此我不做評價,關(guān)于goroutine的運行機制本文不講,大家可以看這篇博文,講的很生動,本文主要對goroutine的使用進(jìn)行講解,如果大家熟悉node js的async await或者c#的async(其實node js就是學(xué)習(xí)的c#的async await),可以來對比一下兩者在使用上的不同,從而對協(xié)程纖程的概念產(chǎn)生進(jìn)一步的了解
在golang中開辟一個協(xié)程非常簡單,只需要一個go關(guān)鍵字
package main
import (
"fmt"
"time"
)
func main(){
for i := 0;i<10;i++{
go func(i int){
for{
fmt.Printf("%d",i);
}
}(i)
}
time.Sleep(time.Millisecond);
}
打印結(jié)果
5551600088800499999991117777777742222220000044444444888888888999
9666665111177777777777777777777777777333333333333333399999999999
999999999999999999999999999999444442224444444488888888222222222
20888886666666655555555555444011111111111111000000000999999555555
5554444444000077777666666311111197777778888222277777753333444444
9999997777772222000077774444444444444444444
可以看到,完全是隨機的,打印哪個取決于調(diào)度器對協(xié)程的調(diào)度,
goroutine相比于線程,有個特點,那就是非搶占式,如果一個協(xié)程占據(jù)了線程,不主動釋放或者沒有發(fā)生阻塞的話,那么永遠(yuǎn)不會交出線程的控制權(quán),我們舉個例子來驗證下
package main
import (
"time"
)
func main(){
for i := 0;i<10;i++{
go func(i int){
for{
i++
}
}(i)
}
time.Sleep(time.Millisecond);
}
這段程序在執(zhí)行后,永遠(yuǎn)不會退出,并且占滿了cpu,原因就是goroutine中,一直在執(zhí)行i++,沒有釋放,而一直占用線程,當(dāng)四個線程占滿之后,其他的所有g(shù)oroutine都沒有執(zhí)行的機會了,所以本該一秒鐘后就退出的程序一直沒有退出,cpu滿載再跑,但是為什么前面例子的Printf沒有發(fā)生這種情況呢?是因為Printf其實是個io操作,io操作會阻塞,阻塞的時候goroutine就會自動的釋放出對線程的占有,所以其他的goroutine才有執(zhí)行的機會,除了io阻塞,golang還提供了一個api,讓我們可以手動交出控制權(quán),那就是Gosched(),當(dāng)我們調(diào)用這個方法時,goroutine就會主動釋放出對線程的控制權(quán)
package main
import (
"time"
"runtime"
)
func main(){
for i := 0;i<10;i++{
go func(i int){
for{
i++;
runtime.Gosched();
}
}(i)
}
time.Sleep(time.Millisecond);
}
修改之后,一秒鐘之后,代碼正常退出
常見的觸發(fā)goroutine切換,有一下幾種情況
1、I/O,select
2、channel
3、等待鎖
4、函數(shù)調(diào)用(是一個切換的機會,是否會切換由調(diào)度器決定)
5、runtime.Gosched()
說完了goroutine的基本用法,接下來我們說一下goroutine之間的通信,Go中通信的理念是“不要通過共享數(shù)據(jù)來通信,而是通過通信來共享數(shù)據(jù)“,Go中實現(xiàn)通信主要通過channel,它類似于unix shell中的雙向管道,可以接受和發(fā)送數(shù)據(jù),
我們來看個例子,
package main
import(
"fmt"
"time"
)
func main(){
c := make(chan int)
go func(){
for{
n := <-c;
fmt.Printf("%d",n)
}
}()
c <- 1;
c <- 2;
time.Sleep(time.Millisecond);
}
打印結(jié)果為12,我們通過make來創(chuàng)建channel類型,并指明存放的數(shù)據(jù)類型,通過 <-來接收和發(fā)送數(shù)據(jù),c <- 1為向channel c發(fā)送數(shù)據(jù)1,n := <-c;表示從channel c接收數(shù)據(jù),默認(rèn)情況下,發(fā)送數(shù)據(jù)和接收數(shù)據(jù)都是阻塞的,這很容易讓我們寫出同步的代碼,因為阻塞,所以會很容易發(fā)生goroutine的切換,并且,數(shù)據(jù)被發(fā)送后一定要被接收,不然會一直阻塞下去,程序會報錯退出,
本例中,首先向c發(fā)送數(shù)據(jù)1,main goroutine阻塞,執(zhí)行開辟的協(xié)程,從而讀到數(shù)據(jù),打印數(shù)據(jù),然后main協(xié)程阻塞完成,向c發(fā)送第二個數(shù)據(jù)2,開辟的協(xié)程還在阻塞讀取數(shù)據(jù),成功讀取到數(shù)據(jù)2時,打印2,一秒鐘后,主函數(shù)退出,所有g(shù)oroutine銷毀,程序退出
我們仔細(xì)看這份代碼,其實有個問題,在開辟的goroutine中,我們一直再循環(huán)阻塞的讀取c中的數(shù)據(jù),并不知道c什么時候?qū)懭胪瓿?,不再寫入,如果c不再寫入我們完全可以銷毀這個goroutine,不必占有資源,通過close api我們可以完成這一任務(wù),
package main
import (
"fmt"
"time"
)
func main(){
c := make(chan int);
go func(){
for{
p,ok := <-c;
if(!ok){
fmt.Printf("jieshu");
return
}
fmt.Printf("%d",p);
}
}()
for i := 0;i<10;i++{
c<-i
}
close(c);
}
當(dāng)我們對channel寫入完成后,可以調(diào)用close方法來顯式的告訴接收方對channel的寫入已經(jīng)完畢,這是,在接收的時候我們可以根據(jù)接收的第二個值,一個boolean值來判斷是否完成寫入,如果為false的話,表示此channel已經(jīng)關(guān)閉,我們沒有必要繼續(xù)對channel進(jìn)行阻塞的讀,
除了判斷第二個boolean參數(shù),go還提供了range來對channel進(jìn)行循環(huán)讀取,當(dāng)channel被關(guān)閉時就會退出循環(huán),
package main
import (
"fmt"
"time"
)
func main(){
c := make(chan int);
go func(){
// for{
// p,ok := <-c;
// if(!ok){
// fmt.Printf("jieshu");
// return
// }
for p := range c{
fmt.Printf("%d",p);
}
fmt.Printf("jieshu");
// }
}()
for i := 0;i<10;i++{
c<-i
}
close(c);
time.Sleep(time.Millisecond);
}
兩種方式打印的都是123456789jieshu
另外,通過Buffered Channels我們可以創(chuàng)建帶緩存的channel,使用方法為創(chuàng)建channel時傳入第二個參數(shù),指明緩存的數(shù)量,
package main
import "fmt"
func main() {
c := make(chan int, 2)//修改2為1就報錯,修改2為3可以正常運行
c <- 1
c <- 2
fmt.Println(<-c)
fmt.Println(<-c)
}
例子中,我們創(chuàng)建channel時,傳入?yún)?shù)2,便可以存儲兩個兩個數(shù)據(jù),前兩個數(shù)據(jù)的寫入可以無阻塞的,不需要等待數(shù)據(jù)被讀出,如果我們連續(xù)寫入三個數(shù)據(jù),就會報錯,阻塞在第三個數(shù)據(jù)的寫入出無法進(jìn)行下一步
最后,我們說一下select,這個和操作系統(tǒng)io模型中的select很像,先執(zhí)行先到達(dá)的channel我們看個例子
package main
import (
"fmt"
"time"
)
func main(){
c := make(chan int);
c2:= make(chan int);
go func(){
for{
select{
case p := <- c : fmt.Printf("c:%d\n",p);
case p2:= <- c2: fmt.Printf("c2:%d\n",p2);
}
}
}()
for i :=0;i<10;i++{
go func(i int){
c <- i
}(i)
go func (i int){
c2 <-i
}(i)
}
time.Sleep(5*time.Millisecond);
}
打印結(jié)果為
c:0
c2:1
c:1
c:2
c2:0
c:3
c:4
c:5
c:7
c2:2
c:6
c:8
c:9
c2:3
c2:5
c2:4
c2:6
c2:7
c2:8
c2:9
可以看到,c和c2的接收完全是隨機的,誰先接收到執(zhí)行誰的回調(diào),當(dāng)然這不僅限于接收,發(fā)送數(shù)據(jù)時也可以使用select函數(shù),另外,和switch語句一樣,golang中的select函數(shù)也支持設(shè)置default,當(dāng)沒有接收到值的時候就會執(zhí)行default回調(diào),如果沒有設(shè)置default,就會阻塞在select函數(shù)處,直到某一個發(fā)送或者接收完成。
golang中 goroutine的基本使用就是這些,大家可以根據(jù)上面goroutine運行機制的文章和本文一起來體會golang的運行過程。
補充一個runtime包的幾個處理函數(shù)
- Goexit
退出當(dāng)前執(zhí)行的goroutine,但是defer函數(shù)還會繼續(xù)調(diào)用 - Gosched
讓出當(dāng)前goroutine的執(zhí)行權(quán)限,調(diào)度器安排其他等待的任務(wù)運行,并在下次某個時候從該位置恢復(fù)執(zhí)行。 - NumCPU
返回 CPU 核數(shù)量 - NumGoroutine
返回正在執(zhí)行和排隊的任務(wù)總數(shù) - GOMAXPROCS
用來設(shè)置可以并行計算的CPU核數(shù)的最大值,并返回之前的值。