1. 程式人生 > >[轉帖]go 的goroutine 以及 channel 的簡介.

[轉帖]go 的goroutine 以及 channel 的簡介.

程序,執行緒的概念在作業系統的書上已經有詳細的介紹。程序是記憶體資源管理和cpu排程的執行單元。為了有效利用多核處理器的優勢,將程序進一步細分,允許一個程序裡存在多個執行緒,這多個執行緒還是共享同一片記憶體空間,但cpu排程的最小單元變成了執行緒。
那協程又是什麼東西,以及與執行緒的差異性??

協程,可以看作是輕量級的執行緒。但與執行緒不同的是,執行緒的切換是由作業系統控制的,而協程的切換則是由使用者控制的。

最早支援協程的程式語言應該是lisp方言scheme裡的continuation(續延),續延允許scheme儲存任意函式呼叫的現場,儲存起來並重新執行。Lua,C#,python等語言也有自己的協程實現。

go的goroutinue
今天要講的goroutinue,本質上就是協程。但有兩點不同:
1. goroutinue可以實現並行,也就是說,多個協程可以在多個處理器同時跑。而協程同一時刻只能在一個處理器上跑(把宿主語言想象成單執行緒的就好了)。
2. goroutine之間的通訊是通過channel,而協程的通訊是通過yield和resume()操作。

 

在Go裡實現goroutine非常簡單,只需要在函式的呼叫前面加關鍵字go即可,


go doSth()

下面的例子演示,啟動10個goroutines分別列印索引。


package main
import (
"fmt"
"time"
)

func main() {
for i:=1;i<10;i++ {
go func(i int) {
fmt.Println(i)
}(i)
}
//暫停一會,保證列印全部結束
time.Sleep(1e9)
}

在分析goroutine執行的隨機性和併發性,把goroutine看作是java的守護執行緒是完全可以的。上面的例子中,啟動了10個goroutine,再加上main函式的主goroutine,總共有11個goroutines。由於goroutine類似於”守護執行緒“,如果主goroutine不等待片刻,可能程式就沒有輸出列印了。上面的例子輸出如下:(輸出的索引是完全隨機的)

 

go的channel
在java的世界裡,併發主要是靠鎖住臨界資源(共享記憶體)來保證同步的。而channel則是goroutinues之間進行通訊的利器。

channel可以形象比喻為工廠裡的傳送帶,一頭的生產者goroutine往傳輸帶放東西,另一頭的消費者goroutinue則從輸送帶取東西。channel實際上是一個有型別的訊息佇列,遵循先進先出的特點。

1. channel的操作符號

ch <- ele 表示ele被髮送給channel ch;

ele2 <- ch 表示從channel ch取一個值,然後賦給ele2

2. 阻塞式channel

channel預設是沒有緩衝區的,也就是說,通訊是阻塞的。send操作必須等到有消費者accept才算完成。

舉個栗子


package main
import "fmt"

func main() {
ch1 := make(chan int)
go pump(ch1) // pump hangs
fmt.Println(<-ch1) // prints only 0
}

func pump(ch chan int) {
for i:= 0; ; i++ {
ch <- i
}
}

上面程式碼pump()裡的channel在接受到第一個元素後就被阻塞了,直到主goroutinue拿走了資料。最終channel阻塞在接受第二個元素,程式只打印 0

3 帶有buff的channel

沒有buff的channel只能容納一個元素,而帶有buff的channel則可以非阻塞容納N個元素。傳送資料到buffed channel不會被阻塞,除非channel已滿;同樣的,從buffed channel取資料也不會被阻塞,除非channel空了。這有點像java的ConcurrentLinkedQueue。

 

goroutine和channel的應用
結合goroutine和channel,可以模擬出java處理併發情況的若干情景

1. 實現future


package main
import "fmt"
import "time"

func main() {
future := heavyCalculation()
fmt.Println(<-future)
}

func heavyCalculation() (chan int) {

future := make(chan int)
go func() {
//模擬耗時計算
time.Sleep(1e9)
future <- 666
}()

return future
}

2. 實現CountDownLatch


package main
import "fmt"

func main() {
nTask := 5
ch := make(chan int)
for i:=1;i<=nTask;i++ {
go doTask(ch)
}
for i:=1;i<=nTask;i++ {
<-ch
}
fmt.Println("finished all tasks")
}

func doTask(ch chan<- int) {
//doSth...
ch<- 0
}

 

3. 併發訪問物件

Hashtable是執行緒安全的,意味著多條執行緒同時操作hashtable物件是不會引起狀態不一致的。檢視Hashtable原始碼可知,其幾乎全部方法都新增synchronized關鍵字,例如put(),remove()操作。在go裡,我們可以在物件內部儲存一個函式型別的channel,涉及物件狀態的操作都放入channel裡,物件初始化的時候開啟一條goroutinue,不停地執行匿名函式。

package main
import (
"fmt"
"strconv"
"time"
)

type Person struct {
Name string
salary float64
chF chan func()
}
func NewPerson(name string, salary float64) *Person {
p := &Person{name, salary, make(chan func())}
go p.backend()
return p
}
func (p *Person) backend() {
for f := range p.chF {
f()
}
}

func (p *Person) AddSalary(sal float64) {
p.chF <- func() { p.salary += sal } // (ThreadSafe)

// p.salary += sal (NotThreadSafe)
}

func (p *Person) ReduceSalary(sal float64) {
p.chF <- func() { p.salary -= sal } // (ThreadSafe)

// p.salary -= sal (NotThreadSafe)
}

func (p *Person) Salary() float64 {
fChan := make(chan float64)
p.chF <- func() { fChan <- p.salary }
return <-fChan
}
func (p *Person) String() string {
return p.Name + " - salary is: " +
strconv.FormatFloat(p.Salary(), 'f', 2, 64)
}

func main() {
p := NewPerson("Kingston", 8888.8)
fmt.Println(p)
for i:=1;i<=500;i++ {
go func() {
p.AddSalary(1);
}()
}
for i:=1;i<=500;i++ {
go func() {
p.ReduceSalary(1);
}()
}
time.Sleep(3e9)
fmt.Println("After changed:")
fmt.Println(p)
}


4. 生產者消費者模式

每次涉及到併發情景,都喜歡用生產者消費者模式,因為它太經典啦

2個麵包師同時生產麵包,5個顧客同時取麵包(儘管以下例子的列印不能說明併發的真實情況,因為channel的操作和列印的組合不是原子操作,但不影響程式的邏輯)

package main
import (
"fmt"
"time"
)


func main() {
bread := make(chan int,3)
for i:=1;i<=2;i++ {
go produce(bread)
}
for i:=1;i<=5;i++ {
go consume(bread)
}
time.Sleep(1e9)
}

func produce(ch chan<- int) {
for {
ch <- 1
fmt.Println("produce bread")
time.Sleep(100 * time.Millisecond)
}
}

func consume(ch <-chan int) {
for {
<-ch
fmt.Println("take bread")
time.Sleep(200 * time.Millisecond)
}
}


---------------------
作者:littleschemer
來源:CSDN
原文:https://blog.csdn.net/littleschemer/article/details/70232659
版權宣告:本文為博主原創文章,轉載請附上博文連結!