https://blog.csdn.net/u011957758/article/details/81159481
如果說php是最好的語言,那么golang就是最并發(fā)的語言。
支持golang的并發(fā)很重要的一個是goroutine的實現(xiàn),那么本文將重點圍繞goroutine來做一下相關(guān)的筆記,以便日后快速留戀。
10s后,以下知識點即將靠近:
1.從并發(fā)模型說起
2.goroutine的簡介
3.goroutine的使用姿勢
4.通道(channel)的簡介
5.重要的四種通道使用
6.goroutine死鎖與處理
7.select的簡介
8.select的應(yīng)用場景
9.select死鎖
看過很多大神簡介,各種研究高并發(fā),那么就通俗的說下并發(fā)。
并發(fā)目前來看比較主流的就三種:
1.多線程
每個線程一次處理一個請求,線程越多可并發(fā)處理的請求數(shù)就越多,但是在高并發(fā)下,多線程開銷會比較大。
2.協(xié)程
無需搶占式的調(diào)度,開銷小,可以有效的提高線程的并發(fā)性,從而避免了線程的缺點的部分
3.基于異步回調(diào)的IO模型
說一個熟悉的,比如nginx使用的就是epoll模型,通過事件驅(qū)動的方式與異步IO回調(diào),使得服務(wù)器持續(xù)運轉(zhuǎn),來支撐高并發(fā)的請求
為了追求更高效和低開銷的并發(fā),golang的goroutine來了。
定義:在go里面,每一個并發(fā)執(zhí)行的活動成為goroutine。
詳解:goroutine可以認為是輕量級的線程,與創(chuàng)建線程相比,創(chuàng)建成本和開銷都很小,每個goroutine的堆棧只有幾kb,并且堆棧可根據(jù)程序的需要增長和縮小(線程的堆棧需指明和固定),所以go程序從語言層面支持了高并發(fā)。
程序執(zhí)行的背后:當一個程序啟動的時候,只有一個goroutine來調(diào)用main函數(shù),稱它為主goroutine,新的goroutine通過go語句進行創(chuàng)建。
在函數(shù)或者方法前面加上關(guān)鍵字go,即創(chuàng)建一個并發(fā)運行的新goroutine。
上代碼:
package main
import (
'fmt'
'time'
)
func HelloWorld() {
fmt.Println('Hello world goroutine')
}
func main() {
go HelloWorld() // 開啟一個新的并發(fā)運行
time.Sleep(1*time.Second)
fmt.Println('我后面才輸出來')
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
以上執(zhí)行后會輸出:
Hello world goroutine 我后面才輸出來1212
需要注意的是,執(zhí)行速度很快,一定要加sleep,不然你一定可以看到goroutine里頭的輸出。
這也說明了一個關(guān)鍵點:當main函數(shù)返回時,所有的gourutine都是暴力終結(jié)的,然后程序退出。
package main
import (
'fmt'
'time'
)
func DelayPrint() {
for i := 1; i <= 4; i++ {
time.Sleep(250 * time.Millisecond)
fmt.Println(i)
}
}
func HelloWorld() {
fmt.Println('Hello world goroutine')
}
func main() {
go DelayPrint() // 開啟第一個goroutine
go HelloWorld() // 開啟第二個goroutine
time.Sleep(2*time.Second)
fmt.Println('main function')
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
函數(shù)輸出:
Hello world goroutine 1 2 3 4 5 main function12345671234567
有心的同學(xué)可能會發(fā)現(xiàn),DelayPrint里頭有sleep,那么會導(dǎo)致第二個goroutine堵塞或者等待嗎?
答案是:no
疑惑:當程序執(zhí)行g(shù)o FUNC()的時候,只是簡單的調(diào)用然后就立即返回了,并不關(guān)心函數(shù)里頭發(fā)生的故事情節(jié),所以不同的goroutine直接不影響,main會繼續(xù)按順序執(zhí)行語句。
如果說goroutine是Go并發(fā)的執(zhí)行體,那么'通道'就是他們之間的連接。
通道可以讓一個goroutine發(fā)送特定的值到另外一個goroutine的通信機制。
聲明
var ch chan int // 聲明一個傳遞int類型的channel
ch := make(chan int) // 使用內(nèi)置函數(shù)make()定義一個channel
//=========
ch <- value // 將一個數(shù)據(jù)value寫入至channel,這會導(dǎo)致阻塞,直到有其他goroutine從這個channel中讀取數(shù)據(jù)
value := <-ch // 從channel中讀取數(shù)據(jù),如果channel之前沒有寫入數(shù)據(jù),也會導(dǎo)致阻塞,直到channel中被寫入數(shù)據(jù)為止
//=========
close(ch) // 關(guān)閉channel
1
2
3
4
5
6
7
8
9
10
11
1
2
3
4
5
6
7
8
9
10
11
有沒注意到關(guān)鍵字'阻塞'?,這個其實是默認的channel的接收和發(fā)送,其實也有非阻塞的,請看下文。
1.無緩沖通道
說明:無緩沖通道上的發(fā)送操作將會被阻塞,直到另一個goroutine在對應(yīng)的通道上執(zhí)行接收操作,此時值才傳送完成,兩個goroutine都繼續(xù)執(zhí)行。
上代碼:
package main import ( 'fmt' 'time' ) var done chan bool func HelloWorld() { fmt.Println('Hello world goroutine') time.Sleep(1*time.Second) done <- true } func main() { done = make(chan bool) // 創(chuàng)建一個channel go HelloWorld() <-done }12345678910111213141516171234567891011121314151617
輸出:
Hello world goroutine
1
1
由于main不會等goroutine執(zhí)行結(jié)束才返回,前文專門加了sleep輸出為了可以看到goroutine的輸出內(nèi)容,那么在這里由于是阻塞的,所以無需sleep。
(小嘗試:可以將代碼中'done <- true'和'<-done',去掉再執(zhí)行,看看會發(fā)生啥?)
2.管道
通道可以用來連接goroutine,這樣一個的輸出是另一個輸入。這就叫做管道。
例子:
package main import ( 'fmt' 'time' ) var echo chan string var receive chan string // 定義goroutine 1 func Echo() { time.Sleep(1*time.Second) echo <- '咖啡色的羊駝' } // 定義goroutine 2 func Receive() { temp := <- echo // 阻塞等待echo的通道的返回 receive <- temp } func main() { echo = make(chan string) receive = make(chan string) go Echo() go Receive() getStr := <-receive // 接收goroutine 2的返回 fmt.Println(getStr) }123456789101112131415161718192021222324252627282930313233123456789101112131415161718192021222324252627282930313233
在這里不一定要去關(guān)閉channel,因為底層的垃圾回收機制會根據(jù)它是否可以訪問來決定是否自動回收它。(這里不是根據(jù)channel是否關(guān)閉來決定的)
3.單向通道類型
當程序則夠復(fù)雜的時候,為了代碼可讀性更高,拆分成一個一個的小函數(shù)是需要的。
此時go提供了單向通道的類型,來實現(xiàn)函數(shù)之間channel的傳遞。
上代碼:
package main
import (
'fmt'
'time'
)
// 定義goroutine 1
func Echo(out chan<- string) { // 定義輸出通道類型
time.Sleep(1*time.Second)
out <- '咖啡色的羊駝'
close(out)
}
// 定義goroutine 2
func Receive(out chan<- string, in <-chan string) { // 定義輸出通道類型和輸入類型
temp := <-in // 阻塞等待echo的通道的返回
out <- temp
close(out)
}
func main() {
echo := make(chan string)
receive := make(chan string)
go Echo(echo)
go Receive(receive, echo)
getStr := <-receive // 接收goroutine 2的返回
fmt.Println(getStr)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
程序輸出:
咖啡色的羊駝11
4.緩沖管道
goroutine的通道默認是是阻塞的,那么有什么辦法可以緩解阻塞?
答案是:加一個緩沖區(qū)。
對于go來說創(chuàng)建一個緩沖通道很簡單:
ch := make(chan string, 3) // 創(chuàng)建了緩沖區(qū)為3的通道
//=========
len(ch) // 長度計算
cap(ch) // 容量計算
1
2
3
4
5
1
2
3
4
5
來一個死鎖現(xiàn)場一:
package main func main() { ch := make(chan int) <- ch // 阻塞main goroutine, 通道被鎖 }123456123456
輸出:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
1
2
3
4
1
2
3
4
死鎖現(xiàn)場2:
package main func main() { cha, chb := make(chan int), make(chan int) go func() { cha <- 1 // cha通道的數(shù)據(jù)沒有被其他goroutine讀取走,堵塞當前goroutine chb <- 0 }() <- chb // chb 等待數(shù)據(jù)的寫 }123456789101112123456789101112
為什么會有死鎖的產(chǎn)生?
非緩沖通道上如果發(fā)生了流入無流出,或者流出無流入,就會引起死鎖。
或者這么說:goroutine的非緩沖通道里頭一定要一進一出,成對出現(xiàn)才行。
上面例子屬于:一:流出無流入;二:流入無流出
當然,有一個例外:
func main() {
ch := make(chan int)
go func() {
ch <- 1
}()
}
1
2
3
4
5
6
1
2
3
4
5
6
執(zhí)行以上代碼將會發(fā)現(xiàn),竟然沒有報錯。
what?
不是說好的一進一出就死鎖嗎?
仔細研究會發(fā)現(xiàn),其實根本沒等goroutine執(zhí)行完,main函數(shù)自己先跑完了,所以就沒有數(shù)據(jù)流入主的goroutine,就不會被阻塞和報錯
有兩種辦法可以解決:
1.把沒取走的取走便是
package main func main() { cha, chb := make(chan int), make(chan int) go func() { cha <- 1 // cha通道的數(shù)據(jù)沒有被其他goroutine讀取走,堵塞當前goroutine chb <- 0 }() <- cha // 取走便是 <- chb // chb 等待數(shù)據(jù)的寫 }1234567891011121312345678910111213
2.創(chuàng)建緩沖通道
package main
func main() {
cha, chb := make(chan int, 3), make(chan int)
go func() {
cha <- 1 // cha通道的數(shù)據(jù)沒有被其他goroutine讀取走,堵塞當前goroutine
chb <- 0
}()
<- chb // chb 等待數(shù)據(jù)的寫
}
1
2
3
4
5
6
7
8
9
10
11
12
1
2
3
4
5
6
7
8
9
10
11
12
這樣的話,cha可以緩存一個數(shù)據(jù),cha就不會掛起當前的goroutine了。除非再放兩個進去,塞滿緩沖通道就會了。
定義:在golang里頭select的功能與epoll(nginx)/poll/select的功能類似,都是堅挺IO操作,當IO操作發(fā)生的時候,觸發(fā)相應(yīng)的動作。
select有幾個重要的點要強調(diào):
1.如果有多個case都可以運行,select會隨機公平地選出一個執(zhí)行,其他不會執(zhí)行
上代碼:
package main import 'fmt' func main() { ch := make (chan int, 1) ch<-1 select { case <-ch: fmt.Println('咖啡色的羊駝') case <-ch: fmt.Println('黃色的羊駝') } }123456789101112131415123456789101112131415
輸出:
(隨機)二者其一
1
1
2.case后面必須是channel操作,否則報錯。
上代碼:
package main import 'fmt' func main() { ch := make (chan int, 1) ch<-1 select { case <-ch: fmt.Println('咖啡色的羊駝') case 2: fmt.Println('黃色的羊駝') } }12345678910111213141234567891011121314
輸出報錯:
2 evaluated but not used
select case must be receive, send or assign recv
1
2
1
2
3.select中的default子句總是可運行的。所以沒有default的select才會阻塞等待事件
上代碼:
package main import 'fmt' func main() { ch := make (chan int, 1) // ch<-1 <= 注意這里備注了。 select { case <-ch: fmt.Println('咖啡色的羊駝') default: fmt.Println('黃色的羊駝') } }12345678910111213141234567891011121314
輸出:
黃色的羊駝
1
1
4.沒有運行的case,那么江湖阻塞事件發(fā)生報錯(死鎖)
package main import 'fmt' func main() { ch := make (chan int, 1) // ch<-1 <= 注意這里備注了。 select { case <-ch: fmt.Println('咖啡色的羊駝') } }123456789101112123456789101112
輸出報錯:
fatal error: all goroutines are asleep - deadlock!
1
1
1.timeout 機制(超時判斷)
package main import ( 'fmt' 'time' ) func main() { timeout := make (chan bool, 1) go func() { time.Sleep(1*time.Second) // 休眠1s,如果超過1s還沒I操作則認為超時,通知select已經(jīng)超時啦~ timeout <- true }() ch := make (chan int) select { case <- ch: case <- timeout: fmt.Println('超時啦!') } }12345678910111213141516171819201234567891011121314151617181920
以上是入門版,通常代碼中是這么寫的:
package main
import (
'fmt'
'time'
)
func main() {
ch := make (chan int)
select {
case <-ch:
case <-time.After(time.Second * 1): // 利用time來實現(xiàn),After代表多少時間后執(zhí)行輸出東西
fmt.Println('超時啦!')
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2.判斷channel是否阻塞(或者說channel是否已經(jīng)滿了)
package main import ( 'fmt' ) func main() { ch := make (chan int, 1) // 注意這里給的容量是1 ch <- 1 select { case ch <- 2: default: fmt.Println('通道channel已經(jīng)滿啦,塞不下東西了!') } }123456789101112131415123456789101112131415
3.退出機制
package main
import (
'fmt'
'time'
)
func main() {
i := 0
ch := make(chan string, 0)
defer func() {
close(ch)
}()
go func() {
DONE:
for {
time.Sleep(1*time.Second)
fmt.Println(time.Now().Unix())
i++
select {
case m := <-ch:
println(m)
break DONE // 跳出 select 和 for 循環(huán)
default:
}
}
}()
time.Sleep(time.Second * 4)
ch<-'stop'
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
輸出:
1532390471 1532390472 1532390473 stop 15323904741234512345
這邊要強調(diào)一點:退出循環(huán)一定要用break + 具體的標記,或者goto也可以。否則其實不是真的退出。
package main
import (
'fmt'
'time'
)
func main() {
i := 0
ch := make(chan string, 0)
defer func() {
close(ch)
}()
go func() {
for {
time.Sleep(1*time.Second)
fmt.Println(time.Now().Unix())
i++
select {
case m := <-ch:
println(m)
goto DONE // 跳出 select 和 for 循環(huán)
default:
}
}
DONE:
}()
time.Sleep(time.Second * 4)
ch<-'stop'
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
輸出:
1532390525 1532390526 1532390527 1532390528 stop1234512345
select不注意也會發(fā)生死鎖,前文有提到一個,這里分幾種情況,重點再次強調(diào):
1.如果沒有數(shù)據(jù)需要發(fā)送,select中又存在接收通道數(shù)據(jù)的語句,那么將發(fā)送死鎖
package main
func main() {
ch := make(chan string)
select {
case <-ch:
}
}
1
2
3
4
5
6
7
1
2
3
4
5
6
7
預(yù)防的話加default。
空select,也會引起死鎖
package main func main() { select {} }1234512345
聯(lián)系客服