经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » Go语言 » 查看文章
Go并发4种方法简明讲解
来源:jb51  时间:2022/4/7 8:47:49  对本文有异议

一、goroutine

1、协程(Coroutine)

Golang 在语言层面对并发编程进行了支持,使用了一种协程(goroutine)机制,

协程本质上是一种用户态线程,不需要操作系统来进行抢占式调度,但是又寄生于线程中,因此系统开销极小,可以有效的提高线程的任务并发性,而避免多线程的缺点。但是协程需要语言上的支持,需要用户自己实现调度器,因为在Go语言中,实现了调度器所以我们可以很方便的能过 go关键字来使用协程。

  1. func main() {
  2. for i := 0; i <10; i++ {
  3. go func(i int) {
  4. for {
  5. fmt.Printf("Hello goroutine %d\n",i)
  6. }
  7. }(i)
  8. }
  9. time.Sleep(time.Millisecond)
  10. }

最简单的一个并发编程小例子,并发输出一段话。

我们同时开了10个协程进行输出,每次在fmt.printf时交出控制权(不一定每次都会交出控制权),回到调度器中,再由调度器分配。

2、goroutine 可能切换的点

  • I/O,Select
  • channel
  • 等待锁
  • 函数调用
  • runtime.Gosched()

我们看一个小例子:

  1. func main() {
  2. var a [10]int
  3. for i := 0; i <10; i++ {
  4. go func(i int) {
  5. for {
  6. a[i]++
  7. }
  8. }(i)
  9. }
  10. time.Sleep(time.Millisecond)
  11. fmt.Println(a)
  12. }

在这里,代码直接锁死,程序没有退出,因为在执行函数中没有协程的切换,因为 main函数也是一个协程。

如果想要程序退出,可以通过 runtime.Gosched()函数,在执行函数中添加一行。

  1. for {
  2. a[i]++
  3. runtime.Gosched()
  4. }

加上这个函数之后,代码是可以正常执行了,但是真的是正常执行吗?不一定,我们可以使用 -reac命令来看一下数据是否有冲突:

这说明数据还是有冲突的,数组a中的元素一边在做自增,一边在输出。解决这个问题,我们只能使用 channel 来解决。

二、Channel

Channel 中 Go语言在语言级别提供了对 goroutine 之间通信的支持,我们可以使用 channel 在两个或者多个goroutine之间进行信息传递,能过 channel 传递对像的过程和调用函数时的参数传递行为一样,可以传递普通参数和指针。

Channel 有两种模式:

  1. var ch1 = make(chan int) // 无缓冲 channel,同步
  2. var ch2 = make(chan int, 2) // 有缓冲 channel, 异步

无缓冲的方式,数据进入 channel 只要没有被接收,就会处在阻塞状态。

  1. var ch1 = make(chan int) // 无缓冲 channel,同步
  2. ch1 <- 1
  3. ch1 <- 2
  4. // error: all goroutines are asleep - deadlock!
  5. fmt.Println(<-ch1)

如果想要运行,必须要再开一个协程不停的去请求数据:

  1. var ch1 = make(chan int) // 无缓冲 channel,同步
  2. go func() {
  3. for {
  4. n := <-ch1
  5. fmt.Println(n)
  6. }
  7. }()
  8. ch1 <- 1
  9. ch1 <- 2

有缓冲的方式,只要缓冲区没有满就可以一直进数据,缓冲区在填满之后没有接收也会处理阻塞状态。

  1. func bufferChannel() {
  2. var ch2 = make(chan int,2)
  3. ch2<-1
  4. ch2<-2
  5. fmt.Println(ch2)
  6. // 不加这一行的话,是可以正常运行的
  7. ch2<-3 // error: all goroutines are asleep - deadlock!
  8. }

1、chaanel 指定方向

比如我现在有一个函数创建一个 channel,并且不断的需要消费channel中的数据:

  1. func worker(ch chan int) {
  2. for {
  3. fmt.Printf("hello goroutine worker %d\n", <-ch)
  4. }
  5. }
  6.  
  7. func createWorker() chan int{
  8. ch := make(chan int)
  9. go worker(ch)
  10. return ch
  11. }
  12.  
  13. func main() {
  14. ch := createWorker()
  15. ch<-1
  16. ch<-2
  17. ch<-3
  18. time.Sleep(time.Millisecond)
  19. }

这个函数我是要给别人用的,但是我怎么保证使用 createWorker 函数创建的 channel 都是往里面传入数据的呢?

如果外面有人消费了这个 channel 中的数据,我们怎么限制?

这个时候,我们就可以给返回的channel 加上方向,指明这个 channel 中能往里传入数据,不能从中取数据:

  1. func worker(ch <-chan int) {
  2. for {
  3. fmt.Printf("hello goroutine worker %d\n", <-ch)
  4. }
  5. }
  6.  
  7. func createWorker() chan<- int{
  8. ch := make(chan int)
  9. go worker(ch)
  10. return ch
  11. }

我们可以在返回 channel 的地方加上方向,指明返回的函数只能是一个往里传入数据,不能从中取数据。

并且我们还可以给专门消费的函数加上一个方向,指明这个函数只能出不能进。

2、channel 关闭

在使用 channel 的时候,随说我们可以等待channel中的函数使用完之后自己结束,或者等待 main 函数结束时关闭所有的 goroutine 函数,但是这样的方式显示不够优雅。

当一个数据我们明确知道他的结束时候,我们可以发送一个关闭信息给这个 channel ,当这个 channel 接收到这个信号之后,自己关闭。

  1. // 方法一
  2. func worker(ch <-chan int) {
  3. for {
  4. if c ,ok := <- ch;ok{
  5. fmt.Printf("hello goroutine worker %d\n", c)
  6. }else {
  7. break
  8. }
  9. }
  10. }
  11. // 方法二
  12. func worker(ch <-chan int) {
  13. for c := range ch{
  14. fmt.Printf("hello goroutine worker %d\n", c)
  15. }
  16. }
  17.  
  18. func main() {
  19. ch := createWorker()
  20. ch<-1
  21. ch<-2
  22. ch<-3
  23. close(ch)
  24. time.Sleep(time.Millisecond)
  25. }

通过 Closeb函数,我们可以能过 channel 已经关闭,并且我们还可以通过两种方法判断通道内是否还有值。

三、Select

当我们在实际开发中,我们一般同时处理两个或者多个 channel 的数据,我们想要完成一个那个 channel 先来数据,我们先来处理个那 channel 怎么办呢?

此时,我们就可以使用 select 调度:

  1. func genInt() chan int {
  2. ch := make(chan int)
  3. go func() {
  4. i := 0
  5. for {
  6. // 随机两秒以内生成一次数据
  7. time.Sleep(time.Duration(rand.Intn(2000)) * time.Millisecond)
  8. ch <- i
  9. i++
  10. }
  11. }()
  12. return ch
  13. }
  14.  
  15. func main() {
  16. var c1 = genInt()
  17. var c2 = genInt()
  18. for {
  19. select {
  20. case n := <-c1:
  21. fmt.Printf("server 1 generator %d\n", n)
  22. case n := <- c2:
  23. fmt.Printf("server 2 generator %d\n", n)
  24. }
  25. }
  26. }

1、定时器

  1. for {
  2. tick := time.Tick(time.Second)
  3. select {
  4. case n := <-c1:
  5. fmt.Printf("server 1 generator %d\n", n)
  6. case n := <-c2:
  7. fmt.Printf("server 2 generator %d\n", n)
  8. case <-tick:
  9. fmt.Println("定时每秒输出一次!")
  10. }
  11. }

2、超时

  1. for {
  2. tick := time.Tick(time.Second)
  3. select {
  4. case n := <-c1:
  5. fmt.Printf("server 1 generator %d\n", n)
  6. case n := <-c2:
  7. fmt.Printf("server 2 generator %d\n", n)
  8. case <-tick:
  9. fmt.Println("定时每秒输出一次!")
  10. case <-time.After(1300 * time.Millisecond): // 如果 1.3秒内没有数据进来,那么就输出超时
  11. fmt.Println("timeout")
  12. }
  13. }

四、传统的并发控制

1、sync.Mutex

  1. type atomicInt struct {
  2. value int
  3. lock sync.Mutex
  4. }
  5.  
  6. func (a *atomicInt) increment() {
  7. a.lock.Lock()
  8. defer a.lock.Unlock() // 使用 defer 解锁,以防忘记
  9. a.value++
  10. }
  11.  
  12. func main() {
  13. var a atomicInt
  14. a.increment()
  15. go func() {
  16. a.increment()
  17. }()
  18. time.Sleep(time.Millisecond)
  19. fmt.Println(a.value)
  20. }

2、sync.WaitGroup

  1. type waitGrouInt struct {
  2. value int
  3. wg sync.WaitGroup
  4. }
  5.  
  6. func (w *waitGrouInt) addInt() {
  7. w.wg.Add(1)
  8. w.value++
  9. }
  10.  
  11. func main() {
  12. var w waitGrouInt
  13. for i := 0; i < 10; i++ {
  14. w.addInt()
  15. w.wg.Done()
  16. }
  17. w.wg.Wait()
  18. fmt.Println(w.value)
  19. }
  20.  

 更多关于Go并发简明讲解请查看下面的相关链接

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号