go语言并发编程与原理
一、前置知识
1、CPU、操作系统与多线程
1.1单进程时代的特点和问题
单进程时代的两个问题:
① 单一执行流程、计算机只能一个任务一个任务地处理。
② 进程阻塞所带来的CPU浪费时间
1.2多线程、多进程的特点和问题
多进程、多线程解决了阻塞问题。
多线程带来新的问题:
①线程的切换是需要时间和计算成本的,因为需要保存未完成任务的线程的状态。如图。
进程、线程的数量越多,切换成本就越大,也就越浪费。
CPU可能60%执行程序,40%在切换中。
②多线程带来的开发设计难度增大,越来越复杂:同步竞争(如锁、共享内存、竞争资源冲突等)。
③高内存占用。如图。
解决上述问题,就是下一代语言应考虑的问题。
2、协程
一个线程,操作系统分为用户空间(用户态)和内核空间(内核态)。内核态表示操作系统底层,包括进程的开辟啊,分配物理内存资源啊,分配磁盘资源等等。用户空间就是上层开发写业务逻辑的调接口的。我们能不能把这样的线程一分为二呢?给他划分为一个内核线程和一个用户线程呢?
如果这样切换成功的话,我们把用户线程和内核线程是做了一个绑定。这个内核线程单独用于处理硬件问题,用户线程用来保证业务层面并发的效果。CPU是硬件,不管你是线程还是进程,它的视野就只能看到内核,CPU本身是无感的,操作系统不需要变。操作系统本身不知道的。所以说这么划分完之后,操作系统不需要改代码。
内核线程就是thread。
用户线程就起个别名叫协程co-routine。
他们是一一绑定的。
调度器管理线程、协程有三种关系模式:
N:1关系解决了线程切换的高CPU消耗的切换成本问题,但阻塞问题无法解决。
1:1关系完全退化为了原来的多线程模型,还是会存在高切换成本问题。
不同语言做不同的协程调度器,优化得越好效率越高。
二、Golang中的协程
1、Golang对协程的处理
灵活调度体现在调度器
2、Golang早期调度器
Golang早期的调度器是比较差的。
当M0想获取一个协程,首先从全局队列中,尝试获取锁,取一个G过来执行,执行完尝试还锁,放回到队列。
这种调度器非常简单,有很多弊端:
3、Golang优化后的调度器
3.1、GMP模型
3.2、调度器设计策略
1、复用线程
work stealing机制
hand off机制
2、利用并行
3、抢占
4、全局G队列
三、goroutine实践
1、goroutine的开启与关闭
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
//用关键字go创建承载一个形参为空,返回值为空的一个函数
go func() {
defer fmt.Println("A.defer就算Goexit()退出线程也会执行。")
func() {
defer fmt.Println("B.defer就算Goexit()退出线程也会执行。")
//退出当前goroutine,注掉放开看效果
runtime.Goexit()
fmt.Println("B若Goexit()退出当前线程,则后面的此行代码不会执行。")
}()
fmt.Println("A若Goexit()退出当前线程,则后面的此行代码不会执行。")
}()
go func(a int, b int) bool {
fmt.Println("a = ", a, ", b = ", b)
return true//这里的return是不会被接收的,引出goroutine之间传值需要用到的channel
}(10, 20)
time.Sleep(3 * time.Second)//主goroutine等待让子goroutine执行完毕。
}
2、无缓冲的channel
package main
import "fmt"
func main() {
//定义一个无缓冲channel
c := make(chan int)
go func() {
defer fmt.Println("goroutine结束")
fmt.Println("goroutine 正在运行...")
c <- 666 //将666 发送给c
}()
num := <-c //从c中接受数据,并赋值给num
fmt.Println("num = ", num)
fmt.Println("main goroutine 结束...")
}
3、有缓冲的channel
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int, 3) //带有缓冲的channel
fmt.Println("len(c) = ", len(c), ", cap(c)", cap(c))
go func() {
defer fmt.Println("子go程结束")
for i := 0; i < 5; i++ {//发送的比接收的多一个元素,最终在channel中未被使用,但不会阻塞两边的线程。
c <- i
fmt.Println("子go程正在运行, 发送的元素=", i, " len(c)=", len(c), ", cap(c)=", cap(c))
}
}()
fmt.Println("主线程开始延时等待,发送者阻塞")
time.Sleep(2 * time.Second)
fmt.Println("主线程结束延时等待,开始接收")
for i := 0; i < 4; i++ {
num := <-c //从c中接收数据,并赋值给num
fmt.Println("num = ", num)
}
time.Sleep( time.Second/2)
fmt.Println("main 结束")
}
4、channel的关闭
package main
import "fmt"
func main() {
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
}
//close可以关闭一个channel
close(c)
}()
for {//如果不关闭channel,这里会形成死锁。
//ok如果为true表示channel没有关闭,如果为false表示channel已经关闭
if data, ok := <-c; ok {
fmt.Println(data)
} else {
break
}
}
fmt.Println("Main Finished..")
}
5、channel与for-range
上述代码可以用for-range进行简写:
package main
import "fmt"
func main() {
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
}
//close可以关闭一个channel
close(c)
}()
//可以使用range来迭代不断操作channel
for data := range c {
fmt.Println(data)
}
fmt.Println("Main Finished..")
}
6、channel与select-case
package main
import "fmt"
func fibonacii(c, quit chan int) {
x, y := 1, 1
for {//如果没有selectcase,同一时刻只能监听一个channel
select {
case c <- x://如果c可写,则该case就会进来。
x = y
y = x + y
case <-quit://如果quit可读,则该case就会进来。
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
//sub go
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
//main go
fibonacii(c, quit)
}