基本概念
串行、并发与并行
串行:按照先后顺序来执行任务
并发:同一时间段内执行多个任务
并行:同一时刻执行多个任务
进程、线程和协程
- 进程(Process):程序在操作系统中的一次执行过程,是系统进行资源分配和调度的一个独立单位
- 线程(Thread):操作系统基于进程开启的轻量级进程,是操作系统调度执行的最小单位
- 协程(coroutine):非操作系统提供,而是由用户自行创建和控制的用户态“进程”,比线程更轻量级
goroutine
goroutine
是go语言并发的核心
区别于操作系统线程由系统的内核进行调度,goroutine
由goruntime
负责调度
比如会将m
个goroutine
合理地分配给n
个操作系统线程,类似于m:n
的调度机制,从而不再需要开发者手动维护一个线程池
goroutine
是go程序中最基本的并发执行单元,每一个Go程序都至少有一个goroutine
,也就是main goroutine
当你需要让某个任务并发执行的时候,只需要将这一部分任务包装成为一个函数,开启一个goroutine
来执行这个函数
go关键字
创建一个新的goroutine
,只需要使用go
关键字即可
go f()
// 匿名函数也可以
go func(){
}()
启动goroutine
串行执行的代码
package main
import (
"fmt"
)
func hello() {
fmt.Println("hello")
}
func main() {
hello()
fmt.Println("你好")
}
由于是串行执行,所以结果如下所示:
hello
你好
那么,在hello()
之前使用go
开启一个goroutine
去执行,结果如何
func main() {
go hello() // 启动另外一个goroutine去执行hello函数
fmt.Println("你好")
}
结果发现,输出的结果如下
你好
其实在 Go 程序启动时,Go 程序就会为 main 函数创建一个默认的 goroutine 。在上面的代码中我们在 main 函数中使用 go 关键字创建了另外一个 goroutine 去执行 hello 函数,而此时 main goroutine 还在继续往下执行,我们的程序中此时存在两个并发执行的 goroutine。当 main 函数结束时整个程序也就结束了,同时 main goroutine 也结束了,所有由 main goroutine 创建的 goroutine 也会一同退出。也就是说我们的 main 函数退出太快,另外一个 goroutine 中的函数还未执行完程序就退出了,导致未打印出“hello”。
我们可以使用time.Sleep(time.Second)
来让main goroutine
等待1s后再退出
但这种方式并不算优雅,有可能会停止等待较长的时间
可以换用sync
包下面的相关api来帮我们完成相关的任务
package main
import (
"fmt"
"sync"
)
// 声明全局等待组变量
var wg sync.WaitGroup
func hello() {
fmt.Println("hello")
wg.Done() // 告知当前goroutine完成
}
func main() {
wg.Add(1) // 登记1个goroutine
go hello()
fmt.Println("你好")
wg.Wait() // 阻塞等待登记的goroutine完成
}
ws.Add(x)
会在组中登记x个goroutine
ws.Wait()
会等待直到登记的goroutine
数减少到0
启动多个goroutine
假设我们现在需要以随机的顺序输出0~10十个数,那么可以使用如下代码
package main
import "sync"
var wg sync.WaitGroup
func Hello(x int) {
defer wg.Done()
println("Hello:", x)
}
func main() {
wg.Add(10)
for i := 1; i <= 10; i++ {
go Hello(i)
}
wg.Wait()
}
每次打印的顺序都是不一样的,因为10个
goroutine
是并发执行的,并且goroutine
的调度是随机的
channel
与Java中线程之间的通信相似,在go中,单纯的将函数并发执行是没有任何意义的
函数与函数之间需要交换数据才能体现并发执行的意义
虽然可以使用共享某一块内存的方法来实现内存的交换,但是共享内存在不同的
goroutine
中会发生竞态问题(读写不一致)。 为了保证数据交换的正确性,很多并发模型采用锁
的方式来对内存进行互斥加锁,但这也会带来一定的性能问题
goroutine
是go程序并发的执行体,channel
就是它们之间的连接,通过channel,是可以让一个goroutine
发送特定的值到另外一个goroutine
的通信机制
Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
channel类型
声明通道类型的变量格式如下:
var 变量名称 chan 元素类型
var ch1 chan int // 声明一个传递整型的通道
var ch2 chan bool // 声明一个传递布尔型的通道
var ch3 chan []int // 声明一个传递int切片的通道
channel零值
未初始化的通道类型变量,默认零值是nil
var ch chan int
fmt.Println(ch) // <nil>
channel初始化
声明的通道类型变量需要使用内置的make
函数初始化之后才能使用
make(chan 元素类型, [缓冲大小])
其中,
- channel的缓冲大小是可选的
ch1 := make(chan int)
ch2 := make(chan int, 2)
channel操作
通道共有发送send
、receive
、close
三种操作,而发送和接收操作都使用<-
符号
定义一个通道:
发送ch := make(chan int)
讲一个值发送到通道中
接收ch <- 10
从一个通道中接受值
关闭x := <- ch // 从ch中接收值并赋值给变量x
<-ch // 从ch中接收值,忽略结果
使用内置的close
关键字来关闭通道
注意:一个通道值是可以被垃圾回收掉的。通道通常由发送方执行关闭操作,并且只有在接收方明确等待通道关闭的信号时才需要执行关闭操作。它和关闭文件不一样,通常在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。close(ch)
关闭后的通道有如下特点:
- 对一个关闭的通道再发送值就会导致panic
- 对一个关闭的通道进行接收会一直获取值直到通道为空
- 对一个关闭的并且没有值的通道再执行接收操作会得到对应类型的零值
- 关闭一个已经关闭的通道会导致panic
无缓冲的通道
无缓冲的通道又被称为阻塞的通道,如下所示:
上述代码能够通过编译,但是在运行过程中会出现程序死锁错误: ```go fatal error: all goroutines are asleep - deadlock!func main() {
ch := make(chan int)
ch <- 10
fmt.Println("发送成功")
}
goroutine 1 [chan send]: main.main() …/main.go:8 +0x54
> 因为我们使用`ch := make(chan int)`创建的是无缓冲的通道,无缓冲的通道只有在有接收方能够接收值的时候才能发送成功,否则会一直处于等待发送的阶段。同理,如果对一个无缓冲通道执行接收操作时,没有任何向通道中发送值的操作那么也会导致接收操作阻塞。就像田径比赛中的4x100接力赛,想要完成交棒必须有一个能够接棒的运动员,否则只能等待。简单来说就是无缓冲的通道必须有至少一个接收方才能发送成功。
上面的代码会阻塞在`ch <- 10`,因为没有变量可以接收通道中的值<br />因此可以新建一个`goroutine`去接收通道中的值
```go
func recv(c chan int) {
ret := <-c
fmt.Println("接收成功", ret)
}
func main() {
ch := make(chan int)
go recv(ch) // 创建一个 goroutine 从通道接收值
ch <- 10
fmt.Println("发送成功")
}
首先无缓冲通道
ch
上的发送操作会阻塞,直到另一个 goroutine 在该通道上执行接收操作,这时数字10才能发送成功,两个 goroutine 将继续执行。相反,如果接收操作先执行,接收方所在的 goroutine 将阻塞,直到 main goroutine 中向该通道发送数字10。 使用无缓冲通道进行通信将导致发送和接收的 goroutine 同步化。因此,无缓冲通道也被称为同步通道
。
有缓冲的通道
在使用make
函数的时候,可以在第二个参数中设置缓冲区的大小
func main() {
ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
ch <- 10
fmt.Println("发送成功")
}
多返回值模式
对于一个已经关闭通道我们再执行操作,有时候会导致这样或那样的异常
那么如何判断一个通道是否关闭?
我们可以在接收值的时候,使用两个参数来接受返回值,如下所示:
func f2(ch chan int) {
for {
v, ok := <-ch
if !ok {
fmt.Println("通道已关闭")
break
}
fmt.Printf("v:%#v ok:%#v\n", v, ok)
}
}
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch)
f2(ch)
}
其中:
value
:从通道中取出的值,如果通道被关闭则返回对应类型的零值。ok
:通道ch关闭时返回 false,否则返回 true。
for range接收值
通常我们会选择使用for range
循环从通道中接收值,当通道被关闭后,会在通道内的所有值被接收完毕后会自动退出循环。上面那个示例我们使用for range
改写后会很简洁。
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func ReadData(ch chan int) {
defer wg.Done()
for data := range ch {
fmt.Println("data is:", data)
}
}
func WriteData(ch chan int) {
defer wg.Done()
for i := 1; i <= 10; i++ {
ch <- i
}
close(ch)
}
func main() {
ch := make(chan int)
wg.Add(2)
go ReadData(ch)
go WriteData(ch)
wg.Wait()
}
单向通道
总结
select多路复用
并发安全和锁
有时候我们的代码中可能存在多个goroutine
同时操作一个资源的情况,从而导致出现竞态问题
这与Java中多线程操作同一份数据是相同的原理
比如我们起10个线程,每个线程都对某个变量执行1000次+1操作,最后变量的值很大概率不会为1000
原因就是因为可能修改操作存在覆盖的情况
互斥锁
互斥锁是控制共享资源访问的一种情况,能够保证同一时间内只有一个goroutine
可以访问共享资源
方法名 | 功能 |
---|---|
func (m *Mutex) Lock() | 获取互斥锁 |
func (m *Mutex) Unlock() | 释放互斥锁 |
使用互斥锁可以使每次只有一个goroutine
能够操作共享变量,从而保证数据的正确性
package main
import (
"fmt"
"sync"
)
var (
sum = 0
wg sync.WaitGroup
m sync.Mutex
)
func HandleAdd() {
defer wg.Done()
for i := 1; i <= 1000; i++ {
m.Lock()
sum += 1
m.Unlock()
}
}
func main() {
wg.Add(10)
for i := 0; i < 10; i++ {
go HandleAdd()
}
wg.Wait()
fmt.Println("sum:", sum)
}
在进行加锁之后,无论运行多少次,最后的结果一定都是10 * 1000 = 10000
读写互斥锁
互斥锁是完全互斥的,但是实际上
使用互斥