協(xié)程张症,通道
- 我們在普通程序中要執(zhí)行代碼如下代碼
func main() {
foo()
bar()
}
func foo() {
for i := 0; i < 45; i++ {
fmt.Println("Foo:", i)
}
}
func bar() {
for i := 0; i < 45; i++ {
fmt.Println("Bar:", i)
}
}
得到結(jié)果
Foo: 41
Foo: 42
Foo: 43
Foo: 44
Bar: 0
Bar: 1
Bar: 2
Bar: 3
這個是按順序執(zhí)行的
- 錯誤使用協(xié)程
package main
import "fmt"
func main() {
go foo()
go bar()
}
func foo() {
for i := 0; i < 45; i++ {
fmt.Println("Foo:", i)
}
}
func bar() {
for i := 0; i < 45; i++ {
fmt.Println("Bar:", i)
}
}
由于沒有調(diào)度,主協(xié)程率先執(zhí)行完畢鸵贬,代碼執(zhí)行已經(jīng)關(guān)閉俗他,所以協(xié)程根本沒有執(zhí)行。
- 正確使用協(xié)程
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {
wg.Add(2)
go foo()
go bar()
wg.Wait()
}
func foo() {
for i := 0; i < 45; i++ {
fmt.Println("Foo:", i)
}
wg.Done()
}
func bar() {
for i := 0; i < 45; i++ {
fmt.Println("Bar:", i)
}
wg.Done()
}
用sync.WaitGroup阔逼,可以阻塞主線程兆衅,wg.Add(2)加入兩個子線程,wg.Wait()一直等待嗜浮,每當一個執(zhí)行完成調(diào)用函數(shù)wg.Done()將一個線程去掉羡亩,直到全部執(zhí)行完子線程∥H冢‘’
協(xié)程:正如官方所言畏铆,goroutine 是一個輕量級的執(zhí)行單元,相比線程開銷更小吉殃,完全由 Go 語言負責調(diào)度辞居,是 Go 支持并發(fā)的核
心。開啟一個 goroutine 非常簡單:協(xié)程的運行
package main
import (
"fmt"
"time"
)
func main() {
go fmt.Println("goroutine message")
time.Sleep(1) //1
fmt.Println("main function message")
}
- 協(xié)程的好處蛋勺,上面的例子我們沒有看到協(xié)程究竟能有什么好處
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func main() {
wg.Add(3)
go foo()
go bar()
go dar()
wg.Wait()
}
func foo() {
for i := 0; i < 45; i++ {
fmt.Println("Foo:", i)
time.Sleep(3 * time.Millisecond)
}
wg.Done()
}
func bar() {
for i := 0; i < 45; i++ {
fmt.Println("Bar:", i)
time.Sleep(20 * time.Millisecond)
}
wg.Done()
}
func dar() {
for i := 0; i < 45; i++ {
fmt.Println("dar:", i)
}
wg.Done()
}
從上面的代碼執(zhí)行效果看到瓦灶,協(xié)程能夠使我們,遇到阻塞抱完,不會阻塞到那贼陶,另一個協(xié)程還會繼續(xù)執(zhí)行。
- 在前面我們只是用了系統(tǒng)中的單核,go其實是一門異步多線程語言
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
var wg sync.WaitGroup
func init() {
runtime.GOMAXPROCS(runtime.NumCPU())
}
func main() {
wg.Add(2)
go foo()
go bar()
wg.Wait()
}
func foo() {
for i := 0; i < 45; i++ {
fmt.Println("Foo:", i)
time.Sleep(3 * time.Millisecond)
}
wg.Done()
}
func bar() {
for i := 0; i < 45; i++ {
fmt.Println("Bar:", i)
time.Sleep(20 * time.Millisecond)
}
wg.Done()
}
當某個協(xié)程阻塞時每界,cpu會把其他協(xié)程加載進來執(zhí)行捅僵,這樣確保cpu不會空閑著≌2悖 runtime.GOMAXPROCS(runtime.NumCPU())表示在系統(tǒng)的所有核上并發(fā)運行庙楚。
- 競爭當用同一個變量時,該變量會使該變量變化趴樱。
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var wg sync.WaitGroup
var counter int
func main() {
wg.Add(2)
go incrementor("Foo:")
go incrementor("Bar:")
wg.Wait()
fmt.Println("Final Counter:", counter)
}
func incrementor(s string) {
rand.Seed(time.Now().UnixNano())
for i := 0; i < 20; i++ {
x := counter
x++
time.Sleep(time.Duration(rand.Intn(3)) * time.Millisecond)
counter = x
fmt.Println(s, i, "Counter:", counter)
}
wg.Done()
}
- 加鎖
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var wg sync.WaitGroup
var counter int
var mutex sync.Mutex
func main() {
wg.Add(2)
go incrementor("Foo:")
go incrementor("Bar:")
wg.Wait()
fmt.Println("Final Counter:", counter)
}
func incrementor(s string) {
for i := 0; i < 20; i++ {
time.Sleep(time.Duration(rand.Intn(20)) * time.Millisecond)
mutex.Lock()
counter++
fmt.Println(s, i, "Counter:", counter)
mutex.Unlock()
}
wg.Done()
}
- 原子操作
package main
import (
"fmt"
"math/rand"
"sync"
"sync/atomic"
"time"
)
var wg sync.WaitGroup
var counter int64
func main() {
wg.Add(2)
go incrementor("Foo:")
go incrementor("Bar:")
wg.Wait()
fmt.Println("Final Counter:", counter)
}
func incrementor(s string) {
for i := 0; i < 20; i++ {
time.Sleep(time.Duration(rand.Intn(3)) * time.Millisecond)
atomic.AddInt64(&counter, 1)
fmt.Println(s, i, "Counter:", atomic.LoadInt64(&counter)) // access without race
}
wg.Done()
}
// go run -race main.go
// vs
// go run main.go
為什么要需要channel
time.Sleep(1) 這是為了讓新開啟的 goroutine 有機會得到執(zhí)行馒闷,開啟一個 goroutine 之后,后續(xù)的代碼會繼續(xù)執(zhí)行叁征,在上面的例子中后續(xù)代碼執(zhí)行完畢程序就終止了纳账,而開啟的 goroutine 可能還沒開始執(zhí)行。
如果嘗試去掉 #1 處的代碼捺疼,程序也可能會正常運行疏虫,這是因為恰巧開啟的 goroutine 只是簡單的執(zhí)行了一次輸出,如果 goroutine 中耗時稍長就會導致只能看到主一句 main function message 啤呼。
換句話話說卧秘,這里的 time.sleep 提供的是一種調(diào)度機制,這也是 Go 中 channel 存在的目的:負責消息傳遞和調(diào)度官扣。什么是channel
Channel 是 Go 中為 goroutine 提供的一種通信機制翅敌,channel 是有類型的,而且是有方向的惕蹄,可以把 channel 類比成 unix
中的 pipe蚯涮。channel 是用來傳遞消息的。
package main
import (
"fmt"
)
func main() {
i := make(chan int)//int 類型
s := make(chan string)//字符串類型
r := make(<-chan bool)//只讀
w := make(chan<- []int)//只寫
c := make(chan int)
go func() {
fmt.Println("goroutine message")
c <- 1 //1
}()
<-c //2
fmt.Println("main function message")
}
聲明了一個 int 類型的 channel卖陵,在 goroutine 中在代碼 #1 處向 channel 發(fā)送了數(shù)據(jù) 1 遭顶,在 main 中 #2 處等待數(shù)據(jù)的接收,如果 c 中沒有數(shù)據(jù)泪蔫,代碼的執(zhí)行將發(fā)生阻塞液肌,直到 c 中數(shù)據(jù)接收完畢。這是 channel 最簡單的用法之一:同步 鸥滨,這種類型的 channel 沒有設(shè)置容量,稱之為 unbuffered channel谤祖。
發(fā)送與等待互相阻塞婿滓,