【问题标题】:Golang goroutine cannot use function return value(s)Golang goroutine 不能使用函数返回值
【发布时间】:2016-09-08 17:29:01
【问题描述】:

我希望我可以用 golang 做类似下面的代码:

package main

import (
    "fmt"
    "time"
)

func getA() (int) {

    fmt.Println("getA: Calculating...")
    time.Sleep(300 * time.Millisecond)

    fmt.Println("getA: Done!")
    return 100
}

func getB() (int) {

    fmt.Println("getB: Calculating...")
    time.Sleep(400 * time.Millisecond)

    fmt.Println("getB: Done!")
    return 200
}

func main() {

    A := go getA()
    B := go getB()

    C := A + B // waits till getA() and getB() return
    fmt.Println("Result:", C)
    fmt.Println("All done!")
}

更具体地说,我希望 Go 能够在后台处理并发。

这可能有点跑题了,但我很好奇人们对拥有这种隐式并发支持的看法。值得为此付出一些努力吗?以及潜在的困难和缺点是什么?


编辑:

需要明确的是,问题不在于“Go 现在在做什么?”,甚至不在于“它是如何实现的?”虽然我很欣赏@icza 发布的关于我们现在应该对 Go 有什么期望的帖子。问题是为什么它不能或不能返回值,这样做的潜在并发症是什么?

回到我的简单例子:

   A := go getA()
   B := go getB()

   C := A + B // waits till getA() and getB() return

至少从语法的角度来看,我没有看到任何关于变量范围的问题。 ABC 的范围由它们所在的块明确定义(在我的示例中,范围是 main() 函数)。然而,一个可能更合理的问题是这些变量(这里是AB)是否“准备好”读取?当然,在getA()getB() 完成之前,它们不应该准备好并且可以访问。事实上,这就是我所要求的:编译器可以在幕后实现所有的花里胡哨,以确保执行将被阻止,直到 AB 准备好使用(而不是强迫程序员使用通道显式实现这些等待和哨声)。

这可以使并发编程更加简单,特别是对于计算任务相互独立的情况。如果确实需要,这些通道仍可用于显式通信和同步。

【问题讨论】:

  • 这幕后的东西太抽象了。 Goroutines 是不同的运行进程或线程,据我所知,在任何语言中都没有办法(或至少不推荐)在不显式调用 join 并检索值的情况下“加入”两个进程或线程的结果。跨度>

标签: go concurrency goroutine


【解决方案1】:

但这是非常容易和惯用的。语言提供手段:Channel types

只需将通道传递给函数,并让函数在通道上发送结果而不是返回结果。通道可以安全地并发使用。

在任何给定时间只有一个 goroutine 可以访问该值。设计上不会发生数据竞争。

更多信息,请查看问题:If I am using channels properly should I need to use mutexes?

示例解决方案:

func getA(ch chan int) {
    fmt.Println("getA: Calculating...")
    time.Sleep(300 * time.Millisecond)

    fmt.Println("getA: Done!")
    ch <- 100
}

func getB(ch chan int) {
    fmt.Println("getB: Calculating...")
    time.Sleep(400 * time.Millisecond)

    fmt.Println("getB: Done!")
    ch <- 200
}

func main() {
    cha := make(chan int)
    chb := make(chan int)

    go getA(cha)
    go getB(chb)

    C := <-cha + <-chb // waits till getA() and getB() return
    fmt.Println("Result:", C)
    fmt.Println("All done!")
}

输出(在Go Playground上试试):

getB: Calculating...
getA: Calculating...
getA: Done!
getB: Done!
Result: 300
All done!

注意:

上面的例子也可以用单通道实现:

func main() {
    ch := make(chan int)

    go getA(ch)
    go getB(ch)

    C := <-ch + <-ch // waits till getA() and getB() return
    fmt.Println("Result:", C)
    fmt.Println("All done!")
}

输出是一样的。在Go Playground 上试试这个变体。


编辑:

Go spec states 表示此类函数的返回值被丢弃。更多信息:What happens to return value from goroutine

你的建议是多处伤口流血。 Go 中的每个变量都有一个scope(可以在其中引用它们)。访问变量不会阻塞。语句或运算符的执行可能会阻塞(例如Receive operatorSend statement)。

您的建议:

go A := getA()
go B := getB()

C := A + B // waits till getA() and getB() return

AB的范围是什么? 2 个合理的答案是 a) 从 go 声明或 go 声明之后。无论哪种方式,我们都应该能够在 go 语句之后访问它。在go 语句之后,它们将在范围内,读/写它们的值不应阻塞。

但是如果C := A + B 不会阻塞(因为它只是在读取一个变量),那么要么

a) 在这一行 AB 应该已经被填充,这意味着 go 语句需要等待 getA() 完成(但它会破坏go 语句的目的)

b) 否则我们将需要一些外部代码来同步,但我们又没有任何收获(与使用通道的解决方案相比,它只会变得更糟)。

Do not communicate by sharing memory; instead, share memory by communicating.

通过使用渠道,可以清楚地知道什么(可能)阻止,什么不阻止。很明显,当来自通道的接收完成时,goroutine 就完成了。它为我们提供了在我们想要的时候执行接收的方法(在需要它的值并且我们愿意等待它的时候),以及检查值是否准备好而没有阻塞的方法(逗号-ok 成语)。

【讨论】:

  • 谢谢@icza。是的,这是可行的,这就是我们现在正在做的事情。但除了方便之外,它更像是传统方式的方式。如果您已经有一个实现返回值的函数,则必须更改该函数,或者将其包装在另一个函数中以访问输出。
  • 此外,这在语法上(甚至可能在语义上)与通用函数声明语法及其在 Go 语言中的含义不一致。
  • @user3324751 添加了一些修改,回答了您的问题?
猜你喜欢
  • 2014-01-22
  • 1970-01-01
  • 2016-08-04
  • 2018-06-25
  • 1970-01-01
  • 2015-12-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多