多線程同步問題
- 互斥鎖
- 互斥鎖的本質(zhì)是當一個goroutine訪問的時候, 其它goroutine都不能訪問
- 這樣就能實現(xiàn)資源同步, 但是在避免資源競爭的同時也降低了程序的并發(fā)
性能,程序由原來的并發(fā)執(zhí)行變成了串行
- 打印案例
- 沒有添加互斥鎖, 那么兩個人都有機會輸出自己的內(nèi)容
- 添加互斥鎖,只有當一個輸出完畢的時候,另一個才能輸出
package main
import (
"fmt"
"sync"
"time"
)
//創(chuàng)建一個互斥鎖
//可以讓程序從并發(fā)狀態(tài)變成并行狀態(tài)
var lock = sync.Mutex{}
//定義一個打印字符的函數(shù)
func myprint(str string) {
//添加鎖
lock.Lock()
for _, value := range str {
time.Sleep(time.Microsecond *300)
fmt.Printf("%c", value)
}
//解鎖
lock.Unlock()
}
//調(diào)用者1
func person1() {
myprint("hello")
}
//調(diào)用者2
func person2() {
myprint("world")
}
func main() {
//開啟go程
go person1()
go person2()
//保證主線程不退出,程序不結(jié)束
for {
;
}
}
生產(chǎn)者與消費者
- 生產(chǎn)者消費者模型
- 某個模塊(函數(shù))負責(zé)生產(chǎn)數(shù)據(jù), 這些數(shù)據(jù)由另一個模塊來負責(zé)處理
- 一般生產(chǎn)者消費者模型包含三個部分
生產(chǎn)者、緩沖區(qū)、消費者
- 沒有緩沖區(qū),消費者發(fā)生變化, 會直接影響生產(chǎn)者, 耦合性太強
- 添加緩沖區(qū)可以提高效率
生產(chǎn)者和消費者資源競爭問題
- 生產(chǎn)者生產(chǎn)產(chǎn)比較慢, 而消費比較快, 就會導(dǎo)致消費者消費到錯誤數(shù)據(jù)
package main
import (
"fmt"
"math/rand"
"time"
)
//定義數(shù)組模擬緩沖區(qū)
var arr [10]int
//定義模擬生產(chǎn)者函數(shù)
func producter() {
//定義隨機因子
rand.Seed(time.Now().UnixNano())
//產(chǎn)生隨機數(shù)
for i := 0;i < 10 ;i++ {
num := rand.Intn(100)
fmt.Println("生產(chǎn)者生產(chǎn)了", num)
//將生產(chǎn)的數(shù)據(jù)放入緩沖區(qū)中
arr[i] = num
time.Sleep(time.Millisecond * 300)
}
}
//定義函數(shù)模擬消費者
func consumer(){
for i := 0;i < 10 ;i++ {
value := arr[i]
fmt.Println("------消費者消費了",value)
}
}
func main() {
// 我們想要的是, 只有生產(chǎn)者生產(chǎn)了, 我們才能消費
// 注意點: 在多go程中, 如果生產(chǎn)者生產(chǎn)的太慢, 那么消費者就會消費到錯誤的數(shù)據(jù)
go producter()
// 注意點: 看上去通過給生產(chǎn)者以及消費者同時加鎖就能解決, 只有生產(chǎn)完了才能消費
// 但是取決于誰想執(zhí)行加鎖操作, 所以不完美
go consumer()
for {
;
}
}
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
//創(chuàng)建一把互斥鎖
var lock = sync.Mutex{}
//定義數(shù)組模擬緩沖區(qū)
var arr [10]int
//定義模擬生產(chǎn)者函數(shù)
func producter() {
/*
為什么在生產(chǎn)者和消費者中都上鎖之后, 就可以實現(xiàn)生產(chǎn)完再消費?
因為生產(chǎn)者和消費者中的鎖都是同一把, 都是全局變量lock
調(diào)用Lock()函數(shù)的作用: 是修改Mutex結(jié)構(gòu)體中的state屬性的值, 將它改為一個非0的值
每次上鎖的時候都會判斷有沒有被鎖定, 如果已經(jīng)鎖定就不鎖了, 并且不會執(zhí)行后面的代碼
調(diào)用Unlock函數(shù)的作用: 是修改Mutex結(jié)構(gòu)體中的state屬性的值, 將它改為0
*/
//上鎖
// 這里鎖定的是當前go程, 也就是當前函數(shù)
// 意味著其它的go程不能執(zhí)行當前的函數(shù), 只有當前鎖定這個函數(shù)的go程才能執(zhí)行這個函數(shù)
lock.Lock()
//定義隨機因子
rand.Seed(time.Now().UnixNano())
//產(chǎn)生隨機數(shù)
for i := 0;i < 10 ;i++ {
num := rand.Intn(100)
fmt.Println("生產(chǎn)者生產(chǎn)了", num)
//將生產(chǎn)的數(shù)據(jù)放入緩沖區(qū)中
arr[i] = num
//time.Sleep(time.Millisecond * 300)
}
//解鎖
lock.Unlock()
}
//定義函數(shù)模擬消費者
func consumer(){
//上鎖
lock.Lock()
for i := 0;i < 10 ;i++ {
value := arr[i]
fmt.Println("------消費者消費了",value)
}
//解鎖
lock.Unlock()
}
func main() {
// 我們想要的是, 只有生產(chǎn)者生產(chǎn)了, 我們才能消費
// 注意點: 在多go程中, 如果生產(chǎn)者生產(chǎn)的太慢, 那么消費者就會消費到錯誤的數(shù)據(jù)
go producter()
// 注意點: 看上去通過給生產(chǎn)者以及消費者同時加鎖就能解決, 只有生產(chǎn)完了才能消費
// 但是取決于誰想執(zhí)行加鎖操作, 所以不完美
go consumer()
for {
;
}
}
Go語言管道
管道的基本使用
- Channel的本質(zhì)是一個隊列
- Channel是線程安全的, 也就是自帶鎖定功能
- Channel聲明和初始化
var 變量名稱 chan 數(shù)據(jù)類型
make(chan 數(shù)據(jù)類型, 容量)
- 管道和切片/字典一樣,必須創(chuàng)建后才能使用,否則會報錯
- Channel和切片還有字典一樣, 是引用類型,是地址傳遞
package main
import "fmt"
func main() {
/*
1.什么是管道:
管道就是一個隊列, 具備先進先出的原則
是線程安全的, 也就是自帶鎖定功能
2.管道作用:
在Go語言的協(xié)程中, 一般都使用管道來保證多個協(xié)程的同步, 或者多個協(xié)程之間的通訊
3.如何聲明一個管道, 和如何創(chuàng)建一個管道
管道在Go語言中和切片/字典一樣也是一種數(shù)據(jù)類型
管道和切片/字典非常相似, 都可以用來存儲數(shù)據(jù), 都需要make之后才能使用
3.1管道聲明格式:
var 變量名稱 chan 數(shù)據(jù)類型
var myCh chan int
如上代碼的含義: 聲明一個名稱叫做myCh的管道變量, 管道中可以存儲int類型的數(shù)據(jù)
3.2管道的創(chuàng)建:
make(chan 數(shù)據(jù)類型, 容量)
myCh = make(chan int, 3);
路上代碼的含義: 創(chuàng)建一個容量為3, 并且可以保存int類型數(shù)據(jù)的管道
4.管道的使用
4.1如何往管道中存儲(寫入)數(shù)據(jù)?
myCh<-被寫入的數(shù)據(jù)
4.2如何從管道中獲取(讀取)數(shù)據(jù)?
<-myCh
對管道的操作是IO操作
例如: 過去的往文件中寫入或者讀取數(shù)據(jù), 也是IO操作
例如: 過去的往屏幕上輸出內(nèi)容, 或者從屏幕獲取內(nèi)容, 也是IO操作
stdin / stdout / stderr
注意點:
和切片不同, 在切片中make函數(shù)的第二個參數(shù)表示的切片的長度(已經(jīng)存儲了多少個數(shù)據(jù)),
而第三個參數(shù)才是指定切片的容量
但是在管道中, make函數(shù)的第二個參數(shù)就是指定管道的容量, 默認長度就是0
*/
//1.定義一個管道
//var myChan chan int
//2.使用make創(chuàng)建管道
myChan := make(chan int, 3)
//3.往管道中存儲數(shù)據(jù)
myChan<-1
myChan<-2
myChan<-3
//從管道中取出數(shù)據(jù)
fmt.Println(<-myChan)
fmt.Println(<-myChan)
fmt.Println(<-myChan)
//定義一個管道
var myChan chan int
//直接使用管道
//注意點: 會報錯,管道定義完成后不創(chuàng)建是無法直接使用的
//myChan<-666
//fmt.Println(<-myChan)
//創(chuàng)建管道
myChan = make(chan int, 3)
//只要往管道中寫入了數(shù)據(jù), 那么len就會增加
myChan <- 2
//fmt.Println("len = ", len(myChan), "cap = ", cap(myChan))
myChan <- 4
//fmt.Println("len = ", len(myChan), "cap = ", cap(myChan))
myChan <- 6
//fmt.Println("len = ", len(myChan), "cap = ", cap(myChan))
//注意點: 如果len等于cap, 那么就不能往管道中再寫入數(shù)據(jù)了, 否則會報錯
//myChan <- 8
//管道未寫入數(shù)據(jù),使用管道去取數(shù)據(jù)會報錯
//從管道中取數(shù)據(jù),len會減少
//<-myChan
fmt.Println(<-myChan)
fmt.Println("len=",len(myChan),"cap = ", cap(myChan))
fmt.Println(<-myChan)
fmt.Println("len=",len(myChan),"cap = ", cap(myChan))
fmt.Println(<-myChan)
fmt.Println("len=",len(myChan),"cap = ", cap(myChan))
//注意點: 取數(shù)據(jù)個數(shù)也不可以超出寫入的數(shù)據(jù)個數(shù),否則會報錯
//fmt.Println(<-myChan)
}
管道的遍歷和關(guān)閉
- 管道遍歷推薦兩種方式
for..range方法遍歷
for死循環(huán)遍歷
package main
import "fmt"
func main() {
/*
管道的遍歷:
可以使用for循環(huán), 也可以使用 for range循環(huán), 以及死循環(huán)來遍歷
但是更推薦使用后兩者
因為在企業(yè)開發(fā)中, 有可能我們不知道管道中具體有多少條數(shù)據(jù), 所以如果利用for循環(huán)來遍歷, 那么無法確定遍歷的次數(shù), 并且如果遍歷的次數(shù)太多, 還會報錯
*/
//創(chuàng)建一個管道
myChan := make(chan int, 3)
//往管道中寫入數(shù)據(jù)
myChan <- 2
myChan <- 4
myChan <- 6
//注意點: 如果不關(guān)閉管道,遍歷管道會報錯
close(myChan)
//第一種方法遍歷管道
//for value := range myChan {
// fmt.Println(value)
//}
//第二種方法遍歷管道
//注意點: 如果被遍歷的管道沒有關(guān)閉, 那么會報錯
// 如果管道沒有被關(guān)閉, 那么會將true返回給ok, 否則會將false返回給Ok
for {
if v,ok := <-myChan; ok {
fmt.Println(v)
fmt.Println(ok)
}else {
break
}
}
//注意點: 管道關(guān)閉后無法往里面寫入數(shù)據(jù),會報錯,但是可以讀取數(shù)據(jù)不會報錯
myChan := make(chan int,3)
close(myChan)
//往管道中寫入數(shù)據(jù)
//myChan<-1
//myChan<-2
//myChan<-3
//從管道中讀取數(shù)據(jù)
<-myChan
}
管道阻塞現(xiàn)象
-
阻塞現(xiàn)象只會發(fā)生在go程中運行才會發(fā)生,在普通線程中不會發(fā)生這種現(xiàn)象
package main
import "fmt"
//定義一個管道
var myChan = make(chan int,2)
func test() {
/*myChan<-1
myChan<-2
fmt.Println("管道滿之前的代碼")
//這里不會報錯, 會阻塞, 等到將管道中的數(shù)據(jù)讀出去之后, 有的新的空間再往管道中寫
myChan<-3
fmt.Println("管道滿之后的代碼")*/
//這里不會報錯,會阻塞,等到有人往管道中寫入數(shù)據(jù)之后,有新的數(shù)據(jù)之后才會讀取
fmt.Println("讀取之前的代碼")
<-myChan
fmt.Println("讀取之后的代碼")
}
func main() {
/*
單獨在主線程中操作管道, 寫滿了會報錯, 沒有數(shù)據(jù)去讀取也會報錯
只要在go程中操作管道, 無論有沒有寫滿, 無論有沒有數(shù)據(jù)都會發(fā)生管道阻塞的現(xiàn)象
*/
go test()
for {
;
}
}
package main
import (
"fmt"
"time"
)
//創(chuàng)建一個管道
var myChan = make(chan bool)
func printer(str string) {
for _, value := range str {
fmt.Printf("%c", value)
time.Sleep(time.Microsecond * 300)
}
}
func person1() {
printer("hello")
//往管道中寫入數(shù)據(jù)
//只有printer函數(shù)執(zhí)行完,才會往管道中寫入數(shù)據(jù)
myChan<-true
}
func person2() {
//從管道中讀取數(shù)據(jù)
//只有管道中有數(shù)據(jù)才會讀取,否則會阻塞
<-myChan
printer("world")
}
func main() {
go person1()
go person2()
for {
;
}
}
- 利用管道阻塞實現(xiàn)生產(chǎn)者和消費者模型
package main
import (
"fmt"
"math/rand"
"time"
)
//定義管道模擬緩沖區(qū)
var myChan = make(chan int, 10)
//定義生產(chǎn)者函數(shù)
func producter() {
//定義隨機因子
rand.Seed(time.Now().UnixNano())
//生成隨機數(shù)
for i := 0; i < 10; i++ {
num := rand.Intn(100)
fmt.Println("生產(chǎn)者生產(chǎn)了", num)
//將生產(chǎn)的數(shù)據(jù)存入到管道中
myChan <- num
}
}
//定義函數(shù)模擬消費者
func customer() {
//從管道中讀取數(shù)據(jù)
for i := 0; i < 10; i++ {
num := <-myChan
fmt.Println("----消費者消費了", num)
}
}
func main() {
//創(chuàng)建兩個go程
//多個生產(chǎn)者和多個消費者
go producter()
go producter()
go customer()
go customer()
for {
;
}
}
無緩沖區(qū)管道
package main
import "fmt"
func main() {
/*
// 管道總結(jié):
// 管道一般都在go程中使用, 不會直接在主線程中使用, 無論是有緩沖的還是沒有緩沖的
// 只要是在go程中使用, 無論是有緩沖的還是沒有緩沖的, 都會出現(xiàn)阻塞現(xiàn)象
*/
//創(chuàng)建無緩沖管道
// 注意點:
// 沒有緩沖的管道不能直接存儲數(shù)據(jù)
// 沒有緩沖的管道不能直接獲取數(shù)據(jù)
// 注意點:
// 想使用沒有緩沖的管道, 必須保證讀和寫同時存在, 而且還必須保證讀和寫是在不同的go程中,至少有一個在go程中
// 并且讀必須寫在寫的前面
//myChan := make(chan int,0)
// 沒有緩沖的管道不能直接獲取數(shù)據(jù)
//fmt.Println(<-myChan)
// 沒有緩沖的管道不能直接存儲數(shù)據(jù)
//myChan<-1
/*//讀取管道在go程中
go func() {
fmt.Println("讀之前的代碼")
fmt.Println(<-myChan)
fmt.Println("讀之前的代碼")
}()
//寫入管道在主線程中
func(){
time.Sleep(time.Second * 5)
fmt.Println("寫之前的代碼")
myChan<-2
fmt.Println("寫之后的代碼")
}()*/
//無緩沖管道單個使用
//如果是在go程中使用無緩沖的管道, 那么就可以單個使用(只有讀, 或者只有寫)
myChan := make(chan int,0)
go func() {
//只讀不會報錯
//<-myChan
//只寫不會報錯
fmt.Println("寫之前的代碼")
myChan<-1
fmt.Println("寫之后的代碼")
}()
for {
;
}
}
- 利用無緩沖區(qū)管道解決主線程結(jié)束問題
package main
import "fmt"
func main() {
//創(chuàng)建一個有緩沖管道
myChan := make(chan int,3)
//創(chuàng)建一個無緩沖管道
exitChan := make(chan bool,0)
//往管道中存儲數(shù)據(jù)
go func() {
for i := 0; i < 3; i++ {
myChan<-i
fmt.Println("生產(chǎn)了",i)
}
//無緩沖管道存儲數(shù)據(jù)
exitChan<-true
}()
<-exitChan
//for {
// ;
//}
}
單向管道
- 默認情況下所有的管道都是雙向的管道(可讀可寫),那么在企業(yè)開發(fā)中, 我們可能會需要將管道作為函數(shù)的參數(shù), 并且還需要限制函數(shù)中如何使用管道,,那么這個時候我們就可能會使用單向管道
- 雙向管道格式
- 單向管道格式
var myCh chan<- int; 只寫的管道
var myCh <-chan int; 只讀的管道
-
注意點:
- 雙向管道可以轉(zhuǎn)換為單向的管道
- 但是單向的管道不能轉(zhuǎn)換為雙向的管道
package main
import "fmt"
func main() {
/*
默認情況下所有的管道都是雙向的管道(可讀可寫)
那么在企業(yè)開發(fā)中, 我們可能會需要將管道作為函數(shù)的參數(shù), 并且還需要限制函數(shù)中如何使用管道,
那么這個時候我們就可能會使用單向管道
雙向格式:
var myCh chan int;
myCh = make(chan int, 5)
myCh = make(chan int)
單向格式:
var myCh chan<- int; 只寫的管道
var myCh <-chan int; 只讀的管道
雙向管道可以轉(zhuǎn)換為單向的管道
但是單向的管道不能轉(zhuǎn)換為雙向的管道
*/
//定義一個雙向管道
myChan := make(chan int,3)
//定義一個只讀的單向管道
var myChan1 <-chan int
//定義一個只寫的單向管道
//var myChan2 chan<- int
/*//將雙向管道賦給只讀的單向管道
myChan1 = myChan
fmt.Println(myChan1)
//將雙向管道賦給只寫的單向管道
myChan2 = myChan
fmt.Println(myChan2)*/
//將單向管道賦給雙向管道
//會報錯
//myChan = myChan1
//fmt.Println(myChan)
}
package main
import (
"fmt"
"math/rand"
"time"
)
//定義一個生產(chǎn)者
func producter(buff chan <- int) {
//定義隨機因子
rand.Seed(time.Now().UnixNano())
//生成隨機數(shù)
for i := 0;i < 5 ;i++ {
num := rand.Intn(100)
//將產(chǎn)生的隨機數(shù)存儲到管道中
buff <- num
fmt.Println("生產(chǎn)者生產(chǎn)了", num)
}
}
//定義消費者函數(shù)
func consumer(buff <- chan int, exitChan chan <- int ) {
for i := 0;i < 5 ;i++ {
//讀取管道中的數(shù)據(jù)
num := <-buff
fmt.Println("------消費者消費", num)
}
//利用管道阻塞解決死循環(huán)問題
exitChan<- 666
}
func main() {
//定義兩個雙向管道
myChan := make(chan int, 5)
exitChan := make(chan int)
//開啟兩個go程
go producter(myChan)
go consumer(myChan,exitChan)
<-exitChan
}
select選擇結(jié)構(gòu)
- 在企業(yè)開發(fā)中, 一般情況下使用select都是用于同時消費多個管道中數(shù)據(jù)
- 在企業(yè)開發(fā)中, 一般情況下select中的default不用寫
- 在企業(yè)開發(fā)中, 一般情況下使用select來控制退出主線程
- 在企業(yè)開發(fā)中, 一般情況下使用select來處理超時
package main
import (
"fmt"
"time"
)
func main() {
// 1.創(chuàng)建一個管道
myCh1 := make(chan int, 5)
//myCh2 := make(chan int, 5)
//exitCh := make(chan bool)
// 2.開啟一個協(xié)程生產(chǎn)數(shù)據(jù)
go func() {
time.Sleep(time.Second * 5)
for i := 0; i < 10 ; i++ {
myCh1<-i
fmt.Println("生產(chǎn)者1生產(chǎn)了", i)
}
close(myCh1)
//exitCh<-true
}()
/*
go func() {
time.Sleep(time.Second * 5)
for i := 0; i < 10 ; i++ {
myCh2<-i
fmt.Println("生產(chǎn)者2生產(chǎn)了", i)
}
close(myCh2)
}()
// 2.在主線程中消費數(shù)據(jù)
//for i := 0; i < 10 ; i++ {
// num := <-myCh
// fmt.Println("------消費者消費了", num)
//}
//for num := range myCh{
// fmt.Println("------消費者消費了", num)
//}
*/
// 注意點: 在企業(yè)開發(fā)中, 一般情況下使用select都是用于同時消費多個管道中數(shù)據(jù)
// 在企業(yè)開發(fā)中, 一般情況下select中的default不用寫
// 在企業(yè)開發(fā)中, 一般情況下使用select來控制退出主線程
// 在企業(yè)開發(fā)中, 一般情況下使用select來處理超時
for{
//fmt.Println("start")
select {
case num1 := <-myCh1:
fmt.Println("------消費者消費了myCh1", num1)
//case num2 := <-myCh2:
// fmt.Println("------消費者消費了myCh2", num2)
//case <-exitCh:
// return
case <-time.After(3):
fmt.Println("超時了")
return
//default:
// fmt.Println("生產(chǎn)者還沒有生產(chǎn)好數(shù)據(jù)")
}
//fmt.Println("=====================")
time.Sleep(time.Millisecond)
}
fmt.Println("程序結(jié)束了")
}
管道是地址傳遞
package main
import "fmt"
func main() {
//定義一個雙向有緩沖區(qū)管道
var myChan = make(chan int,3)
fmt.Println(myChan) //0xc00007c080
fmt.Printf("%p\n", myChan) //0xc00007c080
fmt.Printf("%p\n", &myChan) //0xc000072018
//管道是地址傳遞
//定義一個單向管道
var myChan2 chan <- int
myChan2 = myChan
//打印單向管道len和cap
fmt.Println("len = ", len(myChan2),"cap = ", cap(myChan2))
//打印雙向管道len和cap
fmt.Println("len = ", len(myChan),"cap = ", cap(myChan))
}
定時器
package main
import (
"fmt"
"time"
)
func main() {
/*
type Timer struct {
C <-chan Time
r runtimeTimer
}
*/
//1.使用定時器,就要用到time包
// NewTimer作用, 就是讓系統(tǒng)在指定時間之后, 往Timer結(jié)構(gòu)體的C屬性中寫入當前的時間
// 讓程序阻塞3秒, 3秒之后再執(zhí)行
//func NewTimer(d Duration) *Timer
/*start := time.Now()
fmt.Println(start)
//使用定時器
timer := time.NewTimer(time.Second * 3)
fmt.Println(<-timer.C)*/
//2.func After(d Duration) <-chan Time
//這個定時器底層就是NewTimer實現(xiàn)的
/*start := time.Now()
fmt.Println(start)
//使用定時器
timer := time.After(time.Second * 3)
fmt.Println(<-timer)*/
// 以上的定時器都是一次性的定時器, 也就是只會執(zhí)行一次
/*go func() {
start := time.Now()
fmt.Println(start)
timer := time.After(time.Second * 3)
for {
fmt.Println(<-timer)
}
}()
for {
;
}*/
//周期性定時器
start := time.Now()
fmt.Println(start)
//定義周期性定時器
timer := time.NewTicker(time.Second * 2)
for {
fmt.Println(<-timer.C)
timer.Stop()
}
}