Go 语言的并发是通过 goroutine
实现的,goroutine
类似于线程,属于用户态的线程(程序员自己编写的) ,我们可以根据需要创建成千上万的 goroutine
并发工作,goroutine
是由 Go 语言的运行时 runtime
调度实现的,而线程是由操作系统调度完成。
Go 语言还提供 channel
在多个 goroutine
间通信。
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") }
闭包就是在函数内部,引用外部的变量了,因为外边循环的快,里面循环的慢,所以导致了这种结果。
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
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
就结束了main
函数结束了,由 main
函数创建的那些 goroutine
就都结束了下面改掉之前用 sleep 的说法,用高级点的方法
wg WaitGroup
wg 只有三种方法,而且这三种方法,用的时候要一起用
下面是个例子
func f1(i int) { defer wg.Done() //开头就用,而且一定要在 defer 的后面 fmt.Print(i, " ") } var wg sync.WaitGroup func main() { for i := 0; i < 10; i++ { wg.Add(1) //在 goroutine 执行之前加这个 go f1(i) } wg.Wait() //所有的 goroutine 后面执行这个 } // 输出 // 2 5 1 4 7 9 6 8 3 0
OS栈
(操作系统线程)一般都有固定的栈内存(通常是2M),而一个goroutine
在其生命周期开始的时候,只有很小的栈(通常2kb),goroutine
的栈不是固定的,他会按需增大或者缩小,goroutine
的栈的大小,可以限制到 1GB,虽然极少会用到这么大,所以在 go 中,一次创建十万的 goroutine
也是可以的。
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() }
单纯的将函数并发执行是没有意义的,函数与函数之间,只有交换数据, 才能体现并发函数的意义
虽然可以使用共享内存来实现数据的交互,但是共享内存在不同的 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
函数,关于通道,需要注意的是,通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在操作文件之后,关闭文件是必须的,但是关闭通道不是必须的。
close(b)
一个小例子,但是不知道 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 } }
阻塞就是一个goroutine在等着。死锁是所有的groutine 都在等。
在某些场景中,我们需要从多个通道接受数据,通道在接收数据的时候,如果没有数据将会发生阻塞,当然你可以使用 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
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
上面的程序可以用下面这个图来说明一下
现实中,我们经常用到的一种情况就是,我们打印的时候,不想等太久。如果超时了,就不再等了。
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 }
close
函数关闭通道 ,例如 close(c)
panic