简体   繁体   English

将数据从一个 goroutine 发送到多个其他 goroutine

[英]Sending data from one goroutine to multiple other goroutines

In a project the program receives data via websocket.在一个项目中,程序通过 websocket 接收数据。 This data needs to be processed by n algorithms.这些数据需要经过 n 个算法处理。 The amount of algorithms can change dynamically.算法的数量可以动态变化。

My attempt is to create some pub/sub pattern where subscriptions can be started and canceled on the fly.我的尝试是创建一些可以即时启动和取消订阅的发布/订阅模式。 Turns out that this is a bit more challenging than expected.事实证明,这比预期的更具挑战性。

Here's what I came up with (which is based onhttps://eli.thegreenplace.net/2020/pubsub-using-channels-in-go/ ):这是我想出的(基于https://eli.thegreenplace.net/2020/pubsub-using-channels-in-go/ ):

package pubsub

import (
    "context"
    "sync"
    "time"
)

type Pubsub struct {
    sync.RWMutex
    subs   []*Subsciption
    closed bool
}

func New() *Pubsub {
    ps := &Pubsub{}
    ps.subs = []*Subsciption{}
    return ps
}

func (ps *Pubsub) Publish(msg interface{}) {
    ps.RLock()
    defer ps.RUnlock()

    if ps.closed {
        return
    }

    for _, sub := range ps.subs {
        // ISSUE1: These goroutines apparently do not exit properly... 
        go func(ch chan interface{}) {
            ch <- msg
        }(sub.Data)
    }
}

func (ps *Pubsub) Subscribe() (context.Context, *Subsciption, error) {
    ps.Lock()
    defer ps.Unlock()

    // prep channel
    ctx, cancel := context.WithCancel(context.Background())
    sub := &Subsciption{
        Data:   make(chan interface{}, 1),
        cancel: cancel,
        ps:     ps,
    }

    // prep subsciption
    ps.subs = append(ps.subs, sub)
    return ctx, sub, nil
}

func (ps *Pubsub) unsubscribe(s *Subsciption) bool {
    ps.Lock()
    defer ps.Unlock()

    found := false
    index := 0
    for i, sub := range ps.subs {
        if sub == s {
            index = i
            found = true
        }
    }
    if found {
        s.cancel()
        ps.subs[index] = ps.subs[len(ps.subs)-1]
        ps.subs = ps.subs[:len(ps.subs)-1]

        // ISSUE2: close the channel async with a delay to ensure
        // nothing will be written to the channel anymore
        // via a pending goroutine from Publish()
        go func(ch chan interface{}) {
            time.Sleep(500 * time.Millisecond)
            close(ch)
        }(s.Data)
    }
    return found
}

func (ps *Pubsub) Close() {
    ps.Lock()
    defer ps.Unlock()

    if !ps.closed {
        ps.closed = true
        for _, sub := range ps.subs {
            sub.cancel()

            // ISSUE2: close the channel async with a delay to ensure
            // nothing will be written to the channel anymore
            // via a pending goroutine from Publish()
            go func(ch chan interface{}) {
                time.Sleep(500 * time.Millisecond)
                close(ch)
            }(sub.Data)
        }
    }
}

type Subsciption struct {
    Data   chan interface{}
    cancel func()
    ps     *Pubsub
}

func (s *Subsciption) Unsubscribe() {
    s.ps.unsubscribe(s)
}

As mentioned in the comments there are (at least) two issues with this:正如评论中提到的,这有(至少)两个问题:

ISSUE1:问题 1:

After a while of running in implementation of this I get a few errors of this kind:在执行此操作一段时间后,我遇到了一些此类错误:

goroutine 120624 [runnable]:
bm/internal/pubsub.(*Pubsub).Publish.func1(0x8586c0, 0xc00b44e880, 0xc008617740)
    /home/X/Projects/bm/internal/pubsub/pubsub.go:30
created by bookmaker/internal/pubsub.(*Pubsub).Publish
    /home/X/Projects/bm/internal/pubsub/pubsub.go:30 +0xbb

Without really understanding this it appears to me that the goroutines created in Publish() do accumulate/leak.在没有真正理解这一点的情况下,在我看来,在Publish()中创建的 goroutine 确实会累积/泄漏。 Is this correct and what am I doing wrong here?这是正确的吗?我在这里做错了什么?

ISSUE2:问题2:

When I end a subscription via Unsubscribe() it occurs that Publish() tried to write to a closed channel and panics.当我通过Unsubscribe()结束订阅时, Publish()尝试写入已关闭的频道并出现恐慌。 To mitigate this I have created a goroutine to close the channel with a delay.为了缓解这种情况,我创建了一个 goroutine 来延迟关闭通道。 This feel really off-best-practice but I was not able to find a proper solution to this.这感觉真的不是最佳实践,但我无法找到合适的解决方案。 What would be a deterministic way to do this?什么是确定性的方法来做到这一点?

Heres a little playground for you to test with: https://play.golang.org/p/K-L8vLjt7_9这里有一个小操场供您测试: https://play.golang.org/p/K-L8vLjt7_9

Before diving into your solution and its issues, let me recommend again another Broker approach presented in this answer: How to broadcast message using channel在深入研究您的解决方案及其问题之前,让我再次推荐此答案中提出的另一种代理方法: 如何使用频道广播消息

Now on to your solution.现在开始您的解决方案。


Whenever you launch a goroutine, always think of how it will end and make sure it does if the goroutine is not ought to run for the lifetime of your app.每当您启动一个 goroutine 时,请始终考虑它将如何结束,并确保如果该 goroutine 不应该在您的应用程序的生命周期内运行,它会如何结束。

// ISSUE1: These goroutines apparently do not exit properly... 
go func(ch chan interface{}) {
    ch <- msg
}(sub.Data)

This goroutine tries to send a value on ch .这个 goroutine 尝试在ch上发送一个值。 This may be a blocking operation: it will block if ch 's buffer is full and there is no ready receiver on ch .这可能是一个阻塞操作:如果ch的缓冲区已满并且ch上没有准备好的接收器,它将阻塞。 This is out of the control of the launched goroutine, and also out of the control of the pubsub package.这不受启动的 goroutine 的控制,也不受pubsub package 的控制。 This may be fine in some cases, but this already places a burden on the users of the package.在某些情况下这可能很好,但这已经给 package 的用户带来了负担。 Try to avoid these.尽量避免这些。 Try to create APIs that are easy to use and hard to misuse.尝试创建易于使用且难以误用的 API。

Also, launching a goroutine just to send a value on a channel is a waste of resources (goroutines are cheap and light, but you shouldn't spam them whenever you can).此外,启动一个 goroutine 只是为了在通道上发送一个值是一种资源浪费(goroutines 便宜又轻巧,但你不应该尽可能地向它们发送垃圾邮件)。

You do it because you don't want to get blocked.你这样做是因为你不想被阻止。 To avoid blocking, you may use a buffered channel with a "reasonable" high buffer.为避免阻塞,您可以使用具有“合理”高缓冲区的缓冲通道。 Yes, this doesn't solve the blocking issue, in only helps with "slow" clients receiving from the channel.是的,这并不能解决阻塞问题,只能帮助从通道接收的“慢”客户端。

To "truly" avoid blocking without launching a goroutine, you may use non-blocking send:要“真正”避免在不启动 goroutine 的情况下阻塞,您可以使用非阻塞发送:

select {
case ch <- msg:
default:
    // ch's buffer is full, we cannot deliver now
}

If send on ch can proceed, it will happen.如果 send on ch可以继续,它就会发生。 If not, the default branch is chosen immediately.如果不是,则立即选择default分支。 You have to decide what to do then.你必须决定然后做什么。 Is it acceptable to "lose" a message? “丢失”消息是否可以接受? Is it acceptable to wait for some time until "giving up"?等待一段时间直到“放弃”可以接受吗? Or is it acceptable to launch a goroutine to do this (but then you'll be back at what we're trying to fix here)?或者启动一个 goroutine 来做到这一点是否可以接受(但是你会回到我们在这里试图修复的问题)? Or is it acceptable to get blocked until the client can receive from the channel...或者在客户端可以从频道接收之前被阻止是否可以接受......

Choosing a reasonable high buffer, if you encounter a situation when it still gets full, it may be acceptable to block until the client can advance and receive from the message.选择合理的高缓冲区,如果遇到仍然满的情况,可以接受阻塞,直到客户端可以前进并从消息中接收。 If it can't, then your whole app might be in an unacceptable state, and it might be acceptable to "hang" or "crash".如果不能,那么您的整个应用程序可能处于不可接受的 state 中,并且“挂起”或“崩溃”可能是可以接受的。

// ISSUE2: close the channel async with a delay to ensure
// nothing will be written to the channel anymore
// via a pending goroutine from Publish()
go func(ch chan interface{}) {
    time.Sleep(500 * time.Millisecond)
    close(ch)
}(s.Data)

Closing a channel is a signal to the receiver(s) that no more values will be sent on the channel.关闭通道是向接收器发出的信号,表明通道上将不再发送任何值。 So always it should be the sender's job (and responsibility) to close the channel.因此,关闭通道始终应该是发送者的工作(和责任)。 Launching a goroutine to close the channel, you "hand" that job and responsibility to another "entity" (a goroutine) that will not be synchronized to the sender.启动一个 goroutine 来关闭通道,你将那个工作和职责“交给”另一个不会同步到发送者的“实体”(一个 goroutine)。 This may easily end up in a panic (sending on a closed channel is a runtime panic, for other axioms see How does a non initialized channel behave? ).这可能很容易导致恐慌(在关闭的通道上发送是运行时恐慌,对于其他公理,请参阅未初始化的通道如何表现? )。 Don't do that.不要那样做。

Yes, this was necessary because you launched goroutines to send.是的,这是必要的,因为您启动了 goroutines 来发送。 If you don't do that, then you may close "in-place", without launching a goroutine, because then the sender and closer will be the same entity: the Pubsub itself, whose sending and closing operations are protected by a mutex.如果你不这样做,那么你可以“就地”关闭,而不启动 goroutine,因为发送者和关闭者将是同一个实体: Pubsub本身,其发送和关闭操作受到互斥锁的保护。 So solving the first issue solves the second naturally.所以解决第一个问题自然会解决第二个问题。

In general if there are multiple senders for a channel, then closing the channel must be coordinated.一般来说,如果一个通道有多个发送者,则必须协调关闭通道。 There must be a single entity (often not any of the senders) that waits for all senders to finish, practically using a sync.WaitGroup , and then that single entity can close the channel, safely.必须有一个实体(通常不是任何发件人)等待所有发件人完成,实际上使用的是sync.WaitGroup ,然后该单个实体可以安全地关闭通道。 See Closing channel of unknown length .请参阅关闭未知长度的通道

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM