1.概念
1.1什么是并发
Go
语言是并发语言,而不是并行语言。
并发性是指**同一时间段处理多间事情的能力。**比如当你在看教学视频的时候,看到重要的知识点,你会选择暂停视频,讲知识点记录在笔记本上,然后再继续看教学视频。在这一个时间段中,你既观看了视频,又记录了笔记,这就是并发。
那什么是并行呢?并行是指在同一时刻执行多个任务。比如你一边听歌一边写作业,那么在同一个时刻,你就执行了两种动作:写作业和听歌。这就是并行。
画个图就更容易理解了
1.2 进程、线程、协程
1.2.1 进程
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度。不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
1.2.2 线程
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,而拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程也由操作系统调度(标准线程是这样的)。只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
1.2.3 协程
协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
一个应用程序一般对应一个进程,一个进程一般有一个主线程,还有若干个辅助线程,线程之间是平行运行的,在线程里面可以开启协程,让程序在特定的时间内运行。
2.Go语言的协程 — Goroutine
2.1 如何创建Goroutine
在函数或者方法调用前加上一个关键字go
,就创建了一个Goroutine
。需要注意的是,关键字go
并非执行并发操作,而是创建一个并发任务单元。对应的函数应该是无返回值函数。
每个任务单元除了保存函数指针、调用参数外,还会分配执行所需的栈内存空间。相比系统默认的MB
级别的线程栈,goroutine
自定义栈初试仅需2KB
,所以才能创建成千上万的并发任务。自定义栈采取按需分配策略,在需要时进行扩容,最大能到GB
规模。
func main() {
go PrintNum()
for i := 0; i < 20; i++ {
fmt.Println("主Goroutine:", "A")
}
fmt.Println("主Goroutine结束")
}
func PrintNum() {
for i := 0; i < 20; i++ {
fmt.Println("子Goroutine:", i)
}
}
执行结果为:
主Goroutine: A
主Goroutine: A
主Goroutine: A
主Goroutine: A
主Goroutine: A
主Goroutine: A
主Goroutine: A
主Goroutine: A
主Goroutine: A
主Goroutine: A
主Goroutine: A
主Goroutine: A
主Goroutine: A
主Goroutine: A
主Goroutine: A
主Goroutine: A
主Goroutine: A
子Goroutine: 0
子Goroutine: 1
子Goroutine: 2
子Goroutine: 3
子Goroutine: 4
子Goroutine: 5
子Goroutine: 6
子Goroutine: 7
子Goroutine: 8
子Goroutine: 9
子Goroutine: 10
子Goroutine: 11
子Goroutine: 12
子Goroutine: 13
子Goroutine: 14
子Goroutine: 15
子Goroutine: 16
子Goroutine: 17
子Goroutine: 18
子Goroutine: 19
主Goroutine: A
主Goroutine: A
主Goroutine: A
主Goroutine结束
2.2 延时Goroutine
从上述执行结果我们可以看到,子Goroutine
和主Goroutine
是交替执行的。但是存在一个问题,如果主Goroutine
结束,那么子Goroutine
也会随之结束(上述代码并未体现)。所以,有时候我们为了保证子Goroutine
能够顺利的被执行完,会让主Goroutine
在执行完成之后sleep
一会儿~
func main() {
go PrintNum()
for i := 0; i < 10; i++ {
fmt.Println("主Goroutine:", "A", i)
}
fmt.Println("主Goroutine结束")
time.Sleep(time.Second)
}
func PrintNum() {
for i := 0; i < 10; i++ {
fmt.Println("子Goroutine:", i)
}
}
执行结果为:
主Goroutine: A 0
主Goroutine: A 1
主Goroutine: A 2
主Goroutine: A 3
主Goroutine: A 4
主Goroutine: A 5
主Goroutine: A 6
主Goroutine: A 7
主Goroutine: A 8
主Goroutine: A 9
主Goroutine结束
子Goroutine: 0
子Goroutine: 1
子Goroutine: 2
子Goroutine: 3
子Goroutine: 4
子Goroutine: 5
子Goroutine: 6
子Goroutine: 7
子Goroutine: 8
子Goroutine: 9
可以看到,子goroutine
利用主goroutine
睡的时间将程序执行完了。
与defer
一样,goroutine
也会因“延时执行”而立即计算并复制执行参数。
var c int
func main() {
a := 100
go func(x, y int) {
time.Sleep(time.Second)
fmt.Println("Go", x, y)
}(a, counter()) // 传入的参数是函数,立即执行函数,并且将a复制
a += 100
fmt.Println("Main", a, counter())
time.Sleep(time.Second * 3) // 等待goroutine结束
}
func counter() int {
c ++
return c
}
执行结果为:
Main 200 2
Go 100 1
但是让主Goroutine
sleep
的这种操作不是很推荐,因为它可能会导致资源的浪费,假如你让主Goroutine
睡了1秒,而你的子Goroutine
只用了0.1秒就完成了任务,那么,CPU在剩下的0.9秒内是完全“瘫痪”的。所以这种做法并不提倡。这里我们可以使用sync
包下的WaitGroup
。通过设定计数器,让每个Goroutine
在退出前递减,直至归0时解出拥塞。
这里给出一个实例,关于sync包下更多方法的操作可以移步我写的另一篇关于sync包的文章
package main
import (
"sync"
"time"
"fmt"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
time.Sleep(time.Second) // 模拟进行一系列操作
fmt.Println("Goroutine ", id, "done.")
}(i)
}
fmt.Println("main....")
wg.Wait() // 在计数归零前一直是阻塞状态
fmt.Println("main exit....")
}
执行结果为:
main....
Goroutine 8 done.
Goroutine 0 done.
Goroutine 9 done.
Goroutine 5 done.
Goroutine 2 done.
Goroutine 7 done.
Goroutine 1 done.
Goroutine 6 done.
Goroutine 4 done.
Goroutine 3 done.
main exit....
3. 通道channel
单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。虽然可以使用共享内存进行数据交换,但是共享内存在不同的Goroutine
中容易发生 竞态 问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁。
但是Go
语言提倡通过通信共享内存,而不是通过共享内存来通信。因此就有了channel
这个玩意。
如果说Goroutine
是Go
程序并发的执行体,channel
就是它们之间的连接。channel
是可以让一个Goroutine
发送特定值到另一个Goroutine
的通信机制。
Go
语言中的通道(channel
)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel
的时候需要为其指定元素类型。
3.1 通道的创建
通道是引用类型,通道类型的空值是nil
。声明格式如下:
var 通道名 chan 元素类型
声明的通道后需要使用make
函数初始化之后才能使用。
make (chan 元素类型 [缓冲区大小])
缓冲区大小可写可不写。
创建通道示例:
ch1 := make(chan int)
ch2 := make(chan bool)
ch3 := make(chan []int)
3.2 通道的使用
通道主要有三种操作:发送、接收和关闭
3.2.1 发送
即将一个数据发送到通道中。
ch := make(chan int)
ch <- 10
3.2.2 接收
从通道中接收一个值。
rev := <- ch
<- ch // 从通过到中接收值,忽略结果
3.2.3 关闭
close(ch)
关于关闭通道需要注意的事情是,只有在通知接收方Goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。
关闭后的通道有以下特点:
- 对一个关闭的通道再发送值就会导致
panic
。 - 对一个关闭的通道进行接收会一直获取值直到通道为空。
- 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
- 关闭一个已经关闭的通道会导致
panic
。
3.3 通道用于事件通知
通道除了传递消息之外,还常被用于事件通知。
func main() {
done := make(chan struct{}) // 结束事件
c := make(chan string) // 数据传输通道
go func() {
s := <-c // 接收消息
fmt.Println(s)
close(done) // 关闭通道,通知结束
}()
c <- "Hello Go"
<-done // 阻塞,直到有数据或管道关闭
}
输出结果:
Hello Go
注意,同步模式必须有配对操作的Goroutine出现,否则会一直阻塞,而异步模式在缓冲区未满或者数据未读完之前,不会阻塞。
异步通道更有助于提升性能,减少排队阻塞。
func main() {
c := make(chan int, 3)
c <- 1
c <- 2
fmt.Println(<-c)
fmt.Println(<-c)
}
输出结果为:
1
2
内置函数cap
和len
返回缓冲区大小和当前已缓冲数量;对于同步通道都返回0。可以通过这个来判断某一个通道是同步通道还是异步通道。
func main() {
c1 := make(chan int, 3)
c2 := make(chan string)
c1 <- 1
c1 <- 2
fmt.Println(cap(c1), len(c1)) // 3 2
fmt.Println(cap(c2), len(c2)) // 0 0
}
3.4 收发
除使用简单的发送和接收操作之外,还可用ok-idom
或者range
模式来处理数据。这两者中,range模式显得更加简洁一点。
func main() {
done := make(chan struct{})
c := make(chan int)
go func() {
defer close(done) // 通知关闭通道
for x := range c {
fmt.Println(x)
}
}()
c <- 1
c <- 2
c <- 3
close(c)
<-done
}
输出结果:
1
2
3
3.5 单向
通道默认是双向的,并不区分发送和接收端。但某些时候,我们可以通过限制收发的方向来获得更加严谨的操作逻辑。
虽然可以通过make
来创建单向通道,但我们通常还是使用类型转换来获取单向通道,并分别赋予操作双方。
package main
import (
"sync"
"fmt"
)
func main() {
var wg sync.WaitGroup
wg.Add(2)
c := make(chan int)
var send chan<- int = c
var rec <-chan int = c
go func() {
defer wg.Done()
for x := range rec {
fmt.Println(x)
}
}()
go func() {
defer wg.Done()
defer close(send)
for i := 0; i < 3; i++ {
send <- i
}
}()
wg.Wait()
}
输出结果为:
0
1
2
3.6 选择
如果要同时处理多个通道,可以选择select
语句。它会随机选择一个可用通道做收发操作。
package main
import (
"sync"
"fmt"
)
func main() {
var wg sync.WaitGroup
wg.Add(2)
a, b := make(chan int), make(chan int)
go func() {
defer wg.Done()
for {
var (
name string
x int
ok bool
)
select { // 随机选择通道接收数据
case x, ok = <-a:
name = "a"
case x, ok = <-b:
name = "b"
}
if !ok { // 任意通道关闭则终止接收
return
}
fmt.Println(name, x)
}
}()
go func() {
defer wg.Done()
defer close(a)
defer close(b)
for i := 0; i < 10; i++ {
select { // 随机选择发送通道
case a <- i:
case b <- i * 10:
}
}
}()
wg.Wait()
}
输出结果为:
b 0
a 1
a 2
a 3
a 4
b 50
b 60
a 7
a 8
a 9