golang学习笔记----并发
并发模型
并发目前来看比较主流的就三种:
- 多线程:每个线程一次处理一个请求,线程越多可并发处理的请求数就越多,但是在高并发下,多线程开销会比较大。
- 协程:无需抢占式的调度,开销小,可以有效的提高线程的并发性,从而避免了线程的缺点的部分
- 基于异步回调的IO模型: 比如nginx使用的就是epoll模型,通过事件驱动的方式与异步IO回调,使得服务器持续运转,来支撑高并发的请求
golang的goroutine就是为了追求更高效和低开销的并发
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