您好,登錄后才能下訂單哦!
Go語(yǔ)言從語(yǔ)言層面上就支持了并發(fā),這與其他語(yǔ)言大不一樣。Go語(yǔ)言中有個(gè)概念叫做goroutine,這類似我們熟知的線程,但是更輕。
進(jìn)程和線程
進(jìn)程是程序在操作系統(tǒng)中的一次執(zhí)行過(guò)程,系統(tǒng)進(jìn)行資源分配和調(diào)度的一個(gè)獨(dú)立單位。
線程是進(jìn)程的一個(gè)執(zhí)行實(shí)體,是CPU調(diào)度和分派的基本單位,它是比進(jìn)程更小的能獨(dú)立運(yùn)行的基本單位。
一個(gè)進(jìn)程可以創(chuàng)建和撤銷多個(gè)線程,同一個(gè)進(jìn)程中的多個(gè)線程之間可以并發(fā)執(zhí)行。
所以程序的類型可以分為以下幾種:
并發(fā)和并行的區(qū)別
并發(fā),在微觀上,任意時(shí)刻只有一個(gè)程序在運(yùn)行。因?yàn)榫€程已經(jīng)是CPU調(diào)度的最小單元,一個(gè)CPU一次只能處理一個(gè)線程。但是宏觀上這些程序時(shí)同時(shí)在那里執(zhí)行的,所以這個(gè)只是并發(fā)。
所以在python里,貌似講的都是高并發(fā),似乎沒(méi)聽(tīng)過(guò)并行的概念。
協(xié)程和線程
協(xié)程,獨(dú)一的棧空間,共享堆空間,調(diào)度由用戶自己控制。本質(zhì)上類似于用戶級(jí)線程,這些用戶級(jí)線程的調(diào)度也是自己實(shí)現(xiàn)的。
線程,一個(gè)線程上可以跑多個(gè)協(xié)程,協(xié)程是輕量級(jí)的線程。
Go的調(diào)度器模型:G-P-M模型。
設(shè)置Golang運(yùn)行的cpu核數(shù)
設(shè)置當(dāng)前的程序運(yùn)行在多少核上,下面的例子是獲取CPU的核數(shù),然后運(yùn)行在所有核上:
package main
import (
"fmt"
"runtime"
)
func main() {
num := runtime.NumCPU()
runtim.GOMAXPROCS(num)
fmt.Println(num)
}
上面P的數(shù)目就是這里GOMAXPROCS設(shè)置的數(shù)目,通常設(shè)置為CPU核數(shù)。
1.8版本以上的Golang,是不需要做上面的設(shè)置的,默認(rèn)就是運(yùn)行在所有的核上。當(dāng)然還是可以設(shè)置一下,比如限制只能使用多少核。
goroutine的示例:
package main
import (
"fmt"
"time"
)
func example() {
var i int
for {
fmt.Println(i)
i++
time.Sleep(time.Millisecond * 30)
}
}
func main() {
go example() // 起一個(gè)goroutine
var j int
for j > -100 {
fmt.Println(j)
j--
time.Sleep(time.Millisecond * 100)
}
fmt.Println("運(yùn)行結(jié)束")
}
不同goroutine之間要進(jìn)行通訊,有下面2種方法:
先講管道(channel),然后講 goroutine 和 channel 結(jié)合的一些用法。
這篇的channel可以參考下:
https://www.jianshu.com/p/24ede9e90490
在下面的例子里定義了變量 m 來(lái)實(shí)現(xiàn)goroutine之間的通訊:
package main
import (
"fmt"
"time"
"sync"
)
var (
m = make(map[int]uint64)
lock sync.Mutex
)
type task struct {
n int
}
func calc(t *task) {
var res uint64
res = 1
for i := 1; i <= t.n; i++ {
res *= uint64(i)
}
lock.Lock()
m[t.n] = res // 變量m用來(lái)存放結(jié)果,這樣主線程里就能拿到m的值,操作要加鎖
lock.Unlock()
}
func main() {
for i := 0; i < 100; i++ {
t := &task{i}
go calc(t)
}
for j := 0; j < 10; j++ {
fmt.Printf("\r已運(yùn)行%d秒", j)
time.Sleep(time.Second)
}
fmt.Println("\r運(yùn)行完畢,輸出結(jié)果:")
lock.Lock()
for k, v := range m {
if v != 0 {
fmt.Printf("%d! = %v\n", k, v)
}
}
lock.Unlock()
}
channel的概念如下:
channel 聲明
var 變量名 chan 類型
var test1 chan int
var test2 chan string
var tesr3 chan map[string]string
var test4 chan stu
var test5 chan *stu
只是聲明還不夠,使用前還要make,分配內(nèi)存空間:
package main
import "fmt"
func main() {
var intChan chan int // 聲明
intChan = make(chan int, 10) // 初始化,長(zhǎng)度是10
intChan <- 10 // 存入管道
n := <- intChan // 取出
fmt.Println(n)
}
定義信號(hào)(空結(jié)構(gòu)體)
有一些場(chǎng)景中,一些 goroutine 需要一直循環(huán)處理信息,直到收到 quit 信號(hào)。作為信號(hào),只需要隨便傳點(diǎn)什么,并不關(guān)注具體的值。那么可以選擇使用空結(jié)構(gòu)體,像下面這樣定義了2個(gè)信號(hào):
msgCh := make(chan struct{})
quitCh := make(chan struct{})
// 傳信號(hào)的方法
msgCh <- struct{}{} // 前面的 struct{} 是變量的類型,后面的 {} 則是做初始化傳入空值生成實(shí)例
quitCh <- struct{}{}
起一個(gè)goroutine往管道里存,再起一個(gè)goroutine從管道里把數(shù)據(jù)取出:
package main
import (
"fmt"
"time"
)
func write(ch chan int) {
var i int
for {
ch <- i
i ++
time.Sleep(time.Millisecond)
}
}
func read(ch chan int) {
for {
b := <- ch
fmt.Println(b)
}
}
func main() {
intChan := make(chan int, 10)
go write(intChan)
go read(intChan)
time.Sleep(time.Second * 5)
}
channel 分為不帶緩存的 channel 和帶緩存的 channel。
channel 一定要初始化后才能進(jìn)行讀寫操作,否則會(huì)永久阻塞。這個(gè)不是這里要講的重點(diǎn),順便帶一下。
無(wú)緩存的channle
初始化make的時(shí)候不傳入第二個(gè)參數(shù)設(shè)置容量就是:
ch := make(chan int)
從無(wú)緩存的 channel 中讀取消息會(huì)阻塞,直到有 goroutine 向該 channel 中發(fā)送消息;同理,向無(wú)緩存的 channel 中發(fā)送消息也會(huì)阻塞,直到有 goroutine 從 channel 中讀取消息。
有緩存的 channel
有緩存的 channel 的聲明方式為指定 make 函數(shù)的第二個(gè)參數(shù),該參數(shù)為 channel 緩存的容量:
ch := make(chan int, 10)
有緩存的 channel 類似一個(gè)阻塞隊(duì)列(采用環(huán)形數(shù)組實(shí)現(xiàn))。當(dāng)緩存未滿時(shí),向 channel 中發(fā)送消息時(shí)不會(huì)阻塞,當(dāng)緩存滿時(shí),發(fā)送操作將被阻塞,直到有其他 goroutine 從中讀取消息;
相應(yīng)的,當(dāng) channel 中消息不為空時(shí),讀取消息不會(huì)出現(xiàn)阻塞,當(dāng) channel 為空時(shí),讀取操作會(huì)造成阻塞,直到有 goroutine 向 channel 中寫入消息。
緩沖區(qū)的大小
通過(guò) len 函數(shù)可以獲得 chan 中的元素個(gè)數(shù),通過(guò) cap 函數(shù)可以得到 channel 的緩沖區(qū)長(zhǎng)度。
無(wú)緩存和緩沖區(qū)是1的差別
無(wú)緩存的 channel 的 len和cap 始終都是0。
通過(guò)無(wú)緩存的 channel 進(jìn)行通信時(shí),接收者收到數(shù)據(jù) happens before 發(fā)送者 goroutine 喚醒
上面這句不好理解,不過(guò)可以先看下現(xiàn)象。
下面的這2行函數(shù)會(huì)報(bào)錯(cuò),說(shuō)是死鎖。但是如果設(shè)置了 channel 的容量哪怕是1,就不會(huì)報(bào)錯(cuò)的:
func main() {
ch := make(chan int)
ch <- 1
}
雖然容量1的channel也只能存1個(gè)數(shù),但是無(wú)緩沖區(qū)的channel似乎1個(gè)數(shù)都存不了,除非馬上能取走:
func main() {
ch := make(chan int, 1)
// 要起一個(gè)goroutine可以馬上接收channel里的數(shù)據(jù)
go func () {
fmt.Println(<- ch)
}()
ch <- 1
time.Sleep(time.Second) // 要給goroutine執(zhí)行完成的時(shí)間
}
小結(jié):無(wú)緩存的channel需要有一個(gè)goroutine可以把channel里的數(shù)據(jù)馬上取走。
在學(xué)習(xí)關(guān)閉channel之前,先看下下面的例子。由于沒(méi)有關(guān)閉channel,是會(huì)有問(wèn)題的,不過(guò)例子里都解決了。先看下不用關(guān)閉channel可以怎么搞,然后再接著看關(guān)閉channel的用法:
package main
import (
"time"
"fmt"
)
func calc(taskChan chan int, resChan chan int) {
for v := range taskChan {
// 判斷是不是素?cái)?shù)
flag := true
for i := 2; i < v; i++ {
if v % i == 0 {
flag = false
break
}
}
if flag {
resChan <- v
}
}
}
func main() {
intChan := make(chan int, 1000)
// 這個(gè)也是個(gè)goroutine
go func(){
for i := 2; i < 100000; i++ {
intChan <- i
}
}() // 管道滿了之后,這個(gè)匿名函數(shù)會(huì)阻塞,但是不影響程序繼續(xù)往下走
resultChan := make(chan int, 1000)
// 同時(shí)起8個(gè)goroutine
for i := 0; i < 8; i++ {
go calc(intChan, resultChan)
}
// 再起一個(gè)取結(jié)果的goroutine,不阻塞主線程
go func(){
for v := range resultChan{
fmt.Println("素?cái)?shù):", v)
}
}()
// 給上面的匿名函數(shù)幾秒鐘來(lái)輸出結(jié)果
time.Sleep(time.Second * 5)
}
上面的例子里用了2個(gè)匿名函數(shù),也都是起的goroutine。如果是在主線程里直接for循環(huán)的話,那個(gè)for循環(huán)就會(huì)變成死鎖,程序不會(huì)自己往下走。所以運(yùn)行在goroutine里的死循環(huán),在主線程退出后也就結(jié)束了,不會(huì)有問(wèn)題。后一個(gè)匿名函數(shù)是對(duì)channel的進(jìn)行遍歷,channel取空后,會(huì)進(jìn)入阻塞,如果是運(yùn)行在主線程里的話也會(huì)形成死鎖。
range 遍歷
channel 也可以使用 range 取值,并且會(huì)一直從 channel 中讀取數(shù)據(jù),直到有 goroutine 對(duì)改 channel 執(zhí)行 close 操作,循環(huán)才會(huì)結(jié)束。
golang 提供了內(nèi)置的 close 函數(shù)對(duì) channel 進(jìn)行關(guān)閉操作:
ch := make(chan int)
close(ch)
關(guān)于 channel 的關(guān)閉,有以下的特點(diǎn):
有2種方式可以把管道里的數(shù)據(jù)都取出來(lái),但是都需要把管道關(guān)閉:
關(guān)閉channel然后讀取的示例:
package main
import "fmt"
func main() {
var ch chan int
ch = make(chan int, 5)
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
for {
var b int
b, ok := <- ch
fmt.Println(b, ok)
if ok == false {
break
}
}
}
/* 執(zhí)行結(jié)果
PS H:\Go\src\go_dev\day8\channel\close_chan> go run main.go
0 true
1 true
2 true
3 true
4 true
0 false
PS H:\Go\src\go_dev\day8\channel\close_chan>
*/
上面輸出的最后一條,就是channel已經(jīng)空了,讀出來(lái)的就是類型的0值,并且ok變false了。
遍歷channel的示例:
package main
import "fmt"
func main() {
var ch chan int
ch = make(chan int) // 這個(gè)管道沒(méi)有無(wú)緩存
// 這個(gè)goroutine一次存一個(gè),再存會(huì)阻塞,直到主線程后面的for循環(huán)遍歷的時(shí)候取走數(shù)據(jù)
// 存完100個(gè)數(shù)后,這里的for循環(huán)結(jié)束,會(huì)關(guān)閉管道。主線程后面的for循環(huán)的遍歷就能正常結(jié)束了
go func () {
for i := 0; i < 100; i++ {
ch <- i
}
close(ch)
}()
for v := range ch {
fmt.Println(v)
}
}
判斷子線程結(jié)束
學(xué)到這里,再也不需要用Sleep等待子線程結(jié)束了,可以通過(guò)管道實(shí)現(xiàn)??梢詥为?dú)定義一個(gè)專門用來(lái)判斷子線程結(jié)束的管道。子線程完成任務(wù)后,就傳個(gè)值給管道,主線程就阻塞的讀管道里的信息,一旦讀到信息,就說(shuō)明子線程完成了,可以繼續(xù)執(zhí)行或者退出了。如果起了多個(gè)子線程,則主線程就是用for循環(huán)多讀幾次,就能判斷出有多少子線程已經(jīng)結(jié)束了。
聲明只讀的channel:
var ch <-chan int
聲明只寫的channel:
var ch chan<- int
應(yīng)用場(chǎng)景,管道需要能夠可讀可寫。但是可以限制它在某個(gè)函數(shù)里的功能,也就是在定義函數(shù)的參數(shù)的時(shí)候,把管道的類型設(shè)置為只讀或只寫。或者把管道傳給結(jié)構(gòu)體,結(jié)構(gòu)體里限制管道的讀寫限制?
下面是之前的一個(gè)例子,僅僅只是把2個(gè)函數(shù)在設(shè)置參數(shù)類型的時(shí)候把管道的讀寫限制加上了:
package main
import (
"fmt"
"time"
)
func write(ch chan<- int) {
var i int
for {
ch <- i
i ++
time.Sleep(time.Millisecond)
}
}
func read(ch <-chan int) {
for {
b := <- ch
fmt.Println(b)
}
}
func main() {
intChan := make(chan int, 10)
go write(intChan)
go read(intChan)
time.Sleep(time.Second * 5)
}
select 用法類似IO多路復(fù)用,可以同時(shí)監(jiān)聽(tīng)多個(gè) channel 的消息狀態(tài),用法如下:
select {
case <- ch2:
...
case <- ch3:
...
case ch4 <- 10;
...
default:
...
}
select 可以同時(shí)監(jiān)聽(tīng)多個(gè) channel 的寫入或讀?。?/p>
select只會(huì)執(zhí)行一次
這個(gè)例子只會(huì)輸出一次,隨機(jī)是1或者是2,然后接結(jié)束了:
package main
import "fmt"
func main() {
ch2 := make(chan int, 1)
ch2 <- 1
ch3 := make(chan int, 1)
ch3 <- 2
select {
case v := <- ch2:
fmt.Println(v)
case v := <- ch3:
fmt.Println(v)
default:
fmt.Println(0)
}
}
所以如果要把管道里的數(shù)取完,或者取多次,就要再套一層for循環(huán)。
for循環(huán)和break的效果
在select外面用for套了一層死循環(huán),這樣就是反復(fù)的執(zhí)行select。不過(guò)break在這里就沒(méi)效果了:
package main
import (
"fmt"
"time"
)
func main() {
var ch2, ch3 chan int
ch2 = make(chan int, 10)
ch3 = make(chan int, 10)
for i := 0; i < cap(ch2); i++ {
ch2 <- i
ch3 <- i * i
}
// LABEL1:
for {
select {
case v := <- ch2:
fmt.Println("ch2", v)
case v := <- ch3:
fmt.Println("ch3", v)
default:
fmt.Println("所有元素都已經(jīng)取完")
break // 這個(gè)break沒(méi)有意義,因?yàn)橹凳翘鰏elect,而不是for循環(huán)
// break LABEL1 // 這個(gè)break可以直接跳出for循環(huán)
}
time.Sleep(time.Second)
}
}
如果要跳出for循環(huán),可以配合標(biāo)簽。上面的代碼里已經(jīng)寫好了只是注釋掉了。
定時(shí)器是在 time 包里的,
package main
import (
"fmt"
"time"
)
func main() {
t := time.NewTicker(time.Second)
for v := range t.C {
fmt.Println(v)
}
}
上面調(diào)用的NewTicker()方法返回的是個(gè)結(jié)構(gòu)體,如下:
type Ticker struct {
C <-chan Time // The channel on which the ticks are delivered.
// contains filtered or unexported fields
}
上面的例子里遍歷了 t.C 就是一個(gè)channel。time包內(nèi)部應(yīng)該是會(huì)產(chǎn)生一個(gè)goroutine,每隔一段時(shí)間就傳一個(gè)數(shù)據(jù)進(jìn)去。
設(shè)置超時(shí)時(shí)間
還有一個(gè)After()方法,和上面的方法是一樣的。不過(guò)這個(gè)方法直接返回管道,即 NewTimer(d).C 。而NewTimer()方法的管道在返回的結(jié)構(gòu)體的屬性C里。這個(gè)After()方法用起來(lái)更方便。結(jié)合select正好可以做成一個(gè)設(shè)置任務(wù)超時(shí)時(shí)間的功能:
package main
import (
"fmt"
"time"
)
func task(ch chan struct{}) {
time.Sleep(time.Second * 3)
ch <- struct{}{}
}
func main() {
ch := make(chan struct{}) // 定義好信號(hào)的管道,傳遞空結(jié)構(gòu)體
go task(ch) // 啟動(dòng)一個(gè)任務(wù)
select {
case <- ch:
fmt.Println("任務(wù)執(zhí)行結(jié)束")
case <- time.After(time.Second * 2): // 2秒后超時(shí)
fmt.Println("任務(wù)超時(shí)")
}
}
程序里起的gorountine中如果panic了,并且這個(gè)goroutine里面沒(méi)有捕獲錯(cuò)誤的話,整個(gè)程序就會(huì)掛掉。
下面的程序會(huì)報(bào)錯(cuò)(Panic),是gorountine里的產(chǎn)生的錯(cuò)誤:
package main
func divideZero(ch chan int) {
zero := 0
ch <- 1 / zero
}
func main() {
ch := make(chan int)
go divideZero(ch)
<- ch
}
在gorountine中運(yùn)行錯(cuò)誤了,是可以不影響其他線程和主線程的繼續(xù)執(zhí)行的。所以,好的習(xí)慣是每當(dāng)產(chǎn)生一個(gè)goroutine,就在開(kāi)頭用defer插入recover, 這樣在出現(xiàn)panic的時(shí)候,就只是自己退出而不影響整個(gè)程序。下面是優(yōu)化后的代碼,加入了recover來(lái)捕獲錯(cuò)誤:
package main
import "fmt"
func divideZero(ch chan int) {
defer func () {
if err := recover(); err != nil {
fmt.Println(err)
// 要給管道傳值,否則主線程從空管道里取值會(huì)阻塞,形成死鎖
ch <- 0
}
}()
zero := 0
ch <- 1 / zero
}
func main() {
ch := make(chan int)
go divideZero(ch)
<- ch
}
測(cè)試用例的文件名必須以_test.go結(jié)尾,測(cè)試的函數(shù)也必須以Test開(kāi)頭。符合命名規(guī)則,使用 go test 命令的時(shí)候就能自動(dòng)運(yùn)行測(cè)試用例。
這篇的單元測(cè)試比較粗糙,不過(guò)基本怎么用,以及用法示例都簡(jiǎn)單記下來(lái)了。
先準(zhǔn)備一個(gè)需要被測(cè)試的函數(shù):
package main
import "fmt"
func get_fullname(first, last string) (fullname string) {
fullname = first + " " + last
return
}
func main() {
fullname := get_fullname("Barry", "Allen")
fmt.Println(fullname)
}
上面的 get_fullname() 函數(shù)就是接下來(lái)要進(jìn)行單元測(cè)試的函數(shù)。
package main
import "testing"
func TestName(t *testing.T) {
r := get_fullname("Sara", "Lance")
expect := "Sara Lance"
if r != expect {
t.Fatalf("ERROR: get_fullname expect: %s actual: %s", expect, r)
}
t.Log("測(cè)試成功")
}
寫完測(cè)試用例,就可以執(zhí)行測(cè)試了,使用命令 go test。輸出如下:
PS H:\Go\src\go_dev\day8\unit_test\name> go test
PASS
ok go_dev/day8/unit_test/name 0.058s
PS H:\Go\src\go_dev\day8\unit_test\name>
看到PASS了,但是t.Log()并沒(méi)有輸出,要看到更多信息,要用帶上-v參數(shù)。使用命令 go test -v ,輸出如下:
PS H:\Go\src\go_dev\day8\unit_test\name> go test -v
=== RUN TestName
--- PASS: TestName (0.00s)
name_test.go:11: 測(cè)試成功
PASS
ok go_dev/day8/unit_test/name 0.053s
PS H:\Go\src\go_dev\day8\unit_test\name>
直接用go test命令,只顯示測(cè)試的結(jié)果。如果有多個(gè)測(cè)試用例,也只有一個(gè)結(jié)果??梢杂?v參數(shù)看到詳細(xì)的信息,每個(gè)測(cè)試用例的的結(jié)果都會(huì)打印出來(lái)。
如果某個(gè)測(cè)試失敗了,就會(huì)直接退出,不會(huì)繼續(xù)測(cè)試下去。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。