【问题标题】:Golang Channels - Help me get my head around thisGolang 频道 - 帮助我解决这个问题
【发布时间】:2021-03-07 21:22:12
【问题描述】:

试图让 2 个 Goroutine 在我的项目中正常运行。尽管这一切都在起作用,但我 100% 确信它做得很差......甚至可能是错误的......ly(这甚至是一个词吗?)。

无论如何,这个项目的基本概念是在预先选择的时间内运行一些重复的工作,同时允许在时间结束之前中止运行。

这是我到目前为止的混乱(我只包括了重要的部分,以确保每个人的理智......并隐藏我可怕的编码):

两个 Goroutine:

  1. BenchTimer() 是一个简单的运行时倒计时计时器,它会在设定的时间内执行一些重复工作。
  2. AbortTest() 是一个键盘侦听器,用于从键盘捕获“ESC”(或我想要的任何其他)按键以充当“用户中止”。

每个 Goroutine 在成功运行后(即BenchTimer() 完成倒计时 AbortTest() 捕获中止按键),通过公共通道testAction 发送消息。我使用一个频道,因为这是 OR 有点像(即您不能同时完成倒计时和中止。)。如果BenchTimer() 完成,那么它会沿通道发送“完成”。如果AbortTest()“完成”,它将沿通道发送“中止”。 [到目前为止,这一切似乎都在工作......]

我遇到的下一个问题是如何杀死不是“赢家”的 Goroutine...(即如果 BenchTimer() 正常完成,那么我需要以某种方式杀死 AbortTest()...反之亦然。)经过一堆搜索,我发现不可能在外部杀死一个 Goroutine,但它可以在内部完成......所以我想出了为每个 Goroutine 使用第二个通道来充当一种“终止信号”行:killAbortTestkillBenchTimer

为了将这一切联系在一起,我评估了testAction 频道的结果。因为这个频道会告诉我哪个 Goroutine “赢了”,我可以使用这个知识来发送正确的(即相反的)“kill signal”,让“loser” Goroutine 自行终止。

注意:... 仅表示其他代码存在,但由于本文不需要而被删除。

func main() {
...

testAction := make(chan string)             // Action Result (Timer "Complete" or User "Abort")
killAbortTest := make(chan bool)            // Kill AbortTest() Goroutine when BenchTimer() completes.
killBenchTimer := make(chan bool)           // Kill BenchTimer() Goroutine when AbortTest() completes.

go BenchTimer(testAction, killBenchTimer)   // Run BenchTimer() as Goroutine
go AbortTest(testAction, killAbortTest)     // Run AbortTest() as Goroutine

// Program should wait here until it receives something on testAction channel.
actionVal := <-testAction

// Evaluate the testAction to kill the "loser" Goroutine
switch actionVal {
    case "Abort":
        killBenchTimer <- true             // Abort received, signal BenchTimer() Goroutine to Quit
        fmt.Println()
        fmt.Println("Test Aborted")
    case "Complete":
        killAbortTest <- true             // Countdown finished, signal AbortTest() Goroutine to Quit
        fmt.Println()
        fmt.Println("Test Completed")
}
...
}
// AbortTest - Listen for User Abort
func AbortTest(c chan<- string, k <-chan bool) {

if err := keyboard.Open(); err != nil {
    panic(err)
}
defer func() {
    _ = keyboard.Close()
}()
for {
    select {
        case <-k:
            return
        default:
            _, key, err := keyboard.GetKey()  // Poll for keypress
            if err != nil {
                panic(err)
            }
            if key == keyboard.KeyEsc {       // ESC key was pressed
                c <- "Abort"
                return
            }
    }
}
}
// BenchTimer - Countdown Timer for BenchTest
func BenchTimer(c chan<- string, k <-chan bool) {

seconds := 0

switch testTime {
    case "2-minute (fast)":
        seconds = 120
    case "5-minute (short)":
        seconds = 300
    case "10-minute (long)":
        seconds = 600
    case "20-minute (slow)":
        seconds = 1200
}

ticker := time.Tick(time.Second)
for i := seconds; i >= 0; i-- {
    select {
        case <-k:         // Kill Signal Received
            return
        default:
            <-ticker
            ...
    }
}
c <- "Complete"
}

就是这样。我的烂摊子。有很多喜欢的,但这个是我自己的。就像我说的,它现在有点用,但我希望让它变得更好。

我是否只是过度思考了整个过程,让它变得比需要的复杂得多?

任何帮助都会很棒。

【问题讨论】:

  • “错误地”是一个词,是的。 :-) “完成”频道是常见的 Go 习语。我无法评论任何我看不到的代码,但在 done 频道和另一个频道上选择是很常见的。
  • 使用单个 done 通道并在 actionVal := &lt;-testAction 行之后关闭通道。这允许您删除代码以将值发送到 goroutine 特定的 done 通道,并防止 main 在其中一个 goroutine 未在其通道上接收时永远阻塞。
  • 你真的不需要 AbortTest 来监听完成的事件。当 main 退出时它将退出。并将 defer keyboard.Close() 移动到 main 中。如果需要,我不确定是否需要在退出序列时关闭键盘资源。它可能只是无害的。

标签: go goroutine channels


【解决方案1】:

这是我能想到的最简单的例子。我省略了键盘部分,但它基本上是您代码背后的想法:

正如 mh-cbon 所述,这是安全的:

package main

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

var wg sync.WaitGroup

func main() {
    rand.Seed(time.Now().UnixNano())
    ctx, cancel := context.WithCancel(context.Background())
    wg.Add(2)
    go DoSomeTask(ctx, cancel)
    go CancelTask(ctx, cancel)
    wg.Wait()
}

func DoSomeTask(ctx context.Context, cancel func()) {
    defer wg.Done()
    defer cancel() // force cancellation
    for i := 1; i < 10; i++ {
        select {
        case <-ctx.Done():
            fmt.Println("context cancelled")
            return
        case <-time.After(time.Second):
        }
        fmt.Println("Done something!", i)
    }
}

func CancelTask(ctx context.Context, cancel func()) {
    defer wg.Done()
    defer cancel() // force cancellation
    duration := time.Second * time.Duration((rand.Intn(20-1) + 1))
    fmt.Println("Will cancel in ", duration, " seconds!")
    select {
    case <-ctx.Done():
        fmt.Println("context cancelled")
    case <-time.After(duration):
    }
}

【讨论】:

  • 您的解决方案有问题,因为两个例程正在关闭通道,因为一个通道将在已关闭的通道上写入,并且根据设计,其中一个例程将始终悬空。 (两次写入,一次读取)
  • @mh-cbon 是对的。此外,在您关闭通道之前,主 goroutine 也有可能退出。此外,当您发送到关闭的频道时,可能会出现panic 的情况。
  • @mh-cbon 现在好点了吗?如果我在主程序中推迟关闭通道并将其从异步函数中删除? (我已编辑)
  • 仍然有可能在关闭的通道上发生写入。鉴于该代码,它不会总是触发,但在实际情况下,代码经常更改是错误的来源。有两个例程在不同步关闭的通道上写入是一种众所周知的反模式。必须有一个条件并且只有一个条件才能以同步方式触发通道的关闭。
  • 还请注意,可以乘坐等待组并使用ctx.Done 作为main 中的等待条件
猜你喜欢
  • 1970-01-01
  • 2020-08-06
  • 1970-01-01
  • 2019-05-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-08-12
  • 1970-01-01
相关资源
最近更新 更多