文章506
标签266
分类65

Go创建Goroutine时显式调用时的坑

在Go中可以直接通过go关键字直接创建一个goroutine并在子goroutine中直接调用函数;

但是有时候由于调用的方式不同会存在一些问题;

源代码:


Go创建Goroutine时显式调用时的坑

Goroutine调用概述

对于go关键字创建新goroutine并调用函数的方式有两种:

  • 隐式传参调用
  • 显式传参调用
func main() {
    x := 1
    // 隐式传参调用
    // 此时传入的是x的“引用值”,即两个x指向的是同一个内存地址,在子routine中修改的值,会改变外部的x!
    go func() {
        fmt.Println(x)
    }()

    // 直接传参调用
    // 此时为值传递,内部的x不会影响外部的x;
    go func(x int) {
        fmt.Println(x)
    }(x)
}

两者的区别在于:

  • 当隐式传参时:此时传入的是x的“引用值”,即两个x指向的是同一个内存地址,在子routine中修改的值,会改变外部的x!
  • 当显式传参时:此时为值传递,内部的x不会影响外部的x;

另外,需要注意的:显式的传参,在传参时就必须将参数计算好,这一点和defer函数是相同的!

例如:

func main() {
    wg := sync.WaitGroup{}
    wg.Add(2)

    x := 1
    // 隐式传参调用
    // 此时传入的是x的“引用值”,即两个x指向的是同一个内存地址,在子routine中修改的值,会改变外部的x!
    go func() {
        fmt.Printf("Implicit invoke: %d\n", x)
        wg.Done()
    }()

    // 直接传参调用
    // 此时为值传递,内部的x不会影响外部的x;
    go func(x int) {
        fmt.Printf("Direct invoke: %d\n", x)
        wg.Done()
    }(x)

    x = 3

    wg.Wait()
}

上面的函数大概率输出为:

Direct invoke: 1
Implicit invoke: 3

这是因为,在直接传参调用时,x的值还未被修改(仍然是1)并且已经被确定,而隐式传参调用会根据外部x值的改变而改变;

之所以说是大概率是因为,一般情况下,隐式传参调用的goroutine执行速度还是比main中执行至x=3语句要慢的,所以,大概率会先执行x=3修改x的值,随后才会执行隐式传参调用!


一道关于 Goroutine 的题

下面的代码输出什么呢?

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan int)
    go fmt.Println(<-ch1)
    ch1 <- 5
    time.Sleep(1 * time.Second)
}

以上代码输出什么?(单选)

  • A:5
  • B:不能编译
  • C:运行时死锁

如果你耐心看了上面的讲解,可以很容易知道正确答案是:C;

因为:

在上方创建Goroutine进行调用时,实际上是显式传参!

所以,上方的代码其实类似于:

func main() {
    ch1 := make(chan int)
    x := <-ch1
    go fmt.Println(x)
    ch1 <- 5
    time.Sleep(1 * time.Second)
}

此时x := <-ch1会阻塞main函数,而ch1 <- 5也是在main函数中调用的,所以会被阻塞,最终造成死锁!


附录

Goroutine题目来源:

源代码:



本文作者:Jasonkay
本文链接:https://jasonkayzk.github.io/2021/01/21/Go创建Goroutine时显式调用时的坑/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可