目錄
一,Golang的併發編程
在第一篇我們已經提到,golang最重要的一個特色就是他通過go關鍵字的併發處理。
首先要知道爲什麼我們需要併發,併發爲什麼在如今作爲一種語言特性如此重要?
關於進程線程,堆棧的基礎知識就不贅述(感興趣可以翻我之前的blog),這裏從市場發展需求方面說:1. 一方面, 由於應用程序的迅速增加刺激了用戶對於網絡產品的依賴,我們既要處理靈敏相應的圖形用戶界面,又要執行IO和運算,傳統串行程序肯定會導致IO阻塞,用戶使用體驗變差,最後被市場淘汰。 2. 另一方面,事務越來越分配到分佈式的環境上,這導致相同的工作單元在不同計算機上處理分片數據,必然要通過線程的切換達到分佈式的高性能運轉,此時併發成爲剛需。
關於併發的實現,
從操作系統的實現模型上發展由多進程->多線程->基於回調的非阻塞/異步和協程機制
關於異步框架已經經受過市場的檢驗,可以減小消耗,不過編程難度要比多線程大;
協程是用戶級線程,開銷極小,編程簡單結構清晰,不過需要語言的支持(支持的語言很少,比如lua,C#)
golang正是利用在語言級別實現輕量級線程(goroutine),避開java繁瑣的多線程框架,成爲國內多家公司的當紅語言,可以想像簡單的多線程處理方式會對服務器方的編寫節省多少資源。
二,關於goroutine
我這裏講goroutine就是golang支持的協程
協程的實現:協程是基於線程的。內部實現上,維護了一組數據結構和 n 個線程,真正的執行還是線程,協程執行的代碼被扔進一個待執行隊列中,由這 n 個線程從隊列中拉出來執行。golang 利用並封裝了操作系統的異步函數。包括 linux 的 epoll、select 和 windows 的 iocp、event 等,當這些異步函數返回 busy 或 blocking 時,golang 利用這個時機將現有的執行序列壓棧,讓線程去拉另外一個協程的代碼來執行。簡單來說,一個線程可以利用隊列擁有多個串行執行的協程
協程的特性:在任務調度上,協程是弱於線程的。但是在資源消耗上,協程則是極低的。一個線程的內存在 MB 級別,而協程只需要 KB 級別。而且線程的調度需要內核態與用戶的頻繁切入切出,資源消耗也不小。這就導致線程和進程最多可以創建不到1萬個,而協程可以輕鬆創建幾百萬個也不會令系統資源枯竭。
當我們想調用goroutine的時候,使用go關鍵字就可以,在一個函數調用前加上go關鍵字,這次調用就會在一個新的goroutine中併發執行。當被調用的函數返回時,這個goroutine也自動結束。
然而併發的難度在於協調,而不是調用就行,我們要使用goroutine就需要有兩種最常見的併發通信模型:共享內存 和 消息。
共享數據是指多個併發單元分別保持對同一個數據的引用,實現數據的共享,在實際工程中最常見的就是共享內存了,通過頻繁的加鎖釋放 達到資源的有效調用。在這裏golang提供了另一種通信模型解決這個問題,即以消息機制而非共享內存作爲通信方式。
消息機制即每個併發單元是自包含的獨立的個體,並且都有自己的變量,變量互相不共享,每個單元相互獨立通過消息(輸入輸出)來達成一致,實現消息共享。golang提供的消息機制就是channel
三,關於channel
學會channel,你就搞懂了golang的併發編程。
channel可以理解成類型安全的unix管道,具體實現見$GOROOT/src/runtime/chan.go,或者轉http://litang.me/post/golang-channel/ 由讀隊列,寫隊列,緩存隊列構成
這裏講簡單實用 給下一節做鋪墊,還是拿例子做實戰,充分調用你之前學過語言的支持,結合編譯原理做理解:
//channel.go
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, 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)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // 從通道 c 中接收
fmt.Println(x, y, x+y)
}
其中channel可用於兩個 goroutine 之間通過傳遞一個指定類型的值來同步運行和通訊。操作符 <-
用於指定通道的方向,發送或接收。如果未指定方向,則爲雙向通道。
main函數是起點,s聲明瞭一個包含6個整數的數組,c是一個channel,分別通過go執行goroutine讀前半數組的sum和後半數組的sum,然後把在函數裏讀到的數據寫到x和y上,打印結果爲-5,17,12,在x和y賦值時,主進程會因爲等待兩個goroutine的返回結果而等待,我們使用channel在這裏也避免了加鎖的問題。
另外一個例子:
//channel2.main
package main
import "fmt"
func Count(ch chan int) {
ch <- 1
fmt.Println("Counting")
}
func main() {
chs := make([]chan int, 6)
for i := 0; i < 6; i++ {
chs[i] = make(chan int)
go Count(chs[i])
}
for _, ch := range(chs) {
<- ch
}
}
這裏除了channel數組的使用,請大家注意在main函數中channel寫出數據是沒有接應的,也就是說,在循環中的channel們會在被讀前加鎖,但一旦讀取主進程會立即執行,所以上邊的函數調用結果會輸出5個Counting,而不是6個。
四,Golang中的定時器
golang在src包中的timer有定義定時器的結構(或者ticks)可以根據timer.go去直接拿去使用,下面節選講解一下結構:
timer的結構裏一般有interval(時間間隔),一把互斥鎖mu,一個判斷是否運行的bool變量,和我們自定義的消息管道
type Timer struct {
interval sync2.AtomicDuration
// state management
mu sync.Mutex
running bool
// msg is used for out-of-band messages
msg chan typeAction
}
start函數執行時,首先加鎖,然後判斷timer的運行狀態,如果還沒啓動timer,就把狀態設置爲true,然後通過goroutine執行keephouse函數實體,整個函數執行完去掉資源的互斥鎖
// Start starts the timer.
func (tm *Timer) Start(keephouse func()) {
tm.mu.Lock()
defer tm.mu.Unlock()
if tm.running {
return
}
tm.running = true
go tm.run(keephouse)
}
上邊start函數的管道去執行下邊的run函數:goroutine不斷循環寫進管道時間信息並和interval做判斷,當channel被寫進消息執行判斷action或者成爲nil輸出時,帶着執行keephouse跳出循環(這裏golang的select函數需要注意)
func (tm *Timer) run(keephouse func()) {
for {
var ch <-chan time.Time
interval := tm.interval.Get()
if interval <= 0 {
ch = nil
} else {
ch = time.After(interval)
}
select {
case action := <-tm.msg:
switch action {
case timerStop:
return
case timerReset:
continue
}
case <-ch:
}
keephouse()
}
}
觸發函數將觸發消息直接寫入管道中,上邊的run函數我們知道,當管道收到消息時會立即執行keephouse,所以trigger函數會先執行然後重置(除了寫進channel timerstop消息會返回,否則run循環會一直持續下去)
// Trigger will cause the timer to immediately execute the keephouse function.
// It will then cause the timer to restart the wait.
func (tm *Timer) Trigger() {
tm.mu.Lock()
defer tm.mu.Unlock()
if tm.running {
tm.msg <- timerTrigger
}
}
// TriggerAfter waits for the specified duration and triggers the next event.
func (tm *Timer) TriggerAfter(duration time.Duration) {
go func() {
time.Sleep(duration)
tm.Trigger()
}()
}
stop函數就很簡單了,向channel裏寫進timerstop消息,然後更新running狀態,便於timer可以重新開啓
// Stop will stop the timer. It guarantees that the timer will not execute
// any more calls to keephouse once it has returned.
func (tm *Timer) Stop() {
tm.mu.Lock()
defer tm.mu.Unlock()
if tm.running {
tm.msg <- timerStop
tm.running = false
}
}
瞭解這個之後我們就可以使用timer,下面這個是使用golang原生ticker的demo可以對比實現一下:每5秒報一次數
import (
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(time.Second * 5)
i := 0
for {
<-ticker.C
i++
fmt.Println("i=", i)
if i == 5 {
ticker.Stop()
break
}
}
}
參考資料:go語言編程-許式偉