使用上下文和取消,Go协程不会终止。

huangapple go评论89阅读模式
英文:

Using context with cancel, Go routine doesn't terminate

问题

我是你的中文翻译助手,以下是你提供的代码的翻译:

我对Go和Go并发还不熟悉。我正在尝试使用Go上下文来取消一组Go协程,一旦找到具有给定ID的成员。

一个Group存储了一组Clients,每个Client都有一个成员列表。我想并行搜索所有的Clients和它们的成员,以找到具有给定ID的成员。一旦找到这个成员,我想取消所有其他的Go协程并返回找到的成员。

我尝试了以下实现,使用了context.WithCancel和WaitGroup。

然而,这个实现不起作用,会无限期地挂起,永远无法执行到waitGroup.Wait()这一行,但我不确定具体原因。

func (group *Group) MemberWithID(ID string) (*models.Member, error) {
    found := make(chan *models.Member)
    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()
    var waitGroup sync.WaitGroup

    for _, client := range group.Clients {
        waitGroup.Add(1)

        go func(clientToQuery Client) {
            defer waitGroup.Done()

            select {
            case <-ctx.Done():
                return
            default:
            }

            member, _ := client.ClientMemberWithID(ID)
            if member != nil {
                found <- member
                cancel()
                return
            }

        }(client)

    }

    waitGroup.Wait()

    if len(found) > 0 {
        return <-found, nil
    }

    return nil, fmt.Errorf("no member found with given id")
}

希望对你有帮助!

英文:

I'm new to Go and concurrency in Go. I'm trying to use a Go context to cancel a set of Go routines once I find a member with a given ID.

A Group stores a list of Clients, and each Client has a list of Members. I want to search in parallel all the Clients and all their Members to find a Member with a given ID. Once this Member is found, I want to cancel all the other Go routines and return the discovered Member.

I've tried the following implementation, using a context.WithCancel and a WaitGroup.

This doesn't work however, and hangs indefinitely, never getting past the line waitGroup.Wait(), but I'm not sure why exactly.

func (group *Group) MemberWithID(ID string) (*models.Member, error) {
	found := make(chan *models.Member)
	ctx := context.Background()
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()
	var waitGroup sync.WaitGroup

	for _, client := range group.Clients {
		waitGroup.Add(1)

		go func(clientToQuery Client) {
			defer waitGroup.Done()

			select {
			case &lt;-ctx.Done():
				return
			default:
			}

			member, _ := client.ClientMemberWithID(ID)
			if member != nil {
				found &lt;- member
				cancel()
				return
			}

		} (client)

	}

	waitGroup.Wait()

	if len(found) &gt; 0 {
		return &lt;-found, nil
	}

	return nil, fmt.Errorf(&quot;no member found with given id&quot;)
}

答案1

得分: 4

found是一个无缓冲通道,因此在有人准备好接收时,发送操作会被阻塞。

你的main()函数将会从该通道接收数据,但是只有在waitGroup.Wait()返回之后才会接收。但是这会导致阻塞,直到所有启动的goroutine调用waitGroup.Done()。但是它们只有在能够发送到found通道之后才会返回,这样就造成了死锁。

如果你将found改为有缓冲的通道,那么即使main()函数还没有准备好接收数据,也可以在通道上发送值(缓冲区大小决定了可以发送多少个值)。

但是你应该在waitGroup.Wait()返回之前从found通道接收数据。

另一种解决方案是将found设置为缓冲区大小为1,并在发送操作上使用非阻塞发送。这样,第一个(最快的)goroutine将能够发送结果,而其他的goroutine(假设我们使用非阻塞发送)将会跳过发送操作。

还要注意的是,应该由main()函数调用cancel(),而不是每个启动的goroutine单独调用。

英文:

found is an unbuffered channel, so sending on it blocks until there is someone ready to receive from it.

Your main() function would be the one to receive from it, but only after waitGroup.Wait() returns. But that will block until all launched goroutines call waitGroup.Done(). But that won't happen until they return, which won't happen until they can send on found. It's a deadlock.

If you change found to be buffered, that will allow sending values on it even if main() is not ready to receive from it (as many values as big the buffer is).

But you should receive from found before waitGroup.Wait() returns.

Another solution is to use a buffer of 1 for found, and use non-blocking send on found. That way the first (fastest) goroutine will be able to send the result, and the rest (given we're using non-blocking send) will simply skip sending.

Also note that it should be the main() that calls cancel(), not each launched goroutines individually.

答案2

得分: 4

对于这种情况,我认为sync.Once可能比通道更适合。当你找到第一个非nil成员时,你想要做两件不同的事情:

  1. 记录你找到的成员。
  2. 取消剩余的goroutine。

缓冲通道可以很容易地完成(1),但是(2)会变得稍微复杂一些。但是,sync.Once非常适合在第一次发生有趣的事情时执行两个不同的操作!

我还建议聚合非平凡的错误,这样如果例如数据库连接失败或发生其他非平凡的错误,你可以报告一些更有用的信息。你也可以使用sync.Once来实现这一点!

将所有内容整合在一起,我希望看到类似以下的代码(https://play.golang.org/p/QZXUUnbxOv5):

func (group *Group) MemberWithID(ctx context.Context, id string) (*Member, error) {
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	var (
		wg sync.WaitGroup

		member    *Member
		foundOnce sync.Once

		firstNontrivialErr error
		errOnce            sync.Once
	)

	for _, client := range group.Clients {
		wg.Add(1)
		client := client // https://golang.org/doc/faq#closures_and_goroutines
		go func() {
			defer wg.Done()

			m, err := client.ClientMemberWithID(ctx, id)
			if m != nil {
				foundOnce.Do(func() {
					member = m
					cancel()
				})
			} else if nf := (*MemberNotFoundError)(nil); !errors.As(err, &nf) {
				errOnce.Do(func() {
					firstNontrivialErr = err
				})
			}
		}()
	}
	wg.Wait()

	if member == nil {
		if firstNontrivialErr != nil {
			return nil, firstNontrivialErr
		}
		return nil, &MemberNotFoundError{ID: id}
	}
	return member, nil
}
英文:

For this kind of use case I think a sync.Once is probably a better fit than a channel. When you find the first non-nil member, you want to do two different things:

  1. Record the member you found.
  2. Cancel the remaining goroutines.

A buffered channel can easily do (1), but makes (2) a bit more complicated. But a sync.Once is perfect for doing two different things the first time something interesting happens!


I would also suggest aggregating non-trivial errors, so that you can report something more useful than no member found if, say, your database connection fails or some other nontrivial error occurs. You can use a sync.Once for that, too!


Putting it all together, I would want to see something like this (https://play.golang.org/p/QZXUUnbxOv5):

func (group *Group) MemberWithID(ctx context.Context, id string) (*Member, error) {
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	var (
		wg sync.WaitGroup

		member    *Member
		foundOnce sync.Once

		firstNontrivialErr error
		errOnce            sync.Once
	)

	for _, client := range group.Clients {
		wg.Add(1)
		client := client // https://golang.org/doc/faq#closures_and_goroutines
		go func() {
			defer wg.Done()

			m, err := client.ClientMemberWithID(ctx, id)
			if m != nil {
				foundOnce.Do(func() {
					member = m
					cancel()
				})
			} else if nf := (*MemberNotFoundError)(nil); !errors.As(err, &amp;nf) {
				errOnce.Do(func() {
					firstNontrivialErr = err
				})
			}
		}()
	}
	wg.Wait()

	if member == nil {
		if firstNontrivialErr != nil {
			return nil, firstNontrivialErr
		}
		return nil, &amp;MemberNotFoundError{ID: id}
	}
	return member, nil
}

huangapple
  • 本文由 发表于 2021年9月2日 21:22:06
  • 转载请务必保留本文链接:https://go.coder-hub.com/69031143.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定