使用Go协程遍历通道的方法

4

我已经在Golang领域工作了很长时间。虽然我知道解决方案,但我仍然遇到这个问题,并且从未找出为什么会发生。

例如,如果我的入站和出站通道存在以下管道情况:

package main

import (
    "fmt"
)

func main() {
    for n := range sq(sq(gen(3, 4))) {
        fmt.Println(n)
    }
    fmt.Println("Process completed")
}

func gen(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}

func sq(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * n
        }
        close(out)
    }()
    return out
}

这不会导致死锁情况。但是,如果我将出站代码中的go例程移除如下:

func sq(in <-chan int) <-chan int {
    out := make(chan int)
    for n := range in {
        out <- n * n
    }
    close(out)
    return out
}

我收到了一个死锁错误。为什么使用range循环通道而不使用go例程会导致死锁。

5个回答

3

这种情况是由于sq函数的输出通道没有缓冲造成的。因此,sq会等待直到下一个函数从输出中读取,但如果sq不是异步的,那么这将不会发生(Playground链接):

package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup

func main() {
    numsCh := gen(3, 4)
    sqCh := sq(numsCh) // if there is no sq in body - we are locked here until input channel will be closed
    result := sq(sqCh) // but if output channel is not buffered, so `sq` is locked, until next function will read from output channel

    for n := range result {
        fmt.Println(n)
    }
    fmt.Println("Process completed")
}

func gen(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}

func sq(in <-chan int) <-chan int {
    out := make(chan int, 100)
    for n := range in {
        out <- n * n
    }
    close(out)
    return out
}

1
我从 sq 中删除了 goroutine,这正是被要求的(您的答案仍然解决了问题)。如果我改变了您的意图,请重新编辑。 - Motti
这并没有回答我的问题。我的问题是为什么我们在将入站通道包装在go例程中时不会出现死锁。这与缓冲区无关。在带缓冲的通道中,当接收器不可用时,通道将数据添加到缓冲区中。 - Himanshu
这是关于同步和异步程序的不同流程。在使用"go"关键字的情况下,程序可以将程序执行的控制权转移到另一个goroutine,因此缓冲区可以被释放。同步: 1)写入通道 2)写入通道-阻塞 而异步使用可以像这样: 异步: 1)写入通道 2)从通道读取 3)写入通道 4)从通道读取 - iHelos

2

您的函数创建一个通道,向其写入数据,然后将其返回。写入操作将会阻塞,直到有人读取相应的值,但是这是不可能的,因为在此函数之外没有人拥有该通道。

func sq(in <-chan int) <-chan int {
    // Nobody else has this channel yet...
    out := make(chan int)
    for n := range in {
        // ...but this line will block until somebody reads the value...
        out <- n * n
    }
    close(out)
    // ...and nobody else can possibly read it until after this return.
    return out
}

如果你将循环放在一个goroutine中,那么循环和sq函数都可以继续执行;即使循环被阻塞,return out语句仍然可以执行,最终你将能够将读取器连接到通道上。

(在goroutine之外循环通道本质上并没有什么坏处;你的main函数可以正确而无害地这样做。)


谢谢,但我仍然不明白。我知道死锁的原因。但使用go routine将让我遍历通道。现在我又想到了一个问题,为什么它在主函数中能够工作。如果您提供有关操作和禁忌背后原因的参考示例,将不胜感激。 - Himanshu
你不会因为goroutine中的写入而发生死锁。那个goroutine被阻塞,外部函数返回通道,调用该函数的程序开始从通道读取,然后内部goroutine就能够在通道上发送数据了。 - Adrian
@Adrian,谢谢你的评论,但这只是我们的假设。文档中没有提到过。此外,如果我知道它将如何帮助我或任何人了解通道如何等待,即使没有缓冲区或没有值从另一端传来,我已经进行了大量实验。例如,请看这个链接https://play.golang.org/p/owLc5QoAROa。 - Himanshu
相关事实已经明确提到,即通道操作可以阻塞,而goroutine则同时运行。您的具体示例仅是这些基本原理的应用。 - Adrian

0

这段代码有点复杂,我们来简化一下

第一个等式不会出现死锁

func main() {
    send := make(chan int)
    receive := make(chan int)
    go func() {
        send<-3
        send<-4
        close(send)
    }()
    go func() {
        receive<- <-send
        receive<- <-send
        close(receive)
    }()
    for v := range receive{
        fmt.Println(v)

    }
}

第二个等式,去掉“go”会出现死锁

func main() {
    send := make(chan int)
    receive := make(chan int)
    go func() {
        send<-3
        send<-4
        close(send)
    }()
    receive<- <-send
    receive<- <-send
    close(receive)
    for v := range receive{
        fmt.Println(v)

    }
}

让我们再次简化第二段代码

func main() {
    ch := make(chan int)
    ch <- 3
    ch <- 4
    close(ch)
    for v := range ch{
        fmt.Println(v)

    }
}

死锁的原因是主 goroutine 中没有等待缓冲通道。

两个解决方案

// add more cap then "channel<-" time
func main() {
    ch := make(chan int,2)
    ch <- 3
    ch <- 4
    close(ch)
    for v := range ch{
        fmt.Println(v)

    }
}

//async "<-channel"
func main() {
    ch := make(chan int)
    go func() {
        for v := range ch {
            fmt.Println(v)
        }
    }()
    ch <- 3
    ch <- 4
    close(ch)
}

0

我的理解是,当主线程被阻塞等待chan的写入或读取时,Go会检测是否有其他Go协程正在运行。如果没有其他Go协程在运行,它将出现“fatal error: all goroutines are asleep - deadlock!” 我通过使用以下简单案例进行了测试

func main() {
    c := make(chan int)
    go func() {
        time.Sleep(10 * time.Second)
    }()
    c <- 1
}

在10秒后报告死锁错误。


0
死锁的原因是因为主程序正在等待sq返回并完成,但是sq正在等待某个人读取通道,然后它才能继续执行。
我通过删除一层sq调用并将一个句子拆分成两个来简化您的代码:
func main() {
    result := sq(gen(3, 4)) // <-- block here, because sq doesn't return
    for n := range result { 
        fmt.Println(n)
    }
    fmt.Println("Process completed")
}

func gen(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}

func sq(in <-chan int) <-chan int {
    out := make(chan int)
    for n := range in {
        out <- n * n   // <-- block here, because no one is reading from the chan
    }
    close(out)
    return out
}

sq 方法中,如果你将代码放在 goroutine 中,则 sq 会返回并且主函数不会被阻塞,而是消耗结果队列,goroutine 将继续执行,从而不再发生阻塞。
func main() {
    result := sq(gen(3, 4)) // will not blcok here, because the sq just start a goroutine and return
    for n := range result {
        fmt.Println(n)
    }
    fmt.Println("Process completed")
}

func gen(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}

func sq(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * n // will not block here, because main will continue and read the out chan
        }
        close(out)
    }()
    return out
}

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