Golang goroutine 无法使用函数返回值

9

我希望我能使用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 并检索值的情况下“连接”两个进程或线程的结果。 - Pandemonium
1个回答

18

但这很容易且符合惯用法。该语言提供了手段:通道类型

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

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

更多信息,请查阅问题:如果我正确使用通道,是否需要使用互斥锁?

示例解决方案:

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规范说明此类函数的返回值将被丢弃。更多信息请参见:什么会发生在goroutine的返回值上

你提出的方案从多个方面都存在问题。在Go中,每个变量都有一个作用域(可以在其中引用它们)。访问变量不会阻塞。语句或运算符的执行可能会阻塞(例如接收运算符发送语句)。

你的建议:

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

C := A + B // waits till getA() and getB() return
AB的作用域是什么?两个合理的答案是:a)从go语句或go语句之后。无论哪种方式,在go语句之后我们都应该能够访问它们。在go语句之后,它们将处于作用域内,读/写其值不应该被阻塞。

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

a)在这一行AB应该已经被填充,这意味着go语句需要等待getA()完成(但这样做就失去了go语句的目的)。

b)否则,我们需要一些外部代码来同步,但这样比使用通道的解决方案更糟糕。

不要通过共享内存来通信;相反,通过通信来共享内存。

通过使用通道,清楚地了解什么(可能)会被阻塞,什么不会被阻塞。当从通道接收完成时,协程就完成了。它为我们提供了执行接收操作的方式(在需要其值且我们愿意等待时),以及检查该值是否已准备好而不会被阻止的方法(逗号-ok惯用法)。


感谢@icza。是的,这是可行的,也是我们现在正在做的事情。但除了方便之外,它更像是传统方式的实现方式。如果您已经有一个实现返回值的函数,您要么必须更改该函数,要么将其包装在另一个函数中以访问输出。 - hagh
此外,这在语法上(甚至可能是语义上)与Go语言中一般函数声明语法及其含义不一致。 - hagh
@user3324751 添加了一些编辑,回答了你的问题吗? - icza

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接