go36-16,17-goroutine(代码片段)

author author     2023-02-22     773

关键词:

go语句及其执行规则

学习之前先看一下下面这句话:

Don’t communicate by sharing memory; share memory by communicating.
不要通过共享数据来通讯,要以通讯的方式共享数据。

通道(也就是 channel)类型的值可以被用来以通讯的方式共享数据。更具体地说,它一般被用来在不同的goroutine之间传递数据。
这篇主要讲goroutine是什么。简单来说,goroutine代表着并发编程模型中的用户级线程。

调度器

Go语言不但有着独特的并发编程模型,以及用户级线程goroutine,还拥有强大的用于调度goroutine、对接系统级线程的调度器。
这个调度器是Go语言运行时系统的重要组成部分,它主要负责统筹调配Go并发编程模型中的三个主要元素:

  • G(goroutine 的缩写),用户级线程
  • P(processor 的缩写),一种可以承载若干个G,且能够使用这些G适时的与M进行对接,并得到真正运行的中介
  • M(machine 的缩写),系统级线程

主goroutine

这里需要知道一个与主goroutine有关的重要特性,一旦主goroutine中的代码(也就是main函数中的那些代码)执行完毕,当前的 Go 程序就会结束运行。
先看下面这个例子:

package main

import "fmt"

func main() 
    for i := 0; i < 10; i++ 
        go func() 
            fmt.Println(i)
        ()
    

上面的程序运行之后,不会有打印任何内容。
只要go语句本身执行完毕,Go程序完全不会等待go函数的执行,它会立刻去执行后面的语句。这就是所谓的异步并发地执行。
在上面的例子中,在for语句执行完毕后,里面包装的10个goroutine还没有获得运行的机会,主goroutine中的代码执行完了,Go程序就会立即结束运行。

使用Sleep等待

上面的例子中,如果要让程序在其他goroutine运行完之后再退出。最简单粗暴的办法是Sleep一段时间:

package main

import (
    "fmt"
    "time"
)

func main() 
    for i := 0; i < 10; i++ 
        go func() 
            fmt.Println(i)
        ()
    
    time.Sleep(time.Second)

这个办法可行,但是Sleep的时间需要预估。太长会浪费时间,太短则不能保证所有goroutine都运行完毕。不容易预估时间,最好是让其他的goroutine在运行完毕后发送通知。

让主goroutine等待其他goroutine

使用通道,通道的长度与启用的goroutine的数量一致。每个goroutine运行完毕前,都向通道发送一个值。在主goroutine则是从这个通道接收值,接收了足够数量的次数后就说明所有goroutine都运行完毕了,可以继续往下执行了(就是退出):

package main

import "fmt"

func main() 
    sign := make(chan struct, 10)
    for i := 0; i < 10; i++ 
        go func() 
            fmt.Println(i)
            sign <- struct
        ()
    
    for j := 0; j < 10; j++ 
        <- sign
    

这里声明的通道的类型是 chan struct ,是一个空结构体。它谭勇的内存空间是0字节。这个值在整个Go程序中永远都只会存在一份。虽然可以无数次的使用这个值字面量,但是用到的都是同一个值。当把通道仅仅刀座是传递某个简单信号的介质的时候,使用空结构体是最好的。
其他方式
在标准库中,有一个sync包,里面有一个sync.WaitGroup类型。这应该是一个更好的实现方式。不过这要等后面讲sync包的时候再说了。

让多个goroutine按照既定的顺序执行

首先改造一下一只使用的例子,把变量i的值传递给每个goroutine,这样输出的是0-9各一次,不过是乱序的:

for i := 0; i < 10; i++ 
    go func(i int) 
        fmt.Println(i)
        sign <- struct
    

讲师的例子

package main

import (
    "fmt"
    "sync/atomic"
    "time"
)

var count uint32

func trigger (i uint32, fn func()) 
    for 
        if n := atomic.LoadUint32(&count); n == i 
            fn()
            atomic.AddUint32(&count, 1)
            break
        
        time.Sleep(time.Nanosecond)
    


func main() 
    for i := uint32(0); i < 10; i++ 
        go func(i uint32) 
            fn := func() 
                fmt.Println(i)
            
            trigger(i, fn)
        (i)
    
    trigger(10, func() )

主要就是trigger函数。在trigger里会检查i,并把要执行的语句打包成fn函数也传入,只有在trigger里判断后符合条件,就会执行fn函数的语句。
trigger里会检查i和count是否相等,在执行了fn函数后,需要把count加1,这里用了原子操作。里有是trigger函数会被多个goroutine并发的调用,所以这个变量被多个用户级线程共用了。因此对它的操作就产生了竞态条件(race condition),破坏了程序的并发安全性。
在最后退出的时候,应该有了trigger函数,只要检查count是否到10了,就表示其他goroutine都执行完了,所以也就不需要通道了。
另外在trigger函数里,是一个for语句的无限循环,在判断条件不成立后,先进行了一个1纳秒的Sleep。如果不加这句的话,测试下来,偶尔会出现程序卡住的情况(甚至是死机)。这里加上Sleep语句应该是希望这个时候程序可以进行一下切换,否则当前应该执行的那个goroutine如果拿不到执行的机会,其他goroutine也都无法通过if条件的判断。

自己的实现

package main

import (
    "fmt"
    "time"
)

func main() 
    sign := make(chan struct, 10)
    var count int
    for i := 0; i < 10; i++ 
        go func(i int) 
            for 
                if count == i
                    fmt.Println(i)
                    count ++
                    sign <- struct
                    break
                
                time.Sleep(time.Nanosecond)
            
        (i)
    
    for j := 0; j < 10; j++ 
        <- sign
    

主要两个问题,当时没有意识到在for无限循环之后,进入下一个迭代前,这个1纳秒Sleep的意义。还有就是我没有使用原子操作。不过这里即使不用原子操作也没问题的样子,因为逻辑上通知只有一个goroutine满足条件会去操作共用的变量count。所以这里和上面讲师的示例就差在对变量count的比较和判断是否是原子操作的问题上了。

原子操作
这里再自我做一些补充。

原子操作,即执行过程不能被中断的操作(并发)。
经典问题:i++是不是原子操作?
答案是否,因为i++看上去只有一行,但是背后包括了多个操作:取值,加法,赋值。

golanglddtree.go(代码片段)

查看详情

golangreplace.go(代码片段)

查看详情

golangbus.go(代码片段)

查看详情

golangmamon.go(代码片段)

查看详情

golanggrace.go(代码片段)

查看详情

golangchannels.go(代码片段)

查看详情

golangpicfetcher.go(代码片段)

查看详情

golangkafka.go(代码片段)

查看详情

golangpanic.go(代码片段)

查看详情

golanghiromi.go(代码片段)

查看详情

golangtime.go(代码片段)

查看详情

golangconsumernew.go(代码片段)

查看详情

golangfibonacci.go(代码片段)

查看详情

golangconfig.go(代码片段)

查看详情

golanghttpclient.go(代码片段)

查看详情

golangcrossing.go(代码片段)

查看详情

golangshinsekai.go(代码片段)

查看详情

golangunshorten.go(代码片段)

查看详情