文章目录
一.概念
- 并发是同一时间段执行多个任务,(你同时和两个女生聊天)
- 并行是同一时刻执行多个任务,(你和你朋友在和女生聊天)
Go 语言的并发是通过 goroutine
实现的,goroutine
类似于线程,属于用户态的线程(程序员自己编写的) ,我们可以根据需要创建成千上万的 goroutine
并发工作,goroutine
是由 Go 语言的运行时 runtime
调度实现的,而线程是由操作系统调度完成。
Go 语言还提供 channel
在多个 goroutine
间通信。
二. goroutien
-
Go 语言使用
goroutine
非常简单,只需要在调用函数的时候,前边加个go
就可以了 -
一个
goroutine
必定对应一个函数,可以创建多个goroutine
去执行相同的函数
func f1(i int) {
fmt.Println(i)
}
func main() {
for i := 0; i < 100000; i++ {
go f1(i)
}
fmt.Println("main")
}
2.1 go语言的闭包问题
闭包就是在函数内部,引用外部的变量了,因为外边循环的快,里面循环的慢,所以导致了这种结果。
func main() {
for i := 0; i < 10; i++ {
go func() {
fmt.Print(i, "\t") // 这个里面的东西,是作用域外边获得的,闭包
}()
}
fmt.Println("main")
time.Sleep(time.Second)
}
// 输出
3 7 5 7 3 7 3 main
想解决闭包问题用很简单,不要让函数读外边的值,而是让函数直接传值
func main() {
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Print(i, "\t") // 这个里面的东西,是作用域外边获得的,闭包
}(i)
}
fmt.Println("main")
time.Sleep(time.Second)
}
// 输出
main
4 1 0 2 6 3 5 7 8 9
2.2 go使用随机数
- 如果不添加种子,每次编译好的代码都是相同的,所以运行出来的随机数也是相同的
- 因为使用时间戳的毫秒数肯定不一样,随意可以拿他当种子
import (
"fmt"
"math/rand"
)
func main() {
for i := 0; i < 5; i++ {
r1 := rand.Int()
r2 := rand.Intn(9) // 可以指定最大值
fmt.Println(r1, r2)
}
}
// 输出
5577006791947779410 6
6129484611666145821 2
3916589616287113937 6
605394647632969758 8
894385949183117216 6
加上 种子
以后,打印:
func main() {
rand.Seed(time.Now().UnixNano()) // 用那秒速
for i := 0; i < 5; i++ {
r1 := rand.Int()
r2 := rand.Intn(9) // 可以指定最大值
fmt.Println(r1, r2)
}
}
三. goroutine 什么时候结束
goroutine
对应的函数结束了,goroutine
就结束了main
函数结束了,由main
函数创建的那些goroutine
就都结束了
下面改掉之前用 sleep 的说法,用高级点的方法
wg WaitGroup
wg 只有方法
- Add
- Done
- Wait
func f1(i int) {
defer wg.Done()
fmt.Println(i)
}
var wg sync.WaitGroup
func main() {
for i := 0; i < 5; i++ {
wg.Add(1) // wg.add 在 gofunc() 前边执行
go f1(i) // wg.down 在最后执行
}
wg.Wait() // wg.的等待函数,在最后执行。
}
// 输出
1
4
3
2
0
四.goroutine调度
4.1 可增长的栈
OS栈
(操作系统线程)一般都有固定的栈内存(通常是2M),而一个goroutine
在其生命周期开始的时候,只有很小的栈(通常2kb),goroutine
的栈不是固定的,他会按需增大或者缩小,goroutine
的栈的大小,可以限制到 1GB,虽然极少会用到这么大,所以在 go 中,一次创建十万的 goroutine
也是可以的。
4.2 goroutien 调度
GMP 是 GO 语言运行时(runtime) 层面的实现,是go 语言自己实现的一套调度系统,区别于系统调度 OS 线程。
- 使用
runtime.GOMAXPROCS(1)
命令断定核数 - 如果不配置,那么默认是跑满
defer wg.Done()
应该加derfer,保证在最后调用
package main
import (
"fmt"
"runtime"
"sync"
)
func f1() {
defer wg.Done() //应当是 derfer 后调用
for i := 0; i < 10; i++ {
fmt.Println("A", i)
}
}
func f2() {
defer wg.Done() // 应当使用derfer 后调用
for i := 0; i < 10; i++ {
fmt.Println("B", i)
}
}
var wg sync.WaitGroup
func main() {
fmt.Println("打印CPU 的核心数")
fmt.Println(runtime.NumCPU())
runtime.GOMAXPROCS(1) // 单核使用,不填的话,始终占满真个线程
wg.Add(2)
go f1()
go f2()
wg.Wait()
}
五.channel
-
单纯的将函数并发执行是没有意义的,函数与函数之间,只有交换数据, 才能体现并发函数的意义
-
虽然可以使用共享内存来实现数据的交互,但是共享内存在不同的
gorontine
中,容易发生竞态问题,为了保证数据交互的正常性。不使用互斥量对内存进行加锁,这种做法势必造成性能问题 -
GO语言的并发模式是
CSP
,提倡通过通信实现共享内存,而不是通过共享内存而实现通信 -
如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine 的通信机制。
-
Go语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
var b chan int
var wg sync.WaitGroup
func f1() {
defer wg.Done()
c := <-b
fmt.Println("f1函数", c)
}
func main() {
fmt.Println(b)
b = make(chan int) // 不指定缓冲通道数
wg.Add(1)
go f1() // 应该先取值,然后再放值
b <- 10
fmt.Println("函数已经完毕")
wg.Wait()
}
// 输出
<nil>
f1函数 10
函数已经完毕
使用指定缓存区大小
这样会出现死锁的情况,因为指定了缓存区只能存一个,现在你硬往里面存两个。
var b chan int
func main() {
b = make(chan int, 1)
fmt.Println(b)
b <- 10
b <- 20
y := <-b
fmt.Println(y)
}
// 输出
fatal error: all goroutines are asleep - deadlock!
关闭通道往往通过内置的 close
函数,关于通道,需要注意的是,通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在操作文件之后,关闭文件是必须的,但是关闭通道不是必须的。
一个小例子,但是不知道 bug 在什么地方,以后再改吧。
/*
启动一个 goroutine ,生成100 个数,发送到 ch1
启动一个 goroutien,从ch1 中取值,计算其平方,然后放到 ch2 中
在 main 中,从 ch2 中取值
*/
var wg sync.WaitGroup
var a chan int
var b chan int
func f1(ch1 chan int) {
defer wg.Done()
for i := 0; i < 100; i++ {
ch1 <- i
}
}
func f2(ch1, ch2 chan int) {
defer wg.Done()
for x := range ch1 {
ch2 <- x * x
}
}
func main() {
a = make(chan int)
b = make(chan int)
wg.Add(2)
go f1(a)
go f2(a, b)
wg.Wait()
for ret := range b {
fmt.Println(ret)
}
}
单通道限制
单通道一般用在函数的参数里面,限定参数只能读或者只能写。
func f2(ch1 <-chan int, ch2 chan<- int) {
defer wg.Done()
for x := range ch1 {
ch2 <- x * x
}
}
通道的总结:
阻塞就是在等着。死锁是所有的groutne 都在等。
六.select
在某些场景中,我们需要从多个通道接受数据,通道在接收数据的时候,如果没有数据将会发生阻塞,当然你可以使用 if
语句进行判断,但是这样性能就会差很多,此时,你可以使用 Go
内置的 select
关键字,同时响应多个通道的操作,select
语句的使用,类似于 switch
语句,它会有一些列 case
分支和一个默认分支,每个 case
都对应一个通道的通信(接受 或者发送过程)。select
会一直在那里等,直到某个 case
的通信操作完成时,就会执行case
对应的语句
简单点来说,就是 select 执行的语句是随机的,不一定执行哪一句,但是如果某一句不满足的话,他肯定不会执行。
func main() {
ch := make(chan int, 1)
for i := 0; i < 10; i++ {
select {
case x := <-ch:
fmt.Println(x) // 从 x 中取值
case ch <- i: // 后面必须加 :
fmt.Println("放值")
}
}
}
// 输出
放值
0
放值
2
放值
4
放值
6
放值
8
出现上面这种情况,主要是因为通道里面只能放一个值,所以他只能执行第二个 case
如果暂存区的大于1的话,那么输出的值就不确定了,结果如下:
func main() {
ch := make(chan int, 10)
for i := 0; i < 10; i++ {
select {
case x := <-ch:
fmt.Println(x) // 从 x 中取值
case ch <- i: // 后面必须加 :
fmt.Println("放值")
}
}
}
// 输出
放值
0
放值
放值
放值
放值
2
3
放值
4
七.通道详解
1. 小例子,一个函数是从通道里读值,一个是从通道里写值
func main() {
c := make(chan int)
for i := 0; i < 5; i++ {
go f1(c, i)
}
for i := 0; i < 5; i++ {
s := <-c
fmt.Println("从通道里取值", s)
}
}
// 输出
往通道里传值 4
从通道里取值 4
往通道里传值 2
从通道里取值 2
往通道里传值 1
从通道里取值 1
往通道里传值 3
从通道里取值 3
往通道里传值 0
从通道里取值 0
上面的程序可以用下面这个图来说明一下
2.select 和time.After的例子
现实中,我们经常用到的一种情况就是,我们打印的时候,不想等太久。如果超时了,就不再等了。
func f1(id int, ch1 chan int) {
time.Sleep(time.Duration(rand.Intn(4)) * time.Second)
ch1 <- id // 往通道里放值
}
func main() {
c := make(chan int)
timeout := time.After(2 * time.Second) // 两秒
for i := 0; i < 5; i++ {
go f1(i,c)
}
for i := 0; i < 5; i++ {
select {
case b := <-c:
fmt.Println(b)
case <-timeout:
fmt.Println("超时了不打印")
}
}
}
// 输出结果
0
3
超时了不打印
4
2
nil 通道的用处
- 对于包含
select
语句的循环,如果不希望每次循环都等待select
所涉及的所有通道数,那么可以将某些通道数设为nil
等到发送值准备就绪之后,再将通道变成一个非nil 的值并执行发送操作。
阻塞和死锁
- 当
goroutine
在等待通道的发送或接受时,我们就说他被阻塞了 - 除了
goroutine
本身占用少量的内存外,被阻塞的goroutien
并不会消耗任何其他的资源,goroutien
静静的停在那里,等待导致其阻塞的事情来解除阻塞 - 当一个或多个
goroutien
因为某些无法发生的事情被阻塞死,我们称这种情况为死锁,而出现死锁的程序通常会崩溃或者挂起
下面就是一个简单的例子构成死锁:
因为他要从c中取值,但是c用于不可能有值,所以就被死锁了。
func main() {
c := make(chan int)
<-c
}
利用 goroutine 来实现装配线
- Go 允许在没有值可发送的情况下,通过
close
函数关闭通道 ,例如close(c)
- 通道被关闭以后,将无法写入任何值,但是可以读,如果尝试写入,就会引发
panic
- 尝试读取被关闭的通道会获得与通道类型对应的零值
- 注意:如果循环里读取一个已关闭的通道,并没有检测通道是否关闭,那么该循环就会一直运转下去,消耗大量的 cpu 时间