G

[Golang基础语法] 协程与通道

RoLingG Golang 2024-03-20

协程

  1. 协程:在Go语言中关键字为go。一般有耗时函数,想要优化成并行运行函数就可以用。
  2. 协程是一起执行的,除非手动设置否则不分先后并发执行。
  3. 使用协程启用函数,对于该协程函数和main函数是并发执行的,main函数运行在一个特殊的协程上(主协程)。
  4. 主线程结束之后,协程函数也会结束。
  5. 启动一个新协程时会立即返回,程序控制不会等待协程执行完成,这就意味着调用协程函数是不会执行协程函数的任何逻辑。
  6. 调用协程后会立即返回到主函数代码接着向下运行,忽略该协程的任何返回值。
  7. 如果主协程终止,则程序终止,其他协程也就终止。所以为了让其他协程运行逻辑,我们可以让主协程后执行,也就是调用time.sleep函数,但是这样休眠主函数的时间是否能让新协程运行完逻辑是不确定的,这就关系到了通道的问题。
  8. 所以不推荐协程间使用休眠的方式来进行互相的等待。
  9. 现在通过下面的代码来验证上述大部分的说法:

    package main
    
    import (
        "fmt"
        "time"
    )
    
    func learning(name string) {
        fmt.Printf("%s 开始学习\n", name)
        time.Sleep(1 * time.Second)
        fmt.Printf("%s 结束学习\n", name)
    }
    
    func main() {
        startTime := time.Now()
    
        //下面这样子操作就像是接力,一个人去回来以后下一个人才能去
        //learning("张三")
        //learning("李四")
        //learning("王五")
    
        //使用协程就可以让他们一起去一起回了
        go learning("张三")
        go learning("李四")
        go learning("王五")
        //但是用了上面这个协程做法之后,会发现输出直接变成了main函数里的输出,结束了整个程序
        //原因是因为使用了协程之后,调用的learning函数就不是和main函数在一个线程了,这样main函数只用执行协程之外的命令
        //所以造成主协程结束之后,分支协程也结束,直接输出主协程结果后结束的现象
    
        //为此我们暂时解决上面那个问题就需要将main函数延时
        //time.Sleep(1 * time.Second)    //但是会发现延时一秒只完成了部分
        //所以这个方法只是土鳖方法,经不起推敲,延迟短 了运行不完整,延时长了造成浪费。
    
        fmt.Println("某人学习结束", time.Since(startTime))
    }
    
    • 所以可以用一个非常简陋的方法解决这些问题:
    package main
    
    import (
        "fmt"
        "time"
    )
    
    var wait int
    
    func learning(name string) {
        fmt.Printf("%s 开始学习\n", name)
        time.Sleep(1 * time.Second)
        fmt.Printf("%s 结束学习\n", name)
        wait--
    }
    
    func main() {
        startTime := time.Now()
        wait = 3
        
        go learning("张三")
        go learning("李四")
        go learning("王五")
    
        for {
            if wait == 0 {
                break
            }
        }
        fmt.Println("某人学习结束", time.Since(startTime))
    }
    //我们加一个wait值用来当信号量,当wait归零时即协程完成,就可以接着执行主函数的内容。
    • Go官方也想到了这个问题,所以给了一个sync.WaitGroup类型用来确认协程的数量。我们可以用这个来让这段代码完善起来:
    • WaitGroup的方法:

      Add()    初始值是0,累加子协程的数量
      Done()    当某个子协程完成后,计数器-1,通常用defer调用
      Wait()    阻塞当前协程,直到实例的计数器归零
    package main
    
    import (
        "fmt"
        "sync"
        "time"
    )
    
    //wait不是全局变量,所以也要将函数参数进行改变
    //wait用指针的原因就和前面指针那一章讲的一样,主函数的wait与函数参数的wait是不一样的,函数参数的wait是拷贝过来的,所以要加指针去指向主函数的原wait。
    func learning(name string, wait *sync.WaitGroup) {
        fmt.Printf("%s 开始学习\n", name)
        time.Sleep(1 * time.Second)
        fmt.Printf("%s 结束学习\n", name)
        wait.Done()    //告知协程完成
    }
    
    func main() {
        startTime := time.Now()
        var wait sync.WaitGroup    //这样wait可以不用当全局变量,避免了许多隐性问题
    
        wait.Add(3)    //事先添加协程的总数量有多少个
        go learning("张三", &wait)
        go learning("李四", &wait)
        go learning("王五", &wait)
    
        wait.Wait()    //等待添加的携程都结束
        fmt.Println("某人学习结束", time.Since(startTime))
    }
    • 其实定义WaitGroup用全局变量也可以,但是程序复杂起来还是得在主函数内声明方便,而且全局变量本身也会引发一些其他问题。
    • 所以最终还是推荐能在主函数内声明就在主函数内声明。
  10. 要启动多协程则要搞懂新协程与主协程之间的关系。
  11. 还有就是如果要从协程内拿取东西出来进行操作,也和通道(channel)有关。

通道(channel)

/*
    通道的声明    //chan为channel的缩写
    var channel_name chan channel_type
*/
  1. 通道:Go语言中协程之间通信的管道就叫通道(信道、channel),在Go语言中,通道的关键字为chan
  2. 如果运行报错:fatal error: all goroutines are asleep - deadlock!说明协程被死锁了。举个例子:

    package main
    
    import (
        "fmt"
        "sync"
        "time"
    )
    
    //var book chan string //只能存对应的单一类型,和map差不多,一开始创建内部的值就是nil
    
    // 想要初始化通道可以用make函数,声明并初始化一个长度为0的通道
    var bookChan = make(chan string) //通道也是有长度的
    // 长度为0的通道就相当于人搬运一个货物只能抓着腾不出手,等人拿了才能接着拿别的货。长度为1的通道类推,人可以放一个货之后再抓住一个货,取的人可以两个货一起取也可以二选一。
    
    func learning(name string, book string, wait *sync.WaitGroup) {
        fmt.Printf("%s 开始学习\n", name)
        time.Sleep(1 * time.Second)
        fmt.Printf("%s 结束学习\n", name)
    
        bookChan <- book //<-为通道赋值标识符
    
        wait.Done()
    }
    
    func main() {
        startTime := time.Now()
        var wait sync.WaitGroup
    
        wait.Add(3)
    
        go learning("张三", "大学高数", &wait)
        go learning("李四", "大学英语", &wait)
        go learning("王五", "大学物理", &wait)
    
        wait.Wait()
        fmt.Println("某人学习结束", time.Since(startTime))
    }
    //这时候写的不完善程序直接运行会报错,原因出在于通道的长度以及没有命令去通道取东西出来导致死锁。
    • 所以优化一下:
    package main
    
    import (
        "fmt"
        "sync"
        "time"
    )
    
    //var book chan string //只能存对应的单一类型,和map差不多,一开始创建内部的值就是nil
    
    // 想要初始化通道可以用make函数,声明并初始化一个长度为0的通道
    var bookChan = make(chan string) //通道也是有长度的
    // 长度为0的通道就相当于人搬运一个货物只能抓着腾不出手,等人拿了才能接着拿别的货。长度为1的通道类推,人可以放一个货之后再抓住一个货,取的人可以两个货一起取也可以二选一。
    
    func learning(name string, book string, wait *sync.WaitGroup) {
        fmt.Printf("%s 开始学习\n", name)
        time.Sleep(1 * time.Second)
        fmt.Printf("%s 结束学习\n", name)
    
        bookChan <- book //<-为通道赋值标识符
    
        wait.Done()
    }
    
    func main() {
        startTime := time.Now()
        var wait sync.WaitGroup
    
        wait.Add(3)
    
        go learning("张三", "大学高数", &wait)
        go learning("李四", "大学英语", &wait)
        go learning("王五", "大学物理", &wait)
        
        for {    //这里的死循环充当阻塞作用
            bookName, ok := <-bookChan    //这样这里还是会出问题,因为是死循环,当通道内的东西被拿完后还会接着拿,就会接着触发死锁。
            fmt.Println(bookName, ok)
            if !ok {
                fmt.Println("协程内值拿不了")
                break
            }
        }
    
        wait.Wait()
        fmt.Println("某人学习结束", time.Since(startTime))
    }
    • 所以要解决上面代码的问题,就要在合适的时机将通道关闭:
    package main
    
    import (
        "fmt"
        "sync"
        "time"
    )
    
    var bookChan = make(chan string)
    
    func learning(name string, book string, wait *sync.WaitGroup) {
        fmt.Printf("%s 开始学习\n", name)
        time.Sleep(1 * time.Second)
        fmt.Printf("%s 结束学习\n", name)
    
        bookChan <- book //<-为通道赋值标识符
    
        wait.Done()
    }
    
    func main() {
        startTime := time.Now()
        var wait sync.WaitGroup
    
        wait.Add(3)
    
        go learning("张三", "大学高数", &wait)
        go learning("李四", "大学英语", &wait)
        go learning("王五", "大学物理", &wait)
        
        //新建一个协程函数
        go func() {
            defer close(bookChan) //写法②
            wait.Wait()
            //close(bookChan)    //写法①
        }()
        
        //for {
        //    bookName, ok := <-bookChan
        //    fmt.Println(bookName, ok)
        //    if !ok {
        //        fmt.Println("协程内值拿不了")
        //        break
        //    }
        //}
    
        //简便写法
        var bookList []string
        for bookName := range bookChan {
            bookList = append(bookList, bookName)
        }
    
        wait.Wait()
        fmt.Println("某人学习结束", time.Since(startTime))
        fmt.Println("刚刚的人都学了", bookList)
    }

    然后细心的可能会发现,协程的输出顺序是无序的,每次可能都会不一样,这是因为协程是一起并行执行的,更何况这三个协程调用的是同一个协程函数,运行完成时间几乎一致,所以导致的输出没有顺序。

PREV
[Golang基础语法] 接口
NEXT
[labuladong学习笔记] Linux文件系统

评论(0)

发布评论