【问题标题】:Infinite loop to be killed when channel receives new value当通道接收到新值时无限循环被终止
【发布时间】:2020-09-03 13:35:52
【问题描述】:

我有 2 个模块(模块 1 和模块 2),我在模块 1 中生成一些随机数,并通过 main 中定义的通道将它们传递给模块 2。当值落在模块 2 中时,我希望它在循环中打印,直到新值再次从模块 1 到达。这是代码。

main.go文件内容

package main

import (
    "time"
    "example/module1"
    "example/module2"
)

func main() {
    ints := make(chan []int, 1)
    for {
        select {
        case <- time.After(5 * time.Second):
            go module1.GenerateRandint(ints)
        case  <- module2.Done:
            go module2.Start(ints)
        }
    }
}

module1的内容如下。

package module1

import (
    "math/rand"
    "time"
)

func GenerateRandint(a chan []int){
    var rint []int
    for  i:=0;i<1;i++ {
        rand.Seed(time.Now().UnixNano())
        rint = append(rint,  rand.Int())
    }
    a <- rint
}

module2 是:

package module2

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

var Done = make(chan struct{})
var quit = make(chan struct{})

type localData struct {
    nums []int
    Lock sync.RWMutex
}

var store localData

func init() {
    go func() {
        Done <- struct{}{}
        quit <- struct{}{}
    }()
    go printer(0, quit)
}

func printer(id int, quit chan struct{}) {

    for {
        select {
        case <-quit:
            return
        default:
            store.Lock.RLock()
            fmt.Println("ID:", id, "====", store.nums)
            time.Sleep(500 * time.Millisecond)
            store.Lock.RUnlock()

        }
    }
}

func Start(a chan []int) {
    rand.Seed(time.Now().UnixNano())
    d := <-a
    fmt.Println("RECEIVED DATA", d)
    store.Lock.Lock()
    go func() {
        quit <- struct{}{}
    }()
    store.nums = d
    store.Lock.Unlock()
    go printer(rand.Int(), quit)
    Done <- struct{}{}

}

在最终输出中,我没有看到printer 函数连续打印切片。它打印一次。我在下面粘贴一个示例输出。

ID: 0 ==== []
RECEIVED DATA [5183630848712612481]
ID: 2057228961822266542 ==== [5183630848712612481]
RECEIVED DATA [7203870927705193095]
RECEIVED DATA [1478549829208931483]
ID: 2311909806311895805 ==== [1478549829208931483]
RECEIVED DATA [3000658591728341557]

理想的输出应该是:

ID: 0 ==== []
RECEIVED DATA [5183630848712612481]
ID: 2057228961822266542 ==== [5183630848712612481]
ID: 2057228961822266542 ==== [5183630848712612481]
ID: 2057228961822266542 ==== [5183630848712612481]
ID: 2057228961822266542 ==== [5183630848712612481]
ID: 2057228961822266542 ==== [5183630848712612481]
RECEIVED DATA [7203870927705193095]
ID: 3052382001843759201 ==== [7203870927705193095]
ID: 3052382001843759201 ==== [7203870927705193095]
ID: 3052382001843759201 ==== [7203870927705193095]
ID: 3052382001843759201 ==== [7203870927705193095]
ID: 3052382001843759201 ==== [7203870927705193095]
RECEIVED DATA [1478549829208931483]
ID: 3850200927174591249 ==== [1478549829208931483]
ID: 3850200927174591249 ==== [1478549829208931483]
ID: 3850200927174591249 ==== [1478549829208931483]
ID: 3850200927174591249 ==== [1478549829208931483]
ID: 3850200927174591249 ==== [1478549829208931483]
RECEIVED DATA [3000658591728341557]

【问题讨论】:

    标签: go channel


    【解决方案1】:

    我假设您在问关于在init() 函数中调用go printer(0, quit) 的问题。它只打印整个列表一次的原因是:

    在 init() 函数中,您尝试将数据发送到 Donequit 通道。现在,它们显然会阻塞,直到接收器准备好。加载模块后,接收器尚未准备好,因此您将转到打印机方法中的default 案例。因此,它打印该语句一次并休眠 500 毫秒。在此期间,main() 启动并为Done 通道创建接收器。这会解除对该通道的写入阻塞,随后也会写入quit 通道。现在,由于quit 也有数据,printer 方法中的 select 语句直接返回,不再打印。

    【讨论】:

    • 感谢您的回复。为简洁起见,我将保持非常简短和直接。第一个假设是错误的。最终目标是获得我的问题的“预期输出”区域中提到的数据。有 2 件事。我不打印机连续打印默认部分。它只打印一次。第二个问题是,如果您注意到我粘贴的输出,它会跳过通过通道接收的一些值并且根本不打印它。我也想知道那里发生了什么。我想知道这是否是实现这一目标的惯用方式。再次感谢。
    【解决方案2】:

    你不能杀死一个 goroutine:没有 killmurder 函数。即使有,也没有 goroutine 的 name —— 没有用户可访问的线程 ID,实际上。因此,杀死一个特定 goroutine 的唯一方法是杀死 所有 goroutine,方法是用核爆炸摧毁整个世界,例如,通过os.Exit 或从@987654329 返回@。

    一个goroutine可以通过从它的初始函数返回或调用runtime.Goexit来优雅地自行退出。1所以你需要做的是安排让您的循环自行停止:

    我有 2 个模块(模块 1 和模块 2),我在模块 1 中生成一些随机数,并通过 main 中定义的通道将它们传递给模块 2。当值到达模块 2 时,我希望它循环打印,直到新值再次从模块 1 到达。

    您的实际代码结构看起来很奇怪。例如,Go 中“完成”某事的信号通常是通过一个简单关闭的通道来处理,而不是在其上发送数据:

    doneChan := make(chan struct{})
    // ...
    close(doneChan) // tell everyone we are done
    

    这特别好用,因为关闭事件实际上是向多个频道阅读器广播:

    doneChan := make(chan struct{})
    go f1(doneChan)
    go f2(doneChan)
    // ...
    go fN(doneChan)
    // ...
    close(doneChan) // tells f1, f2, ..., fN we are done
    

    例如,您的main 可以使用这样的东西来告诉两个模块退出。

    除此之外,在我看来,逻辑安排应该是这样的 (see complete Playground version)。我们从一个只有一个职责的生成函数开始:生成一段随机数并发送它,当被告知退出时退出:

    // it's not clear why `a` is `[]int` when there is only one
    func GenerateRandints(a chan<- []int, done <-chan struct{}) {
        rand.Seed(time.Now().UnixNano())
        for {
            rint := []int{rand.Int()}
            select {
            case a <- rint:
            case <-done:
                return
            }
        }
    }
    

    然后我们有一个函数在循环中使用这些,直到被告知退出。它还通过sync.WaitGroup 指示它何时实际完成,以便它的调用者可以判断它何时真正完成。请注意,我删除了所有的全局变量和花哨的init 函数和互斥体。据推测,在实际代码中,这些是有目的的,您可能需要将它们中的一部分或全部放回去。

    (您的示例生成了一个 ID 为 0 的 Printer 函数,以及另一个具有随机数 ID 的函数。我保留了 fmt.Println 并在此处简单地硬编码了零。)

    func UseRandints(wg *sync.WaitGroup, a <-chan []int, done <-chan struct{}) {
        defer wg.Done()
        var r []int
        for {
            select {
            case r = <-a:
            case <-done:
                return
            default:
                if r != nil {
                    fmt.Println("ID:", 0, "====", r)
                }
                time.Sleep(500 * time.Millisecond)
            }
        }
    }
    

    最后,我们需要main 来推动行动。我在这里编了一个;你的真人无疑会有所不同:

    func main() {
        ints := make(chan []int)
        done := make(chan struct{})
        go GenerateRandints(ints, done)
        var wg sync.WaitGroup
        wg.Add(1)
        go UseRandints(&wg, ints, done)
    
        // wait 5 seconds then tell everyone to quit
        <-time.After(5 * time.Second)
        close(done)
        // wait for UseRandints() to signal that it's done
        wg.Wait()
    }
    

    1请注意,调用runtime.Goexit 和简单地返回之间存在一些细微差别。把它变成complete program 显示效果(虽然从技术上讲,这里有一场比赛):

    func main() {
        go f1()
        runtime.Goexit()
    }
    
    func f1() {
        time.Sleep(time.Second)
        fmt.Println("f1 finished")
        runtime.Goexit()
    }
    

    当这个程序在 Go 操场上运行时,它会等待一秒钟,打印 f1 finished,然后以运行时异常 (fatal error: no goroutines (main called runtime.Goexit) - deadlock!) 终止,因为所有 goroutine 都已退出。

    Commenting out the runtime.Goexit() call in main produces a program that behaves differently:这个程序立即退出,不打印f1 finished,也没有任何运行时错误。这是因为调用 runtime.Goexit() 意味着我们永远不会经历从 main 返回的“杀死所有其他 goroutines”的代码路径。

    【讨论】:

    • 感谢您的回复。杀死,我的意思是用某种方法杀死前面的循环。我通过修改 module2 的 init 函数来解决它。我从 module2 的 init 启动了打印机功能,并不断循环存储变量。将在此处为任何有兴趣改进/弄清楚上述问题的答案的人分享输出。谢谢
    • 你必须告诉 Go 例程“请退出”,然后它必须服从。只要您自己编写 Go 例程,您就可以编写代码来执行此操作。如何(以及何时以及为什么)告诉它退出取决于您,但“关闭done 频道”是常见的 Go 习语。
    【解决方案3】:

    我通过修改main和module2解决了这个问题

    main.go

    var ints = make(chan map[string]int)
    
    func init() {
        go module2.Start(ints)
    }
    
    func main() {
        for {
            select {
            case <-time.After(5 * time.Second):
                go module1.GenerateRandint(ints)
            }
        }
    }
    

    Module2 文件已更改为如下所示。

    type Myint int
    
    var Data = make(chan map[string]Myint)
    var fetch = make(chan map[string]Myint)
    
    type localData struct {
        nums map[string]int
        Lock sync.RWMutex
    }
    
    var store localData
    
    func init() {
        go loopover()
    }
    
    func loopover() {
        for i := 0; i < 2; i++ {
            go func() {
                for {
                    v, k := <-fetch
                    if !k {
                        return
                    }
                    fmt.Println(v)
    
                }
            }()
        }
        for {
    
            for i, v := range store.nums {
                tmp := make(map[string]Myint)
                tmp[i] = Myint(v)
                time.Sleep(1 * time.Second)
                fetch <- tmp
            }
        }
    }
    
    func Start(a chan map[string]int) {
        for {
            select {
            case d := <-a:
                fmt.Println("RECEIVED", d)
                store.Lock.Lock()
                store.nums = d
                store.Lock.Unlock()
            }
        }
    }
    

    【讨论】:

    • loopover 上半部分的循环非常奇怪:它衍生出两个独立的 goroutine,它们反复尝试从 fetch 通道读取数据。这些读取直到通道关闭(!k 测试)......但它从未关闭:任何地方都没有close(fetch)。这在 Go 中并没有错,你可以永远留下一个 goroutine,但是分离其中的 两个 然后让它们竞争读取和打印,并检查封闭性似乎很奇怪没有人关门。
    • 同时,下半场还有数据竞赛。内部for 循环在store.nums 上运行(在进入循环之前对其进行一次评估),但不会锁定函数Start,该函数在锁定下写入store.nums。您可能希望获得锁定下的 store.nums 值,或者避免这种通过共享内存进行通信(store.nums 变量)方法。
    • 最后,Start 中的 select 现在毫无意义,因为只有一件事要做。您不妨在这里使用for d := range a。也没有人做过close(a),所以这个循环也是无限的,但这是一种紧凑且类似于 Go 的编写方式。您可以在两个匿名 goroutine 中使用相同的习惯用法。
    猜你喜欢
    • 2021-06-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-02-22
    • 1970-01-01
    相关资源
    最近更新 更多