Golang基礎第四篇——從go併發到channel到定時器實現

目錄

一,Golang的併發編程

二,關於goroutine

三,關於channel

四,Golang中的定時器


一,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語言編程-許式偉

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章