登录
首页 >  Golang >  Go问答

Codewalk之Golang并发代码回顾

来源:stackoverflow

时间:2024-03-16 20:03:33 241浏览 收藏

**摘要** 本文审阅了 Go 语言中并发代码的最佳实践,以了解 Golang codewalks 中的示例代码。该代码展示了无 goroutine 清理逻辑、作者没有创建写入通道以及异步写入通道等问题。作者认为,通道的清理和写入必须同步,并建议作者负责关闭通道。尽管示例代码旨在用于教学目的,但它违背了一些最佳实践,例如:没有清理 goroutine、通道创建和写入逻辑未由单个实体控制,以及异步写入通道。

问题内容

我正在尝试了解 golang 并发的最佳实践。我读了 o'reilly 的关于 go 并发的书,然后又回到了 golang codewalks,特别是这个例子:

https://golang.org/doc/codewalk/sharemem/

这是我希望与您一起回顾的代码,以便更多地了解 go。我的第一印象是这段代码破坏了一些最佳实践。这当然是我(非常)没有经验的观点,我想讨论并获得对这个过程的一些见解。这不是谁对谁错的问题,请保持友善,我只是想分享我的观点并获得一些反馈。也许这次讨论会帮助其他人了解我为什么错了并教给他们一些东西。

我完全意识到这段代码的目的是为了教导初学者,而不是成为完美的代码。

问题 1 - 无 goroutine 清理逻辑

func main() {
    // create our input and output channels.
    pending, complete := make(chan *resource), make(chan *resource)

    // launch the statemonitor.
    status := statemonitor(statusinterval)

    // launch some poller goroutines.
    for i := 0; i < numpollers; i++ {
        go poller(pending, complete, status)
    }

    // send some resources to the pending queue.
    go func() {
        for _, url := range urls {
            pending <- &resource{url: url}
        }
    }()

    for r := range complete {
        go r.sleep(pending)
    }
}

main 方法无法清理 goroutines,这意味着如果这是库的一部分,它们就会被泄漏。

问题 2 - 作家没有催生频道

我读到,作为最佳实践,创建写入清理通道的逻辑应该由单个实体控制(或实体组)。这背后的原因是写入者在写入封闭通道时会感到恐慌。因此,编写者最好创建通道、写入通道并控制何时关闭通道。如果有多个writer,可以使用waitgroup进行同步。

func statemonitor(updateinterval time.duration) chan<- state {
    updates := make(chan state)
    urlstatus := make(map[string]string)
    ticker := time.newticker(updateinterval)
    go func() {
        for {
            select {
            case <-ticker.c:
                logstate(urlstatus)
            case s := <-updates:
                urlstatus[s.url] = s.status
            }
        }
    }()
    return updates
}

此函数不应该负责创建更新通道,因为它是通道的读取器,而不是写入器。该通道的编写者应该创建它并将其传递给该函数。基本上是对函数说“我将通过此通道向您传递更新”。但相反,这个函数正在创建一个通道,并且不清楚谁负责清理它。

问题 3 - 异步写入通道

这个函数:

func (r *resource) sleep(done chan<- *resource) {
    time.sleep(pollinterval + errtimeout*time.duration(r.errcount))
    done <- r
}

此处引用:

for r := range complete {
    go r.Sleep(pending)
}

这似乎是一个糟糕的主意。当这个通道关闭时,我们将有一个 goroutine 休眠在我们无法到达的地方,等待写入该通道。假设这个 goroutine 休眠了 1 小时,当它醒来时,它将尝试写入在清理过程中关闭的通道。这是为什么频道的作者应该负责清理过程的另一个例子。在这里,我们有一位完全自由的作家,他不知道频道何时关闭。

如果我错过了该代码中的任何问题(与并发相关),请列出它们。这不一定是一个客观问题,如果您出于任何原因以不同的方式设计代码,我也有兴趣了解它。

这段代码的最大教训

对我来说,从审查这段代码中得到的最大教训是通道的清理和对通道的写入必须同步。它们必须处于相同的 for{} 状态,或者至少以某种方式进行通信(可能通过其他通道或原语)以避免写入关闭的通道。


正确答案


  1. 它是 main 方法,因此不需要清理。当main返回时,程序退出。如果这不是 main,那么你就是对的。

  2. 不存在适合所有用例的最佳实践。您在此处显示的代码是一种非常常见的模式。该函数创建一个 goroutine,并返回一个通道,以便其他人可以与该 goroutine 进行通信。没有规则规定如何创建渠道。但没有办法终止该 goroutine。这种模式非常适合的一个用例是从 数据库。该通道允许从读取的数据流式传输 数据库。在这种情况下,通常有其他方法可以终止 goroutine 不过,就像传递上下文一样。

  3. 同样,对于如何创建/关闭通道没有硬性规则。通道可以保持打开状态,当不再使用时它将被垃圾收集。如果用例需要的话,通道可以无限期地保持开放,并且您担心的场景永远不会发生。

  1. 当您询问此代码是否是库的一部分时,是的,在库函数内部不进行清理的情况下生成 goroutine 是很糟糕的做法。如果这些 goroutine 执行库中记录的行为,那么调用者不知道该行为何时会发生就会出现问题。如果您有任何典型的“即发即忘”行为,则应该由呼叫者选择何时忘记它。例如:
func doafter5minutes(f func()) {
   go func() {
       time.sleep(5 * time.minute)
       f()
       log.println("done!")
   }()
}

有道理吧?当您调用该函数时,它会在 5 分钟后执行某些操作。问题是这个函数很容易被误用,如下所示:

// do the important task every 5 minutes
for {
    doafter5minutes(importanttaskfunction)
}

乍一看,这似乎没问题。我们每 5 分钟就会执行一次重要任务,对吧?事实上,我们很快就会生成许多 goroutine,可能会在它们开始消失之前耗尽所有可用内存。

我们可以实现某种回调或通道来在任务完成时发出信号,但实际上,该函数应该像这样简化:

func doafter5minutes(f func()) {
   time.sleep(5 * time.minute)
   f()
   log.println("done!")
}

现在调用者可以选择如何使用它:

// call synchronously
doafter5minutes(importanttaskfunction)
// fire and forget
go doafter5minutes(importanttaskfunction)
  1. 这个功能可以说也应该改变。正如您所说,作者应该有效地拥有该频道,因为他们应该是关闭该频道的人。事实上,这个通道读取函数坚持创建它读取的通道,实际上迫使自己陷入上面提到的这种糟糕的“即发即忘”模式。请注意该函数如何需要从通道读取,但它还需要在读取之前返回通道。因此,它必须将读取行为放入一个新的、非托管的 goroutine 中,以允许自己立即返回通道。
95578​​4645965

请注意,该功能现在更简单、更灵活且同步。先前版本真正完成的唯一一件事是,它(大部分)保证 statemonitor 的每个实例都将拥有一个属于自己的通道,并且不会出现多个监视器在同一通道上竞争读取的情况。虽然这可能可以帮助您避免某些类型的错误,但它也使该功能的灵活性大大降低,并且更有可能出现资源泄漏。

  1. 我不确定我是否真的理解这个例子,但是关闭通道的黄金法则是作者应该始终负责关闭通道。请记住这条规则,并注意有关此代码的几点:
  • sleep 方法写入 r
  • sleep 方法是并发执行的,没有方法跟踪正在运行的实例数量、它们所处的状态等。

仅基于这些点,我们可以说程序中可能没有任何地方可以安全地关闭 r,因为似乎没有办法知道它是否会被使用再次。

理论要掌握,实操不能落!以上关于《Codewalk之Golang并发代码回顾》的详细介绍,大家都掌握了吧!如果想要继续提升自己的能力,那么就来关注golang学习网公众号吧!

声明:本文转载于:stackoverflow 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>