golang學習筆記----併發

併發模型

併發目前來看比較主流的就三種:nginx

  • 多線程:每一個線程一次處理一個請求,線程越多可併發處理的請求數就越多,可是在高併發下,多線程開銷會比較大。
  • 協程:無需搶佔式的調度,開銷小,能夠有效的提升線程的併發性,從而避免了線程的缺點的部分
  • 基於異步回調的IO模型: 好比nginx使用的就是epoll模型,經過事件驅動的方式與異步IO回調,使得服務器持續運轉,來支撐高併發的請求

golang的goroutine就是爲了追求更高效和低開銷的併發golang

goroutine的簡介

定義:在go裏面,每個併發執行的活動成爲goroutine。緩存

詳解:goroutine能夠認爲是輕量級的線程,與建立線程相比,建立成本和開銷都很小,每一個goroutine的堆棧只有幾kb(而線程則需1M),而且堆棧可根據程序的須要增加和縮小(線程的堆棧需指明和固定),因此go程序從語言層面支持了高併發。服務器

程序執行的背後:當一個程序啓動的時候,只有一個goroutine來調用main函數,稱它爲主goroutine,新的goroutine經過go語句進行建立。多線程

 

goroutine的使用

單個goroutine建立:在函數或者方法前面加上關鍵字go,即建立一個併發運行的新goroutine。

package main

import (
	"fmt"
	"time"
)

func HelloWorld() {
	fmt.Println("Hello world goroutine")
}

func main() {
	go HelloWorld() // 開啓一個新的併發運行
	time.Sleep(1 * time.Second)
	fmt.Println("main end")
}

  以上執行後會輸出:併發

Hello world goroutine
main end

  須要注意的是,main執行速度很快,不加sleep,可能先執行完畢就結束了,會看不到goroutine裏頭的輸出。異步

這也說明了一個關鍵點:當main函數返回時,全部的gourutine都是暴力終結的,而後程序退出。函數

多個goroutine建立

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")
}

  輸出:高併發

Hello world goroutine
1
2
3
4
main function

  

DelayPrint裏頭有sleep,那麼會致使第二個goroutine堵塞或者等待嗎?
答案是:no
當main程序執行go FUNC()的時候,只是簡單的調用而後就當即返回了,並不關心函數裏頭髮生的故事情節,因此不一樣的goroutine直接不影響,main會繼續按順序執行語句。

工具

通道(channel)的簡介

若是說goroutine是Go併發的執行體,那麼」通道」就是他們之間的鏈接。


channel是goroutine之間互相通訊的工具。
具體點的說法,channel是一種通訊管道,可以把數據放入管道,也能從管道中讀出數據。一個goroutine把數據放入chan,而後另一個goroutine能夠從chan裏面讀出數據。

 

 

 

聲明&傳值&關閉

package main

import (
	"fmt"
	"time"
)

func main() {
	//var ch chan int      // 聲明一個傳遞int類型的channel
	ch := make(chan int) // 使用內置函數make()定義一個無緩存channel 
	go func() {
		var value int
		value = <-ch // 從channel中讀取數據,若是channel以前沒有寫入數據,也會致使阻塞,直到channel中被寫入數據爲止

		fmt.Println(value)
	}()
	
	//=========
	ch <- 10 // 將一個數據value寫入至channel,這會致使阻塞,直到有其餘goroutine從這個channel中讀取數據
	//=========
	
	time.Sleep(1 * time.Second)
	
	close(ch) // 關閉channel

}

有沒注意到關鍵字」阻塞「?,這個實際上是默認的channel的接收和發送,其實也有非阻塞的,請看下文。

重要的四種通道使用

無緩衝通道:無緩衝通道上的發送操做將會被阻塞,直到另外一個goroutine在對應的通道上執行接收操做,此時值才傳送完成,兩個goroutine都繼續執行。

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) // 建立一個channel
	go HelloWorld()
	<-done
	fmt.Println("main function end")
}

  

輸出:

Hello world goroutine

main function end

因爲main不會等goroutine執行結束才返回,上一個示例專門加了sleep輸出爲了能夠看到goroutine的輸出內容,那麼在這裏因爲是阻塞的,因此無需sleep。

將代碼中」done <- true」和」<-done」,去掉再執行,看看會發生啥?

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) // 建立一個channel
	go HelloWorld()
	//<-done
	fmt.Println("main function end")
}

  輸出:

main function end

main主程序執行完打印以後就結束了

管道:通道能夠用來鏈接goroutine,這樣一個的輸出是另外一個輸入。這就叫作管道。

 

 

 

package main

import (
	"fmt"
	"time"
)

var echo chan string
var receive chan string

// 定義goroutine 1
func Echo() {
	fmt.Println("enter echo function >>>>>>>>>>>>>>>> ")
	time.Sleep(1 * time.Second)
	echo <- "Echo....."
	fmt.Println("exit frome Echo function <<<<<<<<<<<<")
}

// 定義goroutine 2
func Receive() {
	fmt.Println("enter Receive function >>>>>>>>>>>>>")
	temp := <-echo // 阻塞等待echo的通道的返回
	receive <- temp
	fmt.Println("exit frome Receive function <<<<<<<<<<<<")
}

func main() {
	fmt.Println("enter main function >>>>>>")
	echo = make(chan string)
	receive = make(chan string)

	go Echo()
	go Receive()

	getStr := <-receive // 接收goroutine 2的返回

	fmt.Println(getStr)
	fmt.Println("main function end<<<<<<")
}

  輸出:

enter main function >>>>>>
enter Receive function >>>>>>>>>>>>>
enter echo function >>>>>>>>>>>>>>>> 
exit frome Echo function <<<<<<<<<<<<
exit frome Receive function <<<<<<<<<<<<
Echo.....
main function end<<<<<<

在這裏不必定要去關閉channel,由於底層的垃圾回收機制會根據它是否能夠訪問來決定是否自動回收它。(這裏不是根據channel是否關閉來決定的)

 

單向通道類型

當程序則夠複雜的時候,爲了代碼可讀性更高,拆分紅一個一個的小函數是須要的。

此時go提供了單向通道的類型,來實現函數之間channel的傳遞。

package main

import (
	"fmt"
	"time"
)

var echo chan string
var receive chan string

// 定義goroutine 1
func Echo(out chan<- string) { //定義輸出通道類型
	fmt.Println("enter echo function >>>>>>>>>>>>>>>> ")
	time.Sleep(1 * time.Second)
	echo <- "Echo....."
	close(out)
	fmt.Println("exit frome Echo function <<<<<<<<<<<<")
}

// 定義goroutine 2
func Receive(out chan<- string, in <-chan string) { //定義輸出通道類型和輸入通道類型
	fmt.Println("enter Receive function >>>>>>>>>>>>>")
	temp := <-in // 阻塞等待echo的通道的返回
	out <- temp
	close(out)
	fmt.Println("exit frome Receive function <<<<<<<<<<<<")
}

func main() {
	fmt.Println("enter main function >>>>>>")
	echo = make(chan string)
	receive = make(chan string)

	go Echo(echo)
	go Receive(receive, echo)

	getStr := <-receive // 接收goroutine 2的返回

	fmt.Println(getStr)
	fmt.Println("main function end<<<<<<")
}

  輸出:

enter main function >>>>>>
enter Receive function >>>>>>>>>>>>>
enter echo function >>>>>>>>>>>>>>>> 
exit frome Receive function <<<<<<<<<<<<
exit frome Echo function <<<<<<<<<<<<
Echo.....
main function end<<<<<<

 

緩衝管道:goroutine的通道默認是是阻塞的,那麼有什麼辦法能夠緩解阻塞?答案是:加一個緩衝區。

 

 

 對於go來講建立一個緩衝通道很簡單:

ch := make(chan string, 3) // 建立了緩衝區爲3的通道

//=========
len(ch)   // 長度計算
cap(ch)   // 容量計算

  

package main

import (
	"fmt"
)

func f1(c chan int) { // chan int 表示參數的類型是存儲int類型的chanel
	c <- 1 //向這個chanel中傳入1,以後main()中就會接受到1
}

func f2(c chan int) { // chan int 表示參數的類型是存儲int類型的chanel
	c <- 2 //向這個chanel中傳入2,以後main()中就會接收到2
}

func main() {
	c := make(chan int, 2) //建立帶有緩衝的chanel,緩衝大小是2

	//這樣調用函數,那麼f1和f2就是併發執行了
	go f1(c) //將參數c傳遞給f1()
	go f2(c) //將參數c傳遞給f2()

	c1 := <-c
	c2 := <-c //main函數只有從c中接收到倆個值,纔會退出main(),不然main()中會阻塞這那直到c中有數據能夠接收
	fmt.Printf("c1:%d\nc2:%d\n", c1, c2)
}

  輸出:

c1:2
c2:1

  

goroutine死鎖與友好退出

 流出無流入

package main

func main() {
	ch := make(chan int)
	<-ch // 阻塞main goroutine, 通道被鎖
}

  

輸出:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
	D:/GOPATH/src/study.go/main.go:5 +0x54

  

 

流入無流出(cha)死鎖現場2:

package main

func main() {
	cha, chb := make(chan int), make(chan int)

	go func() {
		cha <- 1 // cha通道的數據沒有被其餘goroutine讀取走,堵塞當前goroutine
		chb <- 0
	}()

	<-chb // chb 等待數據的寫
}

爲何會有死鎖的產生?

非緩衝通道上若是發生了流入無流出,或者流出無流入,就會引發死鎖。

或者這麼說:goroutine的非緩衝通道里頭必定要一進一出,成對出現才行。 

固然,有一個例外:

package main

func main() {
	ch := make(chan int)
	go func() {
		ch <- 1
	}()
}

執行以上代碼將會發現,居然沒有報錯。
why?
不是說好的一進一出就死鎖嗎?

仔細研究會發現,main其實根本沒等goroutine執行完,main函數本身先跑完了,因此就沒有數據流入主的goroutine,就不會被阻塞和報錯

 

goroutine的死鎖處理

有兩種辦法能夠解決:

1.把沒取走的取走即是

以下:

package main

func main() {
	cha, chb := make(chan int), make(chan int)

	go func() {
		cha <- 1 // cha通道的數據沒有被其餘goroutine讀取走,堵塞當前goroutine
		chb <- 0
	}()
	<-cha 
	<-chb // chb 等待數據的寫
}

 2.建立緩衝通道

package main

func main() {
	cha, chb := make(chan int, 3), make(chan int)

	go func() {
		cha <- 1 // cha通道的數據沒有被其餘goroutine讀取走,堵塞當前goroutine
		chb <- 0
	}()

	<-chb // chb 等待數據的寫
}

這樣的話,cha能夠緩存一個數據,cha就不會掛起當前的goroutine了。除非再放兩個進去,塞滿緩衝通道就會了。

package main

import (
	"fmt"
)

func main() {
	cha, chb := make(chan int, 3), make(chan int)

	go func() {
		cha <- 1 // cha通道的數據沒有被其餘goroutine讀取走,堵塞當前goroutine
		chb <- 0
		cha <- 1
		cha <- 1
		cha <- 1
		fmt.Println("goroutine end")
	}()

	<-chb // chb 等待數據的寫
	<-cha
	fmt.Println("main end")
}

  輸出:

main end

  

select的簡介

在golang裏頭select的功能與epoll(nginx)/poll/select的功能相似,都是堅挺IO操做,當IO操做發生的時候,觸發相應的動做。

select有幾個重要的點要強調:

1.若是有多個case均可以運行,select會隨機公平地選出一個執行,其餘不會執行
上代碼:

package main

import "fmt"

func main() {
	ch := make(chan int, 1)

	ch <- 1
	select {
	case <-ch:
		fmt.Println("select 1")
	case <-ch:
		fmt.Println("select 2")
	}
}

  輸出:

select 1 和select 2 二選一


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("黃色的羊駝")
	}
}

  輸出報錯:

.\main.go:11:7: 2 evaluated but not used
.\main.go:11:7: select case must be receive, send or assign recv

  

3.select中的default子句老是可運行的。因此沒有default的select纔會阻塞等待事件
上代碼:

package main

import "fmt"

func main() {
	ch := make(chan int, 1)
	// ch<-1   <= 注意這裏備註了。
	select {
	case <-ch:
		fmt.Println("select 1")
	default:
		fmt.Println("default")
	}
}

  輸出:

default

  

4.沒有運行的case,那麼阻塞事件發生,報錯(死鎖)

package main

import "fmt"

func main() {
	ch := make(chan int, 1)
	// ch<-1   <= 注意這裏備註了。
	select {
	case <-ch:
		fmt.Println("select 1")
	}
}

  輸出:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
	D:/GOPATH/src/study.go/main.go:9 +0x5d

  

 

select的應用場景

1.timeout 機制(超時判斷)

package main

import (
	"fmt"
	"time"
)

func main() {
	timeout := make(chan bool, 1)
	go func() {
		time.Sleep(1 * time.Second) // 休眠1s,若是超過1s還沒操做則認爲超時,通知select已經超時啦~
		timeout <- true
	}()
	ch := make(chan int)
	select {
	case <-ch:
	case <-timeout:
		fmt.Println("Timeout!")
	}
}

  輸出:

Timeout!

  

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int)
	select {
	case <-ch:
	case <-time.After(time.Second * 1): // 利用time來實現,After表明多少時間後執行輸出東西
		fmt.Println("超時啦!")
	}
}

  

2.判斷channel是否阻塞(或者說channel是否已經滿了)


 

package main

import (
	"fmt"
)

func main() {
	ch := make(chan int, 2) // 注意這裏給的容量是1
	ch <- 1
	ch <- 3
	select {
	case ch <- 5:
		fmt.Println("1")
	case ch <- 6:
		fmt.Println("2")
	default:
		fmt.Println("通道channel已經滿啦,塞不下東西了!")
	}
}

  

 

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: ", m)
				break DONE // 跳出 select 和 for 循環
			default:
				fmt.Println("default")
			}
		}
	}()

	time.Sleep(time.Second * 3)
	ch <- "stop"
}

  這邊要強調一點:退出循環必定要用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 循環
			default:
			}
		}
	DONE:
	}()

	time.Sleep(time.Second * 4)
	ch <- "stop"
}

  輸出:

1570846669
1570846670
1570846671
1570846672
stop

  

 

select死鎖

select不注意也會發生死鎖,前文有提到一個,這裏分幾種狀況,重點再次強調:

 1.若是沒有數據須要發送,select中又存在接收通道數據的語句,那麼將發送死鎖

package main

func main() {
	ch := make(chan string)
	select {
	case <-ch:
	}
}

 預防的話加default。

package main

import (
	"fmt"
)

func main() {
	ch := make(chan string)
	select {
	case <-ch:
	default:
		fmt.Println("default")
	}
}

  

2.空select,也會引發死鎖

package main

func main() {
	select {}
}

  輸出:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [select (no cases)]:
main.main()
	D:/GOPATH/src/study.go/main.go:4 +0x27
相關文章
相關標籤/搜索