为了账号安全,请及时绑定邮箱和手机立即绑定

Go 频道不适用于生产者/消费者样本

Go 频道不适用于生产者/消费者样本

Go
慕斯王 2023-06-01 18:18:21
我刚刚在 Mac 上安装了 Go,这是代码package mainimport (    "fmt"    "time")func Product(ch chan<- int) {    for i := 0; i < 100; i++ {        fmt.Println("Product:", i)        ch <- i    }}func Consumer(ch <-chan int) {    for i := 0; i < 100; i++ {        a := <-ch        fmt.Println("Consmuer:", a)    }}func main() {    ch := make(chan int, 1)    go Product(ch)    go Consumer(ch)    time.Sleep(500)}我“去运行 producer_consumer.go”,屏幕上没有输出,然后退出。我的程序有问题吗?如何解决?
查看完整描述

3 回答

?
开心每一天1111

TA贡献1836条经验 获得超13个赞

这是一个相当冗长的答案,但简单地说:

  • 过去time.Sleep一直等到希望其他例程完成他们的工作是不好的。

  • 除了他们通过渠道交换的类型之外,消费者和生产者不应该知道彼此的任何信息。您的代码依赖于消费者和生产者都知道将传递多少整数。不是一个现实的场景

  • 通道可以迭代(将它们视为线程安全的共享切片)

  • 通道应该关闭

在这个相当冗长的答案的底部,我试图解释一些基本概念和最佳实践(好吧,更好的实践),你会发现你的代码被重写以工作显示所有值而不依赖于time.Sleep. 我没有测试该代码,但应该没问题


是的,这里有几个问题。就像一个项目符号列表:

  1. 你的通道被缓冲到 1,这很好,但没有必要

  2. 你的频道从未关闭

  3. 您正在等待 500ns,然后退出,无论例程是否已完成,甚至是否已开始处理该问题。

  4. 没有对例程的集中控制,一旦你启动它们,你就没有控制权。如果您按下 ctrl+c,您可能希望在编写处理重要数据的代码时取消例程。检查信号处理和上下文

通道缓冲器

鉴于您已经知道要将多少价值推送到您的频道,为什么不简单地创造呢ch := make(chan int, 100)?这样,无论消费者做什么,您的发布者都可以继续将消息推送到频道。

您不需要这样做,但是根据您要执行的操作向您的频道添加一个合理的缓冲区绝对值得一试。不过目前,这两个例程都在使用fmt.Println& co,这将成为任何一种方式的瓶颈。打印到 STDOUT 是线程安全的,并且是缓冲的。这意味着每次调用fmt.Print*都会获得一个锁,以避免来自两个例程的文本被合并。

关闭通道

您可以简单地将所有值推送到您的频道,然后关闭它。然而,这是一种糟糕的形式。WRT 通道的经验法则是通道是在同一个例程中创建和关闭的。意思是:你正在主例程中创建通道,那是它应该关闭的地方。

您需要一种机制来同步,或者至少密切关注您的例程是否已完成其工作。这是使用sync包或通过第二个渠道完成的。

// using a done channel

func produce(ch chan<- int) <-chan struct{} {

    done := make(chan struct{})

    go func() {

        for i := 0; i < 100; i++ {

            ch <- i

        }

        // all values have been published

        // close done channel

        close(done)

    }()

    return done

}


func main() {

    ch := make(chan int, 1)

    done := produce(ch)

    go consume(ch)

    <-done // if producer has done its thing

    close(ch) // we can close the channel

}


func consume(ch <-chan int) {

    // we can now simply loop over the channel until it's closed

    for i := range ch {

        fmt.Printf("Consumed %d\n", i)

    }

}

好的,但是在这里您仍然需要等待consume例程完成。


您可能已经注意到,done通道在技术上并没有在创建它的同一个例程中关闭。但是,因为例程被定义为闭包,所以这是一个可以接受的折衷方案。现在让我们看看如何使用等待组:


import (

    "fmt"

    "sync"

)


func product(wg *sync.WaitGroup, ch chan<- int) {

    defer wg.Done() // signal we've done our job

    for i := 0; i < 100; i++ {

        ch <- i

    }

}


func main() {

    ch := make(chan int, 1)

    wg := sync.WaitGroup{}

    wg.Add(1) // I'm adding a routine to the channel

    go produce(&wg, ch)

    wg.Wait() // will return once `produce` has finished

    close(ch)

}

好的,这看起来很有希望,我可以让例程告诉我它们何时完成任务。但是,如果我将消费者和生产者都添加到等待组,我就不能简单地遍历通道。只有当两个例程都调用时,通道才会关闭wg.Done(),但如果消费者卡在一个永远不会关闭的通道上循环,那么我就创建了一个死锁。


解决方案:

此时混合将是最简单的解决方案:将消费者添加到等待组,并使用生产者中的完成通道来获取:


func produce(ch chan<- int) <-chan struct{} {

    done := make(chan struct{})

    go func() {

        for i := 0; i < 100; i++ {

            ch <- i

        }

        close(done)

    }()

    return done

}


func consume(wg *sync.WaitGroup, ch <-chan int) {

    defer wg.Done()

    for i := range ch {

        fmt.Printf("Consumer: %d\n", i)

    }

}


func main() {

    ch := make(chan int, 1)

    wg := sync.WaitGroup{}

    done := produce(ch)

    wg.Add(1)

    go consume(&wg, ch)

    <- done // produce done

    close(ch)

    wg.Wait()

    // consumer done

    fmt.Println("All done, exit")

}


查看完整回答
反对 回复 2023-06-01
?
慕田峪9158850

TA贡献1794条经验 获得超7个赞

我稍微改变了(延长时间。睡眠)你的代码。在我的 Linux x86_64 上运行良好


func Product(ch chan<- int) {

    for i := 0; i < 10; i++ {

        fmt.Println("Product:", i)

        ch <- i

    }

}

func Consumer(ch <-chan int) {

    for i := 0; i < 10; i++ {

        a := <-ch

        fmt.Println("Consmuer:", a)

    }

}

func main() {

    ch := make(chan int, 1)

    go Product(ch)

    go Consumer(ch)

    time.Sleep(10000)

}

输出 go run s1.go


Product: 0

Product: 1

Product: 2


查看完整回答
反对 回复 2023-06-01
?
交互式爱情

TA贡献1712条经验 获得超3个赞

time.Sleep需要一个time.Duration,而不是一个整数。godoc显示了如何正确调用它的示例。在你的情况下,你可能想要:

time.Sleep(500 * time.Millisecond)

您的程序快速退出(但没有给您错误)的原因是由于(有点令人惊讶)的time.Duration实施方式。

time.Duration只是 的类型别名int64。在内部,它使用该值来表示以纳秒为单位的持续时间。当您调用 时time.Sleep(500),编译器会很乐意将数字文字解释500time.Duration. 不幸的是,这意味着 500 ns

time.Millisecond是一个常数,等于毫秒中的纳秒数 (1,000,000)。好处是,要求您显式地进行乘法运算会使调用者清楚地知道该参数的单位是什么。不幸的是,time.Sleep(500)这是完全有效的 go 代码,但没有达到大多数初学者的预期。


查看完整回答
反对 回复 2023-06-01
  • 3 回答
  • 0 关注
  • 100 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信