GO-併發技術
併發技術1:CSP併發理論
非同步async
並行:多個任務併發執行
同步sync
序列:多個任務依次執行
阻塞block
某個併發任務由於拿不到資源沒法幹活,從而無所事事地乾等
非同步回撥async callback
A執行緒喚起B執行緒,令其幹活
同時給B一個回撥函式
命令B在幹完活以後,執行這個回撥函式
這個回撥函式會與A執行緒發生互動
A不必阻塞等待B執行的結果,AB兩個執行緒可以併發執行
利弊
- 效率高
- 回撥地獄CallbackHell,邏輯線不清晰
共享記憶體
- 多個併發執行緒通過共享記憶體的方式互動資料
- 執行緒安全問題:AB間共享的資料地址可能被C併發修改
同步鎖/資源鎖
為了解決共享記憶體所導致的執行緒安全問題,共享的記憶體地址在特定時間段被特定執行緒鎖定
加鎖期間,其它執行緒無法訪問,帶來低效率問題
死鎖
A鎖住B的資源
B鎖住A要的資源
AB同時阻塞
案例:小兩口的冷戰
- 女:鎖住女人的尊嚴,得到男人的尊嚴後才釋放
- 男:鎖住男人的尊嚴,得到女人的尊嚴後才釋放
執行緒池
- 背景:執行緒的開銷大
- 記憶體:儲存上下文資料
-
CPU:執行緒排程
為了避免無度建立執行緒(記憶體溢位OutOfMemory),在一個池中建立一堆執行緒,迴圈利用這些執行緒,用完了以後重置並丟回池中.
-
利弊
利:避免了無度建立執行緒,降低了OOM的風險 弊:用不用都佔去了一大塊記憶體開銷
執行緒併發的弊端
開執行緒佔記憶體
啥也不幹就拿走1M棧空間
1024條執行緒就佔用1G記憶體
執行緒切換佔CPU
記憶體共享不安全
加了鎖效率又低下
回撥地獄導致開發難度高
堆疊
棧
- 變數和物件的名稱
- 引用堆地址
堆
- 雜亂無章地堆放各種資料
- 沒有棧對其進行引用時,就由nil進行引用
- 被nil引用的堆地中的內容隨時可能被垃圾回收器回收
垃圾回收
- 一塊堆記憶體如果沒有被棧引用,就會被0號棧(空nil)所引用
- 一切被nil引用的對記憶體,會隨時被垃圾回收器(GarbageCollector=GC)回收
CSP模型
- CommunicatingSequentialProcess
- 可通訊的序列化程式
- 併發的程式間通過管道進行通訊
共享記憶體 VS 管道
- 記憶體共享:通過記憶體共享通訊
- 管道:通過通訊共享記憶體
管道
- 最早由CSP模型提出
- 以點對點管道代替記憶體共享實現併發程式間的資料互動
- 相比記憶體共享資料互動的相率要高很多
協程
- coroutine
- coorperte
- 協作
- IO時讓出CPU
- routine
- 事務
- 微執行緒/纖程
併發技術2:多協程
建立Goroutine
import (
"fmt"
"time"
)
func newTask() {
for {
fmt.Println("勞資是子協程")
time.Sleep(time.Second)
}
}
func main() {
//開一條協程,與主協程併發地執行newTask()
go newTask()
//主協程賴著不死,主協程如果死了,子協程也得陪著死
for {
fmt.Println("this is a main goroutine")
time.Sleep(time.Second)
}
}
出讓協程資源
通過runtime.Gosched()出讓協程資源,讓其他協程優先執行
package main
import (
"fmt"
"runtime"
)
func main() {
go func() {
for i := 0; i < 5; i++ {
fmt.Println("go")
}
}()
for i := 0; i < 2; i++ {
//讓出時間片,先讓別的協程執行,它執行完,再回來執行此協程
//(詹姆斯協程:先排檔期,你們先上)
runtime.Gosched()
fmt.Println("hello")
}
}
協程自殺
package main
import (
"fmt"
"runtime"
"time"
)
func test() {
//遺囑:臨終前說的話
defer fmt.Println("這是test的遺囑")
//自殺,觸發提前執行遺囑,暴斃,後邊的好日子不過了,呼叫它的協程也暴斃
runtime.Goexit()
//自殺了,後邊的好日子不過了
fmt.Println("生活承諾的很多美好事情...")
//到這是test的正常退出
}
func wildMan() {
for i:=0;i<6;i++{
fmt.Println("我是野人,我不喜歡約束,我討厭制約的我的主協程")
time.Sleep(time.Second)
}
}
func main() {
//一個會暴斃的協程
go func() {
fmt.Println("aaaaaaaaaaaaaa")
//test中有協程自殺程式runtime.Goexit()
test()
fmt.Println("bbbbbbbbbbbbbbb")
}()
//一個討厭主協程約束的野人協程,主協程正常結束會把她帶走
//如果主協程暴斃,則野人協程失去約束
go wildMan()
for i:=0;i<3;i++ {
time.Sleep(time.Second)
}
//主協程的暴斃,會令所有子協程失去牽制——野人永遠失去控制
//主協程暴斃的情況下,如果所有協程都結束了,程式崩潰:fatal error: no goroutines (main called runtime.Goexit) - deadlock!
runtime.Goexit()
fmt.Println("主協程正常返回,會帶走所有子協程")
}
檢視可用核心數
package main
import (
"fmt"
"runtime"
)
/*
可用核心越多,併發質量越高
*/
func main() {
//把可用的最大邏輯CPU核心數設為1,返回先前的設定
previousMaxProcs := runtime.GOMAXPROCS(1)
//獲得邏輯CPU核心數
cpu_num := runtime.NumCPU()
fmt.Println("cpu_num = ", cpu_num)//8
fmt.Println("previousMaxProcs=",previousMaxProcs)//8
for {
//主協程打0,子協程打1
go fmt.Print(1)
fmt.Print(0)
}
}
協程間公平競爭資源
package main
import (
"fmt"
"time"
)
func PrinterVII(str string) {
for _, data := range str {
fmt.Printf("%c", data)
time.Sleep(time.Second)
}
fmt.Printf("\n")
}
func person1VII() {
PrinterVII("今生註定我愛你")
}
func person2VII() {
PrinterVII("FUCKOFF")
}
func main() {
go person1VII()
go person2VII()
for {
time.Sleep(time.Second)
}
}
併發技術3:管道通訊
channel 介紹
channel 提供了一種通訊機制,通過它,一個 goroutine 可以想另一 goroutine 傳送訊息。channel 本身還需關聯了一個型別,也就是 channel 可以傳送資料的型別。例如: 傳送 int 型別訊息的 channel 寫作 chan int 。
channel 建立
channel 使用內建的 make 函式建立,下面宣告瞭一個 chan int 型別的 channel:
ch := make(chan int)
c和 map 類似,make 建立了一個底層資料結構的引用,當賦值或引數傳遞時,只是拷貝了一個 channel 引用,指向相同的 channel 物件。和其他引用型別一樣,channel 的空值為 nil 。使用 == 可以對型別相同的 channel 進行比較,只有指向相同物件或同為 nil 時,才返回 true
channel 的讀寫操作
ch := make(chan int)
// write to channel
ch <- x
// read from channel
x <- ch
// another way to read
x = <- chnnel 一定要初始化後才能進行讀寫操作,否則會永久阻塞。
channel 一定要初始化後才能進行讀寫操作,否則會永久阻塞。
關閉 channel
golang 提供了內建的 close 函式對 channel 進行關閉操作。
ch := make(chan int)
close(ch)
有關 channel 的關閉,你需要注意以下事項:
- 關閉一個未初始化(nil) 的 channel 會產生 panic
- 重複關閉同一個 channel 會產生 panic
- 向一個已關閉的 channel 中傳送訊息會產生 panic
- 從已關閉的 channel 讀取訊息不會產生 panic,且能讀出 channel中還未被讀取的訊息,若訊息均已讀出,則會讀到型別的零值。從一個已關閉的 channel 中讀取訊息永遠不會阻塞,並且會返回一個為
- false 的 ok-idiom,可以用它來判斷 channel 是否關閉
- 關閉 channel 會產生一個廣播機制,所有向 channel 讀取訊息的 goroutine 都會收到訊息
ch := make(chan int, 10)
ch <- 11
ch <- 12
close(ch)
for x := range ch {
fmt.Println(x)
}
x, ok := <- ch
fmt.Println(x, ok)
-----
output:
11
12
0 false
channel 的型別
channel 分為不帶快取的 channel 和帶快取的 channel。
無快取的 channel
從無快取的 channel 中讀取訊息會阻塞,直到有 goroutine 向該 channel 中傳送訊息;同理,向無快取的 channel 中傳送訊息也會阻塞,直到有 goroutine 從 channel 中讀取訊息。
通過無快取的 channel 進行通訊時,接收者收到資料 happens before 傳送者 goroutine 喚醒
有快取的 channel
有快取的 channel 的宣告方式為指定 make 函式的第二個引數,該引數為 channel 快取的容量
ch := make(chan int, 10)
有快取的 channel 類似一個阻塞佇列(採用環形陣列實現)。當快取未滿時,向 channel 中傳送訊息時不會阻塞,當快取滿時,傳送操作將被阻塞,直到有其他 goroutine 從中讀取訊息;相應的,當 channel 中訊息不為空時,讀取訊息不會出現阻塞,當 channel 為空時,讀取操作會造成阻塞,直到有 goroutine 向 channel 中寫入訊息。
ch := make(chan int, 3)
// blocked, read from empty buffered channel
<- ch
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
// blocked, send to full buffered channel
ch <- 4
通過 len 函式可以獲得 chan 中的元素個數,通過 cap 函式可以得到 channel 的快取長度。
例項
通過channel實現同步
匯入依賴
import (
"fmt"
"time"
)
//語法點①:建立int型別的無快取管道
//var ch = make(chan int)
var ch = make(chan int,0)
func Printer(str string) {
for _, data := range str {
fmt.Printf("%c", data)
time.Sleep(time.Second)
}
fmt.Printf("\n")
}
func person1() {
//列印完需要7秒鐘
//勞資不列印完是不會往管道中塞資料的,阻塞不死你丫的
Printer("今生註定我愛你")
//箭頭指向管道內部,寫資料
//在打完今生註定我愛你(耗時7秒鐘)後,才寫入資料
//語法點②:向管道里寫資料,無論讀寫,箭頭只能朝左
//語法點⑤:如果管道快取已滿,則阻塞等待至有人取出資料騰出空間,再寫入
ch <- 666
}
func person2() {
//箭頭指向管道外面,代表從管道中拿出資料,讀資料
//語法點③:從管理取出資料,但不不接收
//語法點⑥:管道里沒資料時,阻塞死等
<-ch
//語法點④:從管理取出資料,且使用data變數接收
//data:=<-ch
//fmt.Println("讀出資料:",data)
//終於媽的可以列印了
Printer("FUCKOFF")
}
func main() {
go person1()
go person2()
//主協程賴著不死
for {
time.Sleep(time.Second)
}
}
通過channel實現同步和資料互動
package main
import (
"fmt"
"time"
)
func main() {
//建立無快取管道
ch := make(chan string)
//5、主協程結束
defer fmt.Println("主協程也結束")
//子協程負責寫資料
go func() {
//3、結束任務
defer fmt.Println("子協程呼叫完畢")
//1、緩緩列印2次序號
for i := 0; i < 2; i++ {
fmt.Println("子協程 i= ", i)
time.Sleep(time.Second)
}
//2、向管道傳送資料
ch <- "我是子協程,工作完畢"
}()
//4、阻塞接收
str := <-ch
fmt.Println("str = ", str)
}
無緩衝的channel
package main
import (
"fmt"
"time"
)
func main() {
//建立一個無緩衝的管道
ch := make(chan int, 1)
//長度0,快取能力0
fmt.Printf("len(ch) = %d, cap(ch)=%d\n", len(ch), cap(ch))
go func() {
//向管道中存入0,被阻塞,存入1,被阻塞,存入2
for i := 0; i < 3; i++ {
fmt.Println("子協程: i = ", i)
ch <- i
fmt.Println("5秒以內被列印出來給傑神100萬!")
}
}()
//睡眠2秒
time.Sleep(5 * time.Second)
//讀取0,被阻塞,讀取1,被阻塞,讀取2
for i := 0; i < 3; i++ {
num := <-ch
fmt.Println("num = ", num)
}
}
有快取的channel
package main
import (
"fmt"
"time"
)
func main() {
//建立3快取的管道
ch := make(chan int, 3)
//長度0,快取能力3(即使沒人讀,也能寫入3個值)
fmt.Printf("len(ch) = %d, cap(ch) = %d\n", len(ch), cap(ch))
//一次性存入3個:012,3456789
go func() {
for i := 0; i < 10; i++ {
ch <- i
fmt.Printf("子協程存入[%d]: len(ch) = %d, cap(ch) = %d\n", i, len(ch), cap(ch))
//time.Sleep(1 * time.Second)
}
}()
//time.Sleep(5 * time.Second)
//一次性讀取3個:012,345,678,9
for i := 0; i < 10; i++ {
num := <-ch
fmt.Println("num = ", num)
}
time.Sleep(1*time.Nanosecond)
}
併發技術4:同步排程
等待組
import (
"time"
"fmt"
"sync"
)
//主協程等待子協程全部結束:通過管道阻塞
func main0() {
chanRets := make(chan int, 3)
fmt.Println(len(chanRets),cap(chanRets))
for i := 0; i < 3; i++ {
go func(index int) {
ret := getFibonacci(index)
chanRets <- ret
fmt.Println(index,ret)
}(i)
}
for{
if len(chanRets)==3{
time.Sleep(time.Nanosecond)
break
}
}
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
//等待組中協程數+1(主協程中)
wg.Add(1)
go func(index int) {
ret := getFibonacci(index)
fmt.Println(index,ret)
//等待組中協程數-1(子協程中)
wg.Done()
}(i)
}
//阻塞至等待組中的協程數為0
wg.Wait()
}
func getFibonacci(n int) int {
x, y := 1, 1
for i := 0; i < n; i++ {
x, y = y, x+y
}
<-time.After(3 * time.Second)
return x
}
互斥鎖案例1
package main
import (
"fmt"
"time"
"sync"
)
func main() {
//必須保證併發安全的資料
type Account struct {
money float32
}
var wg sync.WaitGroup
account := Account{1000}
fmt.Println(account)
//資源互斥鎖(誰搶到鎖,誰先訪問資源,其他人阻塞等待)
//全域性就這麼一把鎖,誰先搶到誰操作,其他人被阻塞直到鎖釋放
var mt sync.Mutex
//銀行卡取錢
wg.Add(1)
go func() {
//拿到互斥鎖
mt.Lock()
//加鎖的訪問
fmt.Println("取錢前:",account.money)
account.money -= 500
time.Sleep(time.Nanosecond)
fmt.Println("取錢後:",account.money)
wg.Done()
//釋放互斥鎖
mt.Unlock()
}()
//存摺存錢
wg.Add(1)
go func() {
//拿到互斥鎖(如果別人先搶到,則阻塞等待)
mt.Lock()
fmt.Println("存錢前:",account.money)
account.money += 500
time.Sleep(time.Nanosecond)
fmt.Println("存錢後:",account.money)
wg.Done()
//釋放互斥鎖
mt.Unlock()
}()
wg.Wait()
}
互斥鎖案例2
package main
import (
"sync"
"fmt"
"time"
)
//必須保證併發安全的資料
type Account struct {
name string
money float32
//定義該資料的互斥鎖
mt sync.Mutex
}
//本方法不能被併發執行——併發安全的
func (a *Account) saveGet(amount float32) {
//先將資源鎖起來
a.mt.Lock()
//執行操作
fmt.Println("操作前:", a.money)
a.money += amount
fmt.Println("操作後:", a.money)
<-time.After(3 * time.Second)
//釋放資源
a.mt.Unlock()
}
//本方法可以被併發執行——不是併發安全的,無此必要
func (a *Account) getName() string {
return a.name
}
func main() {
a := Account{name: "張全蛋", money: 1000}
var wg sync.WaitGroup
wg.Add(1)
go func() {
//呼叫一個加鎖的方法(同步)
a.saveGet(500)
wg.Done()
}()
wg.Add(1)
go func() {
//呼叫一個加鎖的方法(同步)
a.saveGet(-500)
wg.Done()
}()
for i:=0;i<3 ;i++ {
wg.Add(1)
go func() {
//呼叫一個普通的沒有訪問鎖的方法(非同步)
fmt.Println(a.getName())
wg.Done()
}()
}
wg.Wait()
}
通過訊號量控制併發數
package main
import (
"fmt"
"time"
"sync"
)
/*訊號量:通過控制管道的“頻寬”(快取能力)控制併發數*/
func main() {
//定義訊號量為5“頻寬”的管道
sema = make(chan int, 5)
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(index int) {
ret := getPingfangshu(index)
fmt.Println(index, ret)
wg.Done()
}(i)
}
wg.Wait()
}
//該函式只允許5併發執行
var sema chan int
func getPingfangshu(i int) int {
sema <- 1
<-time.After(2 * time.Second)
<- sema
return i
}
併發技術5:死鎖問題
1. 同一個goroutine中,使用同一個 channel 讀寫
package main
func main(){
ch:=make(chan int) //這就是在main程裡面發生的死鎖情況
ch<-6 // 這裡會發生一直阻塞的情況,執行不到下面一句
<-ch
}
這是最簡單的死鎖情況
看執行結果
2. 2個 以上的go程中, 使用同一個 channel 通訊。 讀寫channel 先於 go程建立。
package main
func main(){
ch:=make(chan int)
ch<-666 //這裡一直阻塞,執行不到下面
go func (){
<-ch //這裡雖然建立了子go程用來讀出資料,但是上面會一直阻塞執行不到下面
}()
}
這裡如果想不成為死鎖那匿名函式go程就要放到ch<-666這條語句前面
3. 2個以上的go程中,使用多個 channel 通訊。 A go 程 獲取channel 1 的同時,嘗試使用channel 2, 同一時刻,B go 程 獲取channel 2 的同時,嘗試使用channel 1
package main
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() { //匿名子go程
for {
select { //這裡互相等對方造成死鎖
case <-ch1: //這裡ch1有資料讀出才會執行下一句
ch2 <- 777
}
}
}()
for { //主go程
select {
case <-ch2 : //這裡ch2有資料讀出才會執行下一句
ch1 <- 999
}
}
}
第三種是互相等對方造成死鎖
4.在go語言中, channel 和 讀寫鎖、互斥鎖 儘量避免交叉混用。——“隱形死鎖”。如果必須使用。推薦藉助“條件變數”
package main
import (
"runtime"
"math/rand"
"time"
"fmt"
"sync"
)
// 使用讀寫鎖
var rwMutex2 sync.RWMutex
func readGo2(idx int, in <-chan int) { // 讀go程
for {
time.Sleep(time.Millisecond * 500) // 放大實驗現象// 一個go程可以讀 無限 次。
rwMutex2.RLock() // 讀模式加 讀寫鎖
num := <-in // 從 公共的 channel 中獲取資料
fmt.Printf("%dth 讀 go程,讀到:%d\n", idx, num)
rwMutex2.RUnlock() // 解鎖 讀寫鎖
}
}
func writeGo2(idx int, out chan<- int) {
for { // 一個go程可以寫 無限 次。
// 生產一個隨機數
num := rand.Intn(500)
rwMutex2.Lock() // 寫模式加 讀寫鎖
out <- num
fmt.Printf("-----%dth 寫 go程,寫入:%d\n", idx, num)
rwMutex2.Unlock() // 解鎖 讀寫鎖
//time.Sleep(time.Millisecond * 200) // 放大實驗現象
}
}
func main() {
// 播種隨機數種子。
rand.Seed(time.Now().UnixNano())
// 建立 模擬公共區的 channel
ch := make(chan int, 5)
for i:=0; i<5; i++ { // 同時建立 N 個 讀go程
go readGo2(i+1, ch)
}
for i:=0; i<5; i++ { // 同時建立 N 個 寫go程
go writeGo2(i+1, ch)
}
for { // 防止 主 go 程 退出
runtime.GC()
}
}
這是一種隱形的死鎖,我們來看一下結果:
相關文章
- 高併發技術
- 併發技術1:CSP併發理論
- 併發技術中同步
- 併發技術4:讀寫鎖
- 併發技術3:定時器定時器
- 併發技術4:同步排程
- 併發技術3:管道通訊
- 併發技術2:多協程
- 高併發設計技術方案
- 【併發技術04】執行緒技術之死鎖問題執行緒
- 【併發技術03】傳統執行緒互斥技術—synchronized執行緒synchronized
- 併發技術5:死鎖問題
- 【併發技術02】傳統執行緒技術中的定時器技術執行緒定時器
- Java併發技術05:傳統執行緒同步通訊技術Java執行緒
- 多執行緒與併發-----Lock鎖技術執行緒
- Elasticsearch技術解析與實戰(六)Elasticsearch併發Elasticsearch
- 「分散式技術專題」併發系列一:基於加鎖的併發控制分散式
- 「分散式技術專題」併發系列二:基於時間的併發控制分散式
- 「分散式技術專題」併發系列三:樂觀併發控制之理論研究分散式
- 「分散式技術專題」併發系列三:樂觀併發控制之原型系統分散式原型
- 看懂這篇,才能說了解併發底層技術
- 「分散式技術專題」併發系列三:樂觀併發控制之生產系統分散式
- Java併發基礎02:傳統執行緒技術中的定時器技術Java執行緒定時器
- 億級流量高併發春晚互動前端技術揭秘前端
- Mysql核心技術:用NOSql給高併發系統加速MySql
- 如何才能夠系統地學習Java併發技術?Java
- 高併發核心技術 - 冪等性 與 分散式鎖分散式
- 高併發的核心技術 - 訊息中介軟體(MQ)MQ
- [仁潤雲技術團隊]併發程式設計-(2)併發程式設計的目標程式設計
- 【併發技術01】傳統執行緒技術中建立執行緒的兩種方式執行緒
- 高併發數字資產交易平臺開發技術架構架構
- 九種高效能可用高併發的技術架構架構
- 高併發業務下的無損技術方案設計
- 「架構技術專題」9種高效能高可用高併發的技術架構(5)架構
- go-反射Go反射
- 「分散式技術專題」併發系列三:樂觀併發控制之原型系統(分散式驗證)分散式原型
- Java併發基礎04:執行緒技術之死鎖問題Java執行緒
- 雲流送技術原理是什麼,如何實現多併發?