在寫該文之前一直猶豫,是把Go的并發寫的面面俱到顯得高大尚一些,還是簡潔易懂一些?今天看到一個新員工在學習Java,突然間想起第一次接觸Java的并發時,被作者搞了一個雲裡霧裡,直到現在還有陰影,是以決定本文從簡。哈哈,說笑了,言歸正傳。
Go的并發真的很簡單,是以本文不羅嗦程序、線程、協程、信号量、鎖、排程、時間片等亂七八糟的東西,因為這些不影響您了解Go的并發。先看一個小例子:
package main
import "fmt"
func Add(i, j int) {
sum := i + j
fmt.Println(i, " + ", j, " = ", sum)
}
func main() {
for i := 0; i < 10; i++ {
Add(i, i)
}
這個例子很簡單吧,說白了就是計算0+0、1+1、2+2、3+3、.......、9+9之和,并列印出來,運作結果顯而易見:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiInBnauczMxUkU281T5c2MCFUQaZTT0lXdiREeXFTbvl2S39CX4EzLcBzNvwFMw00LcJDMzZWe39CXt92Yu8GdjFTNuMzcvw1LcpDc0RHaiojIsJye.jpg)
這裡沒有使用并發呀,好吧,為了提升計算效率,在main的for循環中使用并發,把代碼修改如下:
go Add(i, i)
沒有花眼吧,前面加了一個go,這就并發了?
嗯,這就并發了。
在方法Add()之前增加了一個關鍵字go,相當于告訴Go編譯器啟動一個goroutine,然後把Add()方法放到goroutine中執行。
什麼是goroutine?
有人把它翻譯為協程,說實話挺反感的,有些單詞還是不要翻譯為好,比如Context,經常寫Web程式的人會遇到,有人把它翻譯為上下文;再如payload,經常做滲透的人會使用,怎麼翻譯好呢?還是不翻譯了吧。
該怎麼了解goroutine?
如上圖所示,main()方法所在的goroutine上又建立了10個goroutine,每個goroutine各自跑一個Add()方法
OK,運作一下該程式,結果如下:
咦,怎麼都沒有,說好的運作結果呢?
這是因為main()所在的goroutine建立10個goroutine後,它裡面的邏輯已執行完,那麼main()就退出了,它根本就不管這10個goroutine的死活。從結果也能看出,當main()退出時,這10個goroutine沒有一個執行完,是以結果什麼都沒有列印。
如何解決這個問題呢?
一種比較容易想到的但又比較垃圾的解決辦法是:“讓main()等一會兒”,下面我們修改一個這個程式
import (
"fmt"
"time"
)
go Add(i, i)
time.Sleep(time.Second * 3) // main()所在goroutine休息3秒鐘
首先引入"time"這個包,然後調用time.Sleep()方法,讓main()所在goroutine等3s,等其它10個goroutine都運作完,這種解決辦法就是“馬兒你慢些跑呀慢些跑” :)
運作一下結果:
可能您會問,為何要等3秒鐘而不是2秒鐘?
我隻能學着印度老外,一邊搖頭一邊微笑地告訴您,我是蒙的,因為我也不知道确切地等多長時間,是以是一種垃圾的解決辦法。
那有沒有一種通知機制呢?當一個goroutine執行完畢後,就告訴主goroutine(即main()方法所在的goroutine):“嘿,哥們,我執行完了,你想幹嘛就幹嘛吧!”
有,這就是Go語言的亮點,十分耀眼的一個亮點:channel
什麼是channel?
說白了就是一個通道(建議還是不翻譯的為好),一個goroutine執行完畢後,就告訴主goroutine,I'm over!怎麼告訴呢?就是向channel中寫一個資料。
怎麼向channel中寫一個資料呢?
OK,follow me,要想寫一個資料到channel則必須有一個channel不是?是以:
(1)建立一個channel
【備注】:所謂channel也是一種Go的類型,與int、float64、string、bool、struct、slice、map等同等地位
var ch chan int // 聲明一個變量為ch,它的類型為chan類型,這個channel裡面可以存放int型的值
ch = make(chan int) // 使用make關鍵字初始一個長度為0的通道
當然聲明和初始化可以一塊來
var ch chan int = make(chan int)
(2)向channel中寫一個資料
ch <- 1
就這麼簡單,使用符号”<-“,前面聲明了channel的類型為int ,是以就把1寫入ch;若聲明channel類型為bool,就可以把布爾值寫入channel,即ch <- true
(3)從channel中讀資料
<- ch
嗯,還是這麼簡單
無論寫還是讀都是用符号”<-“,就看<-後面是誰
OK,既然知道有channel這東東了,我們修改一下上面的程式:
"fmt"
// "time" // 删除掉time包
var ch chan int = make(chan int) // 初始化一個類型為channel的變量ch,其中channel裡面放int型資料
ch <- i // 這個方法執行完,意味着方法所屬的goroutine即将退出,就告訴主goroutine,I'm完事了
/*
* 由于有10個goroutine,是以從channel中讀10次
* 這10個goroutine都告訴主gorouinte說完事了,那麼主gorouinte也就退出了
*/
fmt.Println("i=", <-ch)
// time.Sleep(time.Second * 3) // 不用這種機制了,留着也沒有用
運作結果如下:
目的達到了,我好人做到底,再解釋一下:
可以這樣了解,有10個廚師1個端菜工,這10個廚師各自做各自的菜,做完之後就放到channel,這個端菜工就從channel中取菜。當channel中沒有菜時,端菜工就一直等待直到有菜為止;廚師做好一個菜後,發現channel中沒有菜,就把自己的菜放到channel中,若發現channel在有菜還沒有端正,廚師就拿着自己的菜一直等到channel中的菜被端菜工端走後再把自己的菜放進去。
可能您又要說了,這種channel很類似同步操作,這個channel隻能放一個菜,端菜工端一個菜;channel中沒有菜端菜工等待;channel中有菜廚師等待。效率不高呀。
好吧,Go設計師已提前為您想好處理辦法了,即這個channel可以放10個菜,隻要這個channel還沒有放滿10個菜,廚師就可以向上面放,這樣廚師就不用等待了。剩下的就是端菜工要提高自己的工作效率了。
怎麼放10個菜?
var ch chan int = make(chan int,10)
OK,搞定!代碼如下:
// "time"
var ch chan int = make(chan int, 10)
ch <- i
go Add(i, i)
fmt.Println("i=", <-ch)
// time.Sleep(time.Second * 3)
仔細看,再仔細看,看出什麼東西來了沒有?若沒有,請與上一個運作結果對比着看 :)
還是沒有看來?
沒有發現這兩個基本上是一模一樣的嗎?除了運作程式所花的時間不同之外!
從運作時間上來看,好像效率提升了一些,這是因為廚師不用等待了,一旦指明了channel的容量,相對于廚師來說就變成異步的了;沒有指明channel容量,相對于廚師來說就是同步的。
同步異步不是我想讓您觀察的重點,您難道沒有發現0+0、1+1、2+2、3+3、......、9+9,這個順序太正常了嗎?若真正并發的話,這個順序肯定是亂的!
看一下我電腦資訊:
好呆CPU也是四核的,并發的順序是這麼的正常,太不可思議了 :)
好吧,我再解開這謎團吧
我用的Go版本是1.4,可以在指令視窗中執行go version檢視。由于Go語言1.4版本對多核的處理還沒有做太多的改進,據說1.5版本有突破,後面可以關注一下,是以在這個版本還是使用的一個核。對于單核CPU程序、線程在執行的過程中,系統會把運作的CPU強行切給另一個程序或線程。
有人如果看過其他人的部落格、書,都會把goroutine翻譯為協程,所謂協程就是使用者态的線程,可以這樣了解:”一個協程在執行時,系統不會強行切換時間片“。即一個goroutine在執行的過程中,Go語言會讓這個goroutine瘋狂地執行,直到它運作完為止,再讓另外一個gorouinte運作,是以從結果來看運作順序是固定的。
如果利用多核?
Go語言也提供了一種方式,具體代碼如下:
// "time"
"runtime" // 引入runtime包
runtime.GOMAXPROCS(runtime.NumCPU()) // 讓Go使用多個核
多運作幾次,結果如下:
上面就是Go的并發核心内容,當然還有關于并發的其它内容,如單向寫channel、單向讀channel、傳統并發方式等内容。本文就先寫到這裡,内容多了不容易消化 :)