Golang中goroutine与context的使用问题探讨

Golang中goroutine与context的使用问题探讨 大家好,

我正在尝试学习 contexts。我写了下面这个简单的示例来测试 withCancel 的行为。我期望当我按下 Ctrl-C 中断时,在程序退出之前,Println("cancelled..")(第26行)会被打印到标准输出。然而,它并没有。:slight_smile: 有人能简单地解释一下我做错了什么,以及根据我最终期望的行为,我应该如何解决这个问题吗?

我相信关于使用上下文的概念,我(还)有些地方不清楚。我阅读了 effective-go 参考、Go 博客文章以及其他一些资料,但仍然看不出问题所在。:exploding_head:

提前感谢您的帮助。

以下是代码片段:

package main

import (
	"context"
	"fmt"
	"os"
	"os/signal"
	"time"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	go infiniteForLoop(ctx)
	sig := make(chan os.Signal, 1)
	signal.Notify(sig, os.Interrupt)
	// 等待 SIGINT。
	<-sig
}

func infiniteForLoop(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("cancelled...")
			return
		default:
			time.Sleep(2 * time.Second)
			fmt.Println("looping...")
		}
	}
}

更多关于Golang中goroutine与context的使用问题探讨的实战教程也可以访问 https://www.itying.com/category-94-b0.html

5 回复

感谢你的建议 @christophberger

关于“done/ok channel”解决方案,我该如何为多个并发(可能并行)的 goroutine 实现这样的机制?类似于 WaitGroup.Add() 的概念。在这方面有什么好的实践吗?我想到的一个简单方法是为每个正在运行的 goroutine 创建一个新的“ok channel”(例如,一个 ok channel 的切片?),然后等待所有 channel 都被填充?但这看起来肯定不优雅/不容易实现…… 🤔 🤔

我相信应该有一种“标准”的方法来处理这些事情,因为一个典型的互联网服务器(例如聊天服务器)希望在停止/终止之前关闭连接……

再次提前感谢你宝贵的时间和支持!

更多关于Golang中goroutine与context的使用问题探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


您可以创建一个单一的通道并将其传递给所有 goroutine。它们都将自己的“ok”状态写入该通道,然后接收的 goroutine(通常是主 goroutine)会计算接收到的“ok”数量,直到所有 goroutine 都作出响应。(示例。)

但是,WaitGroup 也能实现相同的功能,并且可能对读者来说更清晰,因为等待毕竟是 WaitGroup 的唯一目的。


编辑:增强示例,其中每个 goroutine 在完成时发送其“ID”回来。只是为了好玩,看看 goroutine 运行的非确定性顺序。

func main() {
    fmt.Println("hello world")
}

你好 @unique_gembom

一个简单的技巧良好实践是仅将 main 函数用于启动应用程序。将所有更复杂的操作,包括那些需要延迟清理的操作,都放入一个单独的函数中。

func main() {
    // 仅进行非常基础的设置
    err := run()
    if err != nil {
        log.Println(err)
    }
}

func run() error {
    ctx, cancel := ...
    defer cancel()
    // 执行其他操作
    return nil
}

run() 函数内部的所有延迟调用都可以在 main() 函数尝试退出之前完成。

使用一个“done”或“ok”通道来通知一个 goroutine 已完成工作是可以的。 或者,你可以使用 sync.WaitGroupErrGroup 来等待多个 goroutine。后者甚至允许 goroutine 返回错误。

我想我找到了问题所在。看起来 main() 函数在 sig 通道接收到信号后立即退出。这样就没有给 goroutine 执行其 println() 留出任何时间。

针对这一点,我可能需要实现第二个通道,用来阻塞 main() 函数,直到在 infiniteForLoop()case ctx.Done() 中填充该通道。这还需要显式调用 cancel() 函数(而不是使用 defer)。所以代码大致如下(见下方):

package main

import (
	"context"
	"fmt"
	"os"
	"os/signal"
	"time"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	//defer cancel()
	ok := make(chan bool, 1)

	go infiniteForLoop(ctx, ok)
	sig := make(chan os.Signal, 1)
	signal.Notify(sig, os.Interrupt)
	// Wait for SIGINT.
	<-sig
	fmt.Println("interrupt received...waiting for loop to cancel...")
	cancel()
	<-ok
}

func infiniteForLoop(ctx context.Context, ok chan bool) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("cancelled...", ctx.Err())
			ok <- true
			return
		default:
			time.Sleep(2 * time.Second)
			fmt.Println("looping...")
		}
	}
}

这是惯用的做法吗?还是有更好的方法建议?再次非常感谢!

在你的代码中,当收到 SIGINT 信号时,主函数会直接退出,而没有调用 cancel() 函数来取消上下文。defer cancel() 会在主函数返回时执行,但由于主函数在收到信号后直接退出,cancel() 没有被触发,导致 infiniteForLoop 中的 ctx.Done() 通道没有关闭,因此 "cancelled..." 不会被打印。

要解决这个问题,你需要在收到信号后显式调用 cancel(),并等待 goroutine 完成。以下是修改后的代码:

package main

import (
	"context"
	"fmt"
	"os"
	"os/signal"
	"sync"
	"time"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()
		infiniteForLoop(ctx)
	}()

	sig := make(chan os.Signal, 1)
	signal.Notify(sig, os.Interrupt)
	<-sig

	// 取消上下文,通知 goroutine 停止
	cancel()
	// 等待 goroutine 完成
	wg.Wait()
}

func infiniteForLoop(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("cancelled...")
			return
		default:
			time.Sleep(2 * time.Second)
			fmt.Println("looping...")
		}
	}
}

在这个修改版本中,我们使用 sync.WaitGroup 来等待 infiniteForLoop 完成。当收到 SIGINT 信号时,主函数调用 cancel() 来取消上下文,然后通过 wg.Wait() 等待 goroutine 打印 "cancelled..." 并返回。这样就能确保在程序退出前打印出取消信息。

回到顶部