Go 并发
2021/7/12 6:06:01
本文主要是介绍Go 并发,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
目录- 概念
- goroutien的规则
- go语言的闭包问题
- 解决闭包问题
- 为什么要给随机数添加种子?
- goroutine 什么时候结束?
- 下面两种情况下,会导致 goroutine 结束
- wg WaitGroup
- goroutine调度
- 4.1 可增长的栈
- goroutien 调度
- channel
- channel的定义, chan int 才是一个完整的定义!
- 下面一个不指定缓冲通道数的小例子
- 使用指定缓存区大小
- 关闭通道
- 单通道限制
- 通道的总结:
- select
- 通道详解
- 1. 小例子,一个函数是从通道里读值,一个是从通道里写值
- select 和time.After的例子
- 阻塞和死锁
- 自己写一个死锁
- 利用 goroutine 来实现装配线
- channel的定义, chan int 才是一个完整的定义!
概念
- 并发是同一时间段执行多个任务,(你同时和两个女生聊天)
- 并行是同一时刻执行多个任务,(你和你朋友在和女生聊天)
Go 语言的并发是通过 goroutine
实现的,goroutine
类似于线程,属于用户态的线程(程序员自己编写的) ,我们可以根据需要创建成千上万的 goroutine
并发工作,goroutine
是由 Go 语言的运行时 runtime
调度实现的,而线程是由操作系统调度完成。
Go 语言还提供 channel
在多个 goroutine
间通信。
goroutien的规则
-
Go 语言使用
goroutine
非常简单,只需要在调用函数的时候,前边加个go
就可以了 -
一个
goroutine
必定对应一个函数,可以创建多个goroutine
去执行相同的函数,比如下面例子
func f1(i int) { fmt.Println(i) } func main() { for i := 0; i < 100000; i++ { go f1(i) } fmt.Println("main") }
go语言的闭包问题
闭包就是在函数内部,引用外部的变量了,因为外边循环的快,里面循环的慢,所以导致了这种结果。
func main() { for i := 0; i < 10; i++ { go func() { fmt.Print(i, "\t") // 这个里面的东西,是作用域外边获得的,闭包 }() } fmt.Println("main") time.Sleep(time.Second) } // 输出 3 7 5 7 3 7 3 main
解决闭包问题
想解决闭包问题用很简单,不要让函数读外边的值,而是让函数直接传值
func main() { for i := 0; i < 10; i++ { go func(i int) { fmt.Print(i, "\t") // 这个里面的东西,是作用域外边获得的,闭包 }(i) } fmt.Println("main") time.Sleep(time.Second) } // 输出如下,顺序是乱的,因为是异步操作,但是数据并不会重复 main 4 1 0 2 6 3 5 7 8 9
为什么要给随机数添加种子?
- 如果不添加种子,每次编译好的代码都是相同的,所以运行出来的随机数也是相同的
- 因为使用时间戳的毫秒数肯定不一样,所以可以拿他当种子
import ( "fmt" "math/rand" ) func main() { for i := 0; i < 5; i++ { r1 := rand.Int() r2 := rand.Intn(9) // 可以指定最大值 fmt.Println(r1, r2) } } // 输出 5577006791947779410 6 6129484611666145821 2 3916589616287113937 6 605394647632969758 8 894385949183117216 6
加上 种子
以后,打印:
func main() { rand.Seed(time.Now().UnixNano()) // 用那秒速 for i := 0; i < 5; i++ { r1 := rand.Int() r2 := rand.Intn(9) // 可以指定最大值 fmt.Println(r1, r2) } }
goroutine 什么时候结束?
下面两种情况下,会导致 goroutine 结束
goroutine
对应的函数结束了,goroutine
就结束了main
函数结束了,由main
函数创建的那些goroutine
就都结束了
下面改掉之前用 sleep 的说法,用高级点的方法
wg WaitGroup
wg WaitGroup
wg 只有三种方法,而且这三种方法,用的时候要一起用
- Add
- Done
- Wait
下面是个例子
func f1(i int) { defer wg.Done() //开头就用,而且一定要在 defer 的后面 fmt.Print(i, " ") } var wg sync.WaitGroup func main() { for i := 0; i < 10; i++ { wg.Add(1) //在 goroutine 执行之前加这个 go f1(i) } wg.Wait() //所有的 goroutine 后面执行这个 } // 输出 // 2 5 1 4 7 9 6 8 3 0
goroutine调度
4.1 可增长的栈
OS栈
(操作系统线程)一般都有固定的栈内存(通常是2M),而一个goroutine
在其生命周期开始的时候,只有很小的栈(通常2kb),goroutine
的栈不是固定的,他会按需增大或者缩小,goroutine
的栈的大小,可以限制到 1GB,虽然极少会用到这么大,所以在 go 中,一次创建十万的 goroutine
也是可以的。
goroutien 调度
GMP 是 GO 语言运行时(runtime) 层面的实现,是go 语言自己实现的一套调度系统,区别于系统调度 OS 线程。
-
使用
runtime.GOMAXPROCS(1)
命令断定核数 -
如果不配置,那么默认是跑满
-
defer wg.Done()
应该加derfer,保证在最后调用
package main import ( "fmt" "runtime" "sync" ) func f1() { defer wg.Done() //应当是 derfer 后调用 for i := 0; i < 10; i++ { fmt.Println("A", i) } } func f2() { defer wg.Done() // 应当使用derfer 后调用 for i := 0; i < 10; i++ { fmt.Println("B", i) } } var wg sync.WaitGroup func main() { fmt.Println("打印CPU 的核心数") fmt.Println(runtime.NumCPU()) runtime.GOMAXPROCS(1) // 单核使用,不填的话,始终占满真个线程 wg.Add(2) go f1() go f2() wg.Wait() }
channel
channel的定义, chan int 才是一个完整的定义!
-
单纯的将函数并发执行是没有意义的,函数与函数之间,只有交换数据, 才能体现并发函数的意义
-
虽然可以使用共享内存来实现数据的交互,但是共享内存在不同的
gorontine
中,容易发生竞态问题,为了保证数据交互的正常性。不使用互斥量对内存进行加锁,这种做法势必造成性能问题 -
GO语言的并发模式是
CSP
,提倡通过通信实现共享内存,而不是通过共享内存而实现通信 -
如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine 的通信机制。
-
Go语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
下面一个不指定缓冲通道数的小例子
var b chan int var wg sync.WaitGroup func f1() { defer wg.Done() c := <-b fmt.Println("f1函数", c) } func main() { fmt.Println(b) b = make(chan int) // 不指定缓冲通道数 wg.Add(1) go f1() // 应该先取值,然后再放值 b <- 10 fmt.Println("函数已经完毕") wg.Wait() } // 输出 <nil> f1函数 10 函数已经完毕
使用指定缓存区大小
这样会出现死锁的情况,因为指定了缓存区只能存一个,现在你硬往里面存两个。
var b chan int func main() { b = make(chan int, 1) fmt.Println(b) b <- 10 b <- 20 y := <-b fmt.Println(y) } // 输出错误 // fatal error: all goroutines are asleep - deadlock!
关闭通道
关闭通道往往通过内置的 close
函数,关于通道,需要注意的是,通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在操作文件之后,关闭文件是必须的,但是关闭通道不是必须的。
close(b)
一个小例子,但是不知道 bug 在什么地方,以后再改吧。
/* 启动一个 goroutine ,生成100 个数,发送到 ch1 启动一个 goroutien,从ch1 中取值,计算其平方,然后放到 ch2 中 在 main 中,从 ch2 中取值 */ var wg sync.WaitGroup var a chan int var b chan int func f1(ch1 chan int) { defer wg.Done() for i := 0; i < 100; i++ { ch1 <- i } } func f2(ch1, ch2 chan int) { defer wg.Done() for x := range ch1 { ch2 <- x * x } } func main() { a = make(chan int) b = make(chan int) wg.Add(2) go f1(a) go f2(a, b) wg.Wait() for ret := range b { fmt.Println(ret) } }
单通道限制
单通道一般用在函数的参数里面,限定参数只能读或者只能写。
func f2(ch1 <-chan int, ch2 chan<- int) { defer wg.Done() for x := range ch1 { ch2 <- x * x } }
通道的总结:
阻塞就是一个goroutine在等着。死锁是所有的groutine 都在等。
select
在某些场景中,我们需要从多个通道接受数据,通道在接收数据的时候,如果没有数据将会发生阻塞,当然你可以使用 if
语句进行判断,但是这样性能就会差很多,此时,你可以使用 Go
内置的 select
关键字,同时响应多个通道的操作,select
语句的使用,类似于 switch
语句,它会有一些列 case
分支和一个默认分支,每个 case
都对应一个通道的通信(接受 或者发送过程)。select
会一直在那里等,直到某个 case
的通信操作完成时,就会执行case
对应的语句
简单点来说,就是 select 执行的语句是随机的,不一定执行哪一句,但是如果某一句不满足的话,他肯定不会执行。
func main() { ch := make(chan int, 1) for i := 0; i < 10; i++ { select { case x := <-ch: fmt.Println(x) // 从 x 中取值 case ch <- i: // 后面必须加 : fmt.Println("放值") } } } // 输出 放值 0 放值 2 放值 4 放值 6 放值 8
出现上面这种情况,主要是因为通道里面只能放一个值,所以他只能执行第二个 case
如果暂存区的大于1的话,那么输出的值就不确定了,结果如下:
func main() { ch := make(chan int, 10) for i := 0; i < 10; i++ { select { case x := <-ch: fmt.Println(x) // 从 x 中取值 case ch <- i: // 后面必须加 : fmt.Println("放值") } } } // 输出 放值 0 放值 放值 放值 放值 2 3 放值 4
通道详解
1. 小例子,一个函数是从通道里读值,一个是从通道里写值
func main() { c := make(chan int) for i := 0; i < 5; i++ { go f1(c, i) } for i := 0; i < 5; i++ { s := <-c fmt.Println("从通道里取值", s) } } // 输出 往通道里传值 4 从通道里取值 4 往通道里传值 2 从通道里取值 2 往通道里传值 1 从通道里取值 1 往通道里传值 3 从通道里取值 3 往通道里传值 0 从通道里取值 0
上面的程序可以用下面这个图来说明一下
select 和time.After的例子
现实中,我们经常用到的一种情况就是,我们打印的时候,不想等太久。如果超时了,就不再等了。
func f1(id int, ch1 chan int) { time.Sleep(time.Duration(rand.Intn(4)) * time.Second) ch1 <- id // 往通道里放值 } func main() { c := make(chan int) timeout := time.After(2 * time.Second) // 两秒 for i := 0; i < 5; i++ { go f1(i,c) } for i := 0; i < 5; i++ { select { case b := <-c: fmt.Println(b) case <-timeout: fmt.Println("超时了不打印") } } } // 输出结果 0 3 超时了不打印 4 2
nil 通道的用处
- 对于包含
select
语句的循环,如果不希望每次循环都等待select
所涉及的所有通道数,那么可以将某些通道数设为nil
等到发送值准备就绪之后,再将通道变成一个非nil 的值并执行发送操作。
阻塞和死锁
- 当
goroutine
在等待通道的发送或接受时,我们就说他被阻塞了 - 除了
goroutine
本身占用少量的内存外,被阻塞的goroutien
并不会消耗任何其他的资源,goroutien
静静的停在那里,等待导致其阻塞的事情来解除阻塞 - 当一个或多个
goroutien
因为某些无法发生的事情被阻塞死,我们称这种情况为死锁,而出现死锁的程序通常会崩溃或者挂起
自己写一个死锁
因为他要从c中取值,但是c用于不可能有值,所以就被死锁了。
func main() { c := make(chan int) <-c }
利用 goroutine 来实现装配线
- Go 允许在没有值可发送的情况下,通过
close
函数关闭通道 ,例如close(c)
- 通道被关闭以后,将无法写入任何值,但是可以读,如果尝试写入,就会引发
panic
- 尝试读取被关闭的通道会获得与通道类型对应的零值
- 注意:如果循环里读取一个已关闭的通道,并没有检测通道是否关闭,那么该循环就会一直运转下去,消耗大量的 cpu 时间
这篇关于Go 并发的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-15SendGrid 的 Go 客户端库怎么实现同时向多个邮箱发送邮件?-icode9专业技术文章分享
- 2024-11-15SendGrid 的 Go 客户端库怎么设置header 和 标签tag 呢?-icode9专业技术文章分享
- 2024-11-12Cargo deny安装指路
- 2024-11-02MongoDB项目实战:从入门到初级应用
- 2024-11-01随时随地一键转录,Google Cloud 新模型 Chirp 2 让语音识别更上一层楼
- 2024-10-25Google Cloud动手实验详解:如何在Cloud Run上开发无服务器应用
- 2024-10-24AI ?先驱齐聚 BAAI 2024,发布大规模语言、多模态、具身、生物计算以及 FlagOpen 2.0 等 AI 模型创新成果。
- 2024-10-20goland工具下,如修改一个项目的标准库SDK的版本-icode9专业技术文章分享
- 2024-10-17Go学习:初学者的简单教程
- 2024-10-17Go学习:新手入门完全指南