初心是记录和总结,自己学习Go语言的历程。如果能帮助到你,这是我的荣幸。
并发是属于比较难的内容,我还不足以把握到其精髓,只能分享一些基本使用和注意事项。
如何开启并发
很简单,只要在方法前使用go关键字,看示例代码。这里使用了匿名函数,匿名函数可以在方法中直接调用执行,通过{}
后加上()
,也可以传入参数,只需要func(参数)
和最后的括号里加入传入(传入参数)
对应即可。
package main
import "fmt"
func main() {
go func() {
fmt.Println("并发程序")
}()
fmt.Println("主程序")
}
程序运行后,会输出主程序
,发现并没有打印并发程序
。这是因为在Go中,主程序也算是一个并发的主协程,而主协程执行完毕后,会将其他的子协程(通过go关键字开启的)强制结束。这里出现了协程两个字,这是在go中特有的描述并发的,协程就是通过go
关键字开启的,英文名叫:goroutine
主协程和子协程如何协调
这个协调是指:主协程能不能等我自己开启的协程结束后才结束。并发本身就是脱离了程序定义的顺序,它并不按照程序语句的顺序执行,而是通过计算机资源调度,和CPU有关系。那么我们如何进行控制呢?
睡眠
睡眠的方式非常简单粗暴,强制该程序‘睡眠’,暂时停止执行。这样CPU就会执行其他协程了。
使用方法:
time.Sleep(d Duration)
Duration是指时间间隔,常用的有
time.Hour
一小时time.Minute
一分time.Second
一秒
使用WaitGroup
WaitGroup等待goroutine的集合完成。主程序调用Add来设置等待的gor例行程序的数量。然后每个goroutine运行并在完成时调用Done。同时,Wait可用于阻塞直到所有goroutine完成。
通过这段话我们可以捕获三个点
- 在主程序中调用WaitGroup的
Add
方法添加字子协程的数量 - 每个子协程运行完成时调用
Done
方法 - 主程序通过使用
Wait
方法等待子协程运行结束
// 定义WaitGroup对象
var wg sync.WaitGroup
func main() {
wg.Add(1) // 添加一个子协程
go func() {
fmt.Println("并发程序")
wg.Done() // 子协程结束后调用Done
}()
fmt.Println("主程序")
wg.Wait() // 等待子协程运行结束,主协程才结束
}
数据传递 - 通道
并发执行时第一个重要的点:数据传递
模拟A协程和B协程,双方进行数据传输,在go语言中,协程数据的传输推荐的是使用通道,这里出现了一个新的数据类型:chan
,使用它只需要关心的是:传输什么类型的数据,所以它的定义方法是:var chan1 int = make(chan int)
或者是简易法:chan1 := make(chan int)
通道的传递和接收语法
// 定义了一个传输数值型的通道
chan1 := make(chan int)
// 从通道中接收内容
x := <-chan1
// 往通道中传入一个值:1
chan1 <- 1
阻塞式通道
chan1 := make(chan int)
这种没有指定缓冲区的通道,默认是阻塞式通道。
理解阻塞:通道的使用有发送方和接收方两个角色。
- 发送方发送数据时,当接收方没有准备好接收数据时,通道对于发送方是阻塞的。
- 接收方,当通道中数据为空的时候,通道对于接收方是阻塞的。
看见一个非常形象的例子:京东快递送货上门,当没有暂存站点时,它会打电话给你,如果你方便接收,那它会给你送过来,如果你不能接收,则不会给你送过来。
常见错误,阻塞式通道兼任两个角色:
阻塞式非常注重发送方和接收方这两个角色,同个协程 不允许兼任两个身份。
func main() {
chan1 := make(chan int)
// 接收
x := <-chan1
//发送
chan1 <- 1
fmt.Println(x)
}
改正,接收方改为新的协程;Ps:这里涉及到资源抢夺的问题,因为并没有使用WaitGroup
func main() {
chan1 := make(chan int)
go func() {
// 接收
x := <-chan1
fmt.Println(x)
}()
//发送
chan1 <- 1
}
常见错误,接收未准备好就发送数据:
先有接收状态才能往通道发送内容!这是阻塞式通道的原则。
func main() {
chan1 := make(chan int)
//发送
chan1 <- 1
go func() {
// 接收
x := <-chan1
fmt.Println(x)
}()
}
改正:把go
协程放在发送注释上方即可。
常见错误,往里存了2个数
// 接收一个
chan1 := make(chan int)
chan1 <- 1
chan1 <- 2
新的输入无法在通道非空的情况下传入,即传送数据时,通道必须是空的。
非阻塞式通道
chan1 := make(chan int,1)
相比阻塞式通道,这里就多指明了一个参数,这个参数是一个数值型,表示的是缓存区,数据在通道中传输时可以保存1个,拿上面常见错误的最后一个例子来说:
// 接收一个
chan1 := make(chan int,1)
chan1 <- 1
chan1 <- 2
这里我们缓冲了一个,被接收了一个,所以再放值的时候不会报错。
非阻塞通道还有这些特点:
- 只要缓存区运行,没有接收方也不会报错。
- 因为缓存区的存在,打破了接收方和发送方的限制,在一个协程中发送和接收是被允许的,甚至不讲顺序。
这两点可以自己去尝试一下。
遍历通道的内容
方法一
使用无穷循环,配上判断;无穷循环不断的去执行获取chan
中的内容,它会返回两个值,第一个值是实际值,第二个值是通道是否关闭,若通道没关闭则进行相应的判断
for {
v, ok := <-chan1
fmt.Println(v)
fmt.Println(ok)
}
方法二
不用关心通道是否关闭,通道关闭时自动退出循环。
for v := range chan1 { // 优雅的从通道循环取值
fmt.Println(v)
}
【重点】 方法一和方法二的取舍:
- 方法一 不需要告知通道关闭,因为通道在垃圾回收的机制内。
- 方法二 需要告诉通道关闭来控制退出循环
综合例子一:传输数字,直到...
往通道里传输数字,直到传到10位置
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {
var chan1 chan int = make(chan int, 10)
i := 0
wg.Add(1)
go func() {
for v := range chan1 { //如果通道关闭会自动退出循环,如果没有告知通道关闭,会报错
fmt.Println(v)
}
defer wg.Done()
}()
for {
if i == 10 {
// 告知通道关闭了
close(chan1)
break
} else {
chan1 <- i
i++
}
}
wg.Wait()
}
综合例子二:筛选偶数
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {
var in chan []int = make(chan []int, 10)
var out chan []int = make(chan []int, 10)
x := make([]int, 0)
x = append(x, 1, 2, 3, 4, 5, 6, 7)
out <- x
close(out) // 关闭通道
wg.Add(1)
go odd(in, out)
for v := range in { //同样需要告诉通道关闭了
fmt.Println(v)
}
//time.Sleep(time.Second * 2000)
wg.Wait()
}
func odd(in chan<- []int, out <-chan []int) {
tmp := make([]int, 0)
for res := range out { // 用range需要告诉通道关闭了
//fmt.Println(res)
for _, value := range res {
if value%2 == 0 {
//fmt.Println(value)
tmp = append(tmp, value)
}
}
}
in <- tmp
close(in)
wg.Done()
}
// 助记
//1. chan<- int是一个只能发送的通道,可以发送但是不能接收;
//2. <-chan int是一个只能接收的通道,可以接收但是不能发送。