go并发
终于走到并发了,一搞这就是第九天了,并发作为go最迷人的地方,一定要把基础打的牢牢的呀
再说回来哈,go的并发是由goroutine
实现的,他是属于 用户态, 开发者大哥写的(站在巨人的肩膀上就是爽)
要记下的是goroutine是运行时(runtime)进行调度完成的,而线程是由操作系统完成的哦。
go里面还设置了管道 channel 用于 多个goroutine之间的通信。
goroutine的使用
- go开启多线程真的是巨简单,直接使用 go + 运行的函数后者方法即可
- 举个例子
- 从运行结果可以看出,并发是很成功的。啊哈哈哈哈哈哈
- 使用方法,直接在前面加 go 就行了,超级方便
- 然后我们看看匿名方法的并发。
func main() {
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Println(i)
}(i)
}
time.Sleep(2 * 1000)
}
- 舒服吧,里面的方法,相当于java里面的run方法体了
- 我们可以看到哈,我加了个time.sleep去等待这个多线程搞完,那是因为for循环比较快,搞完main函数就结束了,里面的多线程就不会去打印了。所以需要去等,那有木有更优雅一点的方式呢?
- 是有的,我们可以使用sync.WaitGroup来实现goroutine的同步问题
package main
import (
"fmt"
"sync"
)
var mg sync.WaitGroup
func main() {
for i := 0; i < 10; i++ {
mg.Add(1)
go func(i int) {
mg.Done()
fmt.Println(i)
}(i)
}
mg.Wait()
}
- 如此,我们也能实现多线程的执行。go里面的多线程就是这么简单
- 那么我们来个问题!
goroutine什么时候结束?
goroutine对应的函数结束了,goroutine就结束了
main函数执行完了,由main函数创建的那些goroutine就都结束了
并发可以配合随机数来玩,啊哈哈和,一起来看看吧
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var mg sync.WaitGroup
func f() {
defer mg.Done() // 记得最后会自动执行哦
rand.Seed(time.Now().UnixNano())
for i := 0; i < 5; i++ {
r1 := rand.Int() // int64
r2 := rand.Intn(10) // 固定大小
fmt.Println(r1, r2)
}
}
func main() {
mg.Add(1)
go f()
mg.Wait()
}
- 记得加随机因子哦!!!
那么 goroutine 和 线程 有什么关系呢?
- goroutine是用户态的线程,就是自己实现的线程,而平时说的线程指的是操作系统的线程OS线程。
- 他们的栈不同,OS线程一般是固定的栈内存(2MB),一个goroutine栈在最开始的时候是最小的情况下只有2kb,goroutine的栈不是固定的,他可以按照需求来进行增大和缩小,最大可以到达1GB,因为开始的时候只需要2KB就可以运行,所以我们搞几十万个都是没有问题的
goroutine调度问题
GMP是 go语言运行时(runtime)层面的实现,是go语言自己实现的一套的调度系统,区别于操作系统调度os线程。
- G很好理解,就是个goroutine的,里面除了存放本goroutine信息外 还有与所在P的绑定等信息。
- P管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。
- M(machine)是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系, 一个groutine最终是要放到M上执行的;
var mg sync.WaitGroup
func a() {
defer mg.Done()
for i := 0; i < 100; i++ {
fmt.Printf("A:%d\n", i)
}
}
func b() {
defer mg.Done()
for i := 0; i < 100; i++ {
fmt.Printf("B:%d\n", i)
}
}
func main() {
fmt.Println(runtime.NumCPU()) //cpu逻辑核心数,你有多少个cpu
mg.Add(2)
go b()
go a()
mg.Wait()
}
操作系统线程和goroutine的关系:
- 一个操作系统可以对应用户态多个goroutine
- go语言可以同时使用多个操作系统线程
- 所以他们的关系是 多对多的 , 即: m:n
channel
单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。
- Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。
- 如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。
channel是一种类型,一种引用类型。声明通道类型的格式如下:
var c chan int
func main() {
fmt.Println(c) // nil
c = make(chan int, 10) // 带数字就是定义了缓存区
fmt.Println(c) // 指针
}
- channel类型必须要进行初始化才可以使用,不然通道就会为nil
channel通道的三种操作
发送操作
- 将一个值发送到通道中
ch <- 10 // 发送
接收操作
- 从一个通道中接收值
x := <- ch // 从ch中接收值并赋值给x
<- ch // 从ch中接收值,但是忽悠结果
关闭操作
- 通过调用内置的close方法进行关闭
close(ch) // 关闭通道
然后来实验一下吧!
var ch chan int
var mg sync.WaitGroup
func main() {
mg.Add(1)
ch = make(chan int, 10)
go func() {
defer mg.Done()
x := <-ch
fmt.Println("从后台通道里面取到了数据x:", x)
}()
ch <- 10
fmt.Println("数据10发送到了通道里面!", ch)
mg.Wait()
}
然后我们可以利用这些做个小题目了,
- 先放入通道1 里面 100 个数据,再在通道2里面存放这一百个数据的平方,最后打印出来
var ch chan int
var ch2 chan int
func main() {
ch = make(chan int, 10)
ch2 = make(chan int, 10)
go func() {
for i := 0; i < 100; i++ {
ch <- i
}
close(ch)
}()
go func() {
for {
v, ok := <-ch // 通道在关闭后再取值,会返回false
if !ok {
break
}
x := v * v
ch2 <- x
}
close(ch2)
}()
for i := range ch2 {
fmt.Println(i)
}
}
- 结果很成果哈,但是为什么我们缓存10也能装下100个数据呢,
- 因为我们启动了两个线程哈,一边不停的放数据,一遍不停的取数据,这样就可以了,但是要记得关闭,不然会卡死在哪里的,因为缓存不够哈,啊哈哈哈哈哈
有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。
- 单向通道就可解决这个问题
单向通道
- chan<- int是一个只写单向通道(只能对其写入int类型值),可以对其执行发送操作但是不能执行接收操作;
- <-chan int是一个只读单向通道(只能从其读取int类型值),可以对其执行接收操作但是不能执行发送操作。
主要做参数,用来限制你只能上传值,或者取值
通道总结
channel | nil | 非空 | 空的 | 满了 | 没满 |
---|---|---|---|---|---|
接收 | 阻塞 | 接收值 | 阻塞 | 接收值 | 接收值 |
发送 | 阻塞 | 发送值 | 发送值 | 阻塞 | 发送值 |
关闭 | panic | 关闭成功,读完数据后返回false | 关闭成功返回false | 关闭成功,读取完数据后返回false | 关闭成功,读取完数据后返回false |
- 注意的是关闭已经关闭的管道也会爆出
panic
错误