这是我参与「第五届青训营 」伴学笔记创作活动的第 2天
协程、线程与进程
进程、线程和协程之间概念的区别,:
对于 进程、线程,都是有内核进行调度,有 CPU 时间片的概念,进行抢占式调度(有多种调度算法)。而对于协程(用户级线程),这是对内核透明的,也就是系统并不知道有协程的存在,是完全由用户自己的程序进行调度的,因为是由用户程序自己控制,那么就很难像抢占式调度那样做到强制的 CPU 控制权切换到其他进程/线程,通常只能进行 协作式调度,需要协程自己主动把控制权转让出去之后,其他协程才能被执行到。
由于Golang在runtime、系统调用等多方面对goroutine调度进行了封装和处理,当遇到长时间执行或者进行系统调用时,会主动把当前goroutine的CPU转让出去,让其他goroutine能被调度并执行,也就是说Golang从语言层面支持了协程。Golang的一大特色就是从语言层面原生支持协程,在函数或者方法前面加 go关键字就可创建一个协程。
协程的优势:协程在切换开销方面,goroutine远比线程小消耗小。且协程默认占用内存远比 Java 、C 的线程少。线程通常大小是MB级别,而协程通常大小为KB级别。
协程的定义和使用
通过在方法前加go关键字的形式可以立刻开启一个协程,注意我们的main函数本身也是一个协程。如果不加任何限制,main线程不会等待其他子协程就结束执行。例如下面的程序中控制台就不会有输出:
package main
import "fmt"
func add(x, y int) {
z := x + y
fmt.Println(z)
}
func main() {
for i:=0; i<10; i++ {
go add(i, i)
}
}
利用sync.WaitGroup来实现协程的同步
sync.WaitGroup开箱即用,其是并发安全的。原理类似于Java中的CountDownLatch,可以想象该类型中有一个计数器,默认值是0。WaitGroup拥有三个指针方法,下面的方法就是操作或判断计数器:
Add : 增加或减少计数器的值。一般情况下,会用这个方法来记录需要等待的goroutine的数量 Done : 用于对其所属值中计数器的值进行减一操作,就是Add(-1),可以在defer语句中调用它 Wait : 阻塞当前的goroutine,直到所属值中的计数器归零。
如下示例,将wg 计数设置为10, 每个for循环运行完毕都把计数器减一,主函数中使用Wait() 一直阻塞,直到wg为零(也就是所有的10个for循环都运行完毕)时,主程序main函数才会执行结束。
func main() {
waitGroup := sync.WaitGroup{}
waitGroup.Add(10)
for i := 0; i <= 9; i++ {
go func(i int, j int) {
defer waitGroup.Done()
fmt.Println("执行加法:", i)
z := i + j
fmt.Println("结果为:", z)
//计数器减1
}(i, 1)
}
//主协程等待waitGroup计数器归零
waitGroup.Wait()
}
协程之间的通信
协程之间的通信可以通过共享内存实现通信,也可以通过通信来实现共享内存。 go支持上述两种方式,但是更提倡后者,即通过通信来实现共享内存。
通过通道来实现通信
通道类似于一个队列,里面拥有一个可变长度的缓冲区,遵循先入先出的原则。 创建通道的方式: 方式1:
var 通道名 chan 类型
方式2:通过make函数,其中缓冲区大小可以省去,则通道中无缓冲通道,称为同步通道。通道缓冲区代表了可存储的空间大小。
ch:=make(chan 元素类型,[缓冲大小])
channel <- 变量名 表示将对应的变量存入通道缓冲区。
变量名:= <-channel 表示从通道中取出一个变量。
由go实现一个经典的消费生产模型非常简单:
func main() {
channel := make(chan int, 5)
go producer(channel)
go consumer(channel)
time.Sleep(time.Second)
}
func consumer(channel chan int) {
if channel != nil {
for {
res := <-channel
fmt.Println("取出", res)
}
}
}
func producer(channel chan int) {
if channel != nil {
for {
random := rand.Int()
channel <- random
fmt.Println("存入", random)
}
}
}
当协程无缓冲区时,在向某通道发送消息之后,会阻塞当前线程,直到消息被取出消费。当协程有缓冲区时,协程会等到通道缓冲区填满时才阻塞。
在向管道中发送数据时,发送的数据和原数据是独立的,因此修改原数据不会影响发送的数据。
通过互斥锁实现共享内存
Golang也支持互斥锁。
命名: lock := sync.Mutex
加锁: lock.lock()
解锁: lock.unlock()
sync.Mutex性能开销较大,因此应该用于保护一段逻辑,而不是保护一个变量。