【问题标题】:Fire and forget goroutine golang火而忘记 goroutine golang
【发布时间】:2021-07-13 09:15:59
【问题描述】:

我编写了一个 API,它可以进行 DB 调用并执行一些业务逻辑。我正在调用一个必须在后台执行某些操作的 goroutine。 由于 API 调用不应等待此后台任务完成,因此我在调用 goroutine 后立即返回 200 OK(假设后台任务永远不会出错。)

我读到 goroutine 将在 goroutine 完成其任务后终止。 这种“一劳永逸”的方式对 goroutine 泄漏安全吗? goroutine 在执行任务后是否会被终止并清理?

func DefaultHandler(w http.ResponseWriter, r *http.Request) {
    // Some DB calls
    // Some business logics
    go func() {
        // some Task taking 5 sec
    }()
    w.WriteHeader(http.StatusOK)
}

【问题讨论】:

  • “这种火灾和遗忘方式对 goroutine 泄漏安全吗?” -- 如果你知道你的 goroutine 将退出(或者你不希望它退出)。例如,只有当您的 goroutine 陷入意外的无限循环时,才会发生“泄漏”。

标签: go goroutine fire-and-forget


【解决方案1】:

我建议始终控制您的 goroutine,以避免内存和系统耗尽。 如果您收到大量请求并且开始不受控制地生成 goroutine,那么系统可能迟早会宕机。

在那些需要立即返回 200Ok 的情况下,最好的方法是创建一个消息队列,因此服务器只需要在队列中创建一个作业并返回 ok 并忘记。其余的将由消费者异步处理。

生产者(HTTP 服务器) >>> 队列 >>> 消费者

通常,队列是外部资源(RabbitMQ、AWS SQS...),但出于教学目的,您可以使用通道作为消息队列来实现相同的效果。

在示例中,您将看到我们如何创建一个通道来通信 2 个进程。 然后我们启动将从通道读取的工作进程,然后使用将写入通道的处理程序启动服务器。

在发送 curl 请求时尝试使用缓冲区大小和作业时间。

package main

import (
    "fmt"
    "log"
    "net/http"
    "time"
)

/*
$ go run .

curl "http://localhost:8080?user_id=1"
curl "http://localhost:8080?user_id=2"
curl "http://localhost:8080?user_id=3"
curl "http://localhost:8080?user_id=....."

*/

func main() {

    queueSize := 10
    // This is our queue, a channel to communicate processes. Queue size is the number of items that can be stored in the channel
    myJobQueue := make(chan string, queueSize) // Search for 'buffered channels'

    // Starts a worker that will read continuously from our queue
    go myBackgroundWorker(myJobQueue)

    // We start our server with a handler that is receiving the queue to write to it
    if err := http.ListenAndServe("localhost:8080", myAsyncHandler(myJobQueue)); err != nil {
        panic(err)
    }
}

func myAsyncHandler(myJobQueue chan<- string) http.HandlerFunc {
    return func(rw http.ResponseWriter, r *http.Request) {
        // We check that in the query string we have a 'user_id' query param
        if userID := r.URL.Query().Get("user_id"); userID != "" {
            select {
            case myJobQueue <- userID: // We try to put the item into the queue ...
                rw.WriteHeader(http.StatusOK)
                rw.Write([]byte(fmt.Sprintf("queuing user process: %s", userID)))
            default: // If we cannot write to the queue it's because is full!
                rw.WriteHeader(http.StatusInternalServerError)
                rw.Write([]byte(`our internal queue is full, try it later`))
            }
            return
        }
        rw.WriteHeader(http.StatusBadRequest)
        rw.Write([]byte(`missing 'user_id' in query params`))
    }
}

func myBackgroundWorker(myJobQueue <-chan string) {
    const (
        jobDuration = 10 * time.Second // simulation of a heavy background process
    )

    // We continuosly read from our queue and process the queue 1 by 1.
    // In this loop we could spawn more goroutines in a controlled way to paralelize work and increase the read throughput, but i don't want to overcomplicate the example.
    for userID := range myJobQueue {
        // rate limiter here ...
        // go func(u string){
        log.Printf("processing user: %s, started", userID)
        time.Sleep(jobDuration)
        log.Printf("processing user: %s, finisehd", userID)
        // }(userID)
    }
}

【讨论】:

  • 但是发送和接收被阻塞,直到对方准备好。所以我认为它不会是一场确切的火灾并忘记。
  • @Anish 只要有默认情况,它就不会阻止。这个例子真的很好。
  • 很抱歉问了很多后续问题,但是当一个默认案例被执行时,这意味着我们的后台任务案例丢失了,因为我们选择走远默认案例,因为我的频道是还没准备好接收。我说的对吗?
  • 这个想法是通过管理通道上的背压来保护您的系统,如果通道已满,则服务器返回 500。然后客户端应实施重试策略,因此不会丢失任何消息。此外,这个例子对生产环境不利,因为如果服务器崩溃,队列中的所有消息都将丢失,如果你生成大量 groutine 并且发生崩溃也是如此......我的建议是 2 个独立的进程:服务器和工人。服务器接收消息并将它们放入队列并返回。 Worker 按照自己的节奏从队列中读取消息进行处理
【解决方案2】:

您无需处理“goroutine 清理”,您只需启动 goroutine,当作为 goroutine 启动的函数返回时它们将被清理。引用自Spec: Go statements:

当函数终止时,它的 goroutine 也会终止。如果函数有任何返回值,则在函数完成时将其丢弃。

所以你做的很好。但是请注意,您启动的 goroutine 不能使用或假设有关请求 (r) 和响应编写器 (w) 的任何内容,您只能在从处理程序返回之前使用它们。

另外请注意,您不必写http.StatusOK,如果您从处理程序返回而没有写任何内容,则认为这是成功的,HTTP 200 OK 将被自动发回。

查看相关/可能的重复:Webhook process run on another goroutine

【讨论】:

  • 当您说“--您只需启动 goroutines,当作为 goroutine 启动的函数返回时,它们将被清理。”您的返回是否意味着执行完毕?询问是因为我的函数没有返回任何内容,它只是执行某些任务。
  • @Anish 它不需要返回任何东西。引用Spec: Go statements:“当函数终止时,它的goroutine也终止。如果函数有任何返回值,则在函数完成时将它们丢弃。”
  • 好的,但可能是我遗漏了一些东西,因为就像我提到的那样,我的方法是一个不返回任何东西的方法。并对我的 API 进行负载测试,CPU 使用率每增加 10rps 就会增加 20%,(rps, cpu) -> (10,23), (20,46), ... 和崩溃。
  • @Anish 如果 goroutine 有一个冗长的任务,持续调用你的处理程序,你最终会得到大量正在运行的 goroutine。这显然需要内存和 CPU(管理、调度和执行 goroutine,清理堆栈等)。
【解决方案3】:

@icza 是绝对正确的,没有“goroutine 清理”,您可以使用 webhook 或像 gocraft 这样的后台作业。我能想到的使用您的解决方案的唯一方法是将同步包用于学习目的。

func DefaultHandler(w http.ResponseWriter, r *http.Request) {
// Some DB calls
// Some business logics
var wg sync.WaitGroup
wg.Add(1)
go func() {
  defer wg.Done()
    // some Task taking 5 sec
}()
w.WriteHeader(http.StatusOK)
wg.wait()

}

【讨论】:

  • 你可以对频道做同样的事情,不是吗?
  • 频道速度较​​慢
【解决方案4】:

您可以使用 &amp;sync.WaitGroup 等待 goroutine 完成:

// BusyTask
func BusyTask(t interface{}) error {
    var wg = &sync.WaitGroup{}

    wg.Add(1)
    go func() {
        // busy doing stuff
        time.Sleep(5 * time.Second)
        wg.Done()
    }()
    wg.Wait() // wait for goroutine

    return nil
}

// this will wait 5 second till goroutune finish
func main() {
    fmt.Println("hello")

    BusyTask("some task...")

    fmt.Println("done")
}

另一种方法是将context.Context附加到goroutine并超时。

//
func BusyTaskContext(ctx context.Context, t string) error {
    done := make(chan struct{}, 1)
    //
    go func() {
        // time sleep 5 second
        time.Sleep(5 * time.Second)
        // do tasks and signle done
        done <- struct{}{}
        close(done)
    }()
    //
    select {
    case <-ctx.Done():
        return errors.New("timeout")
    case <-done:
        return nil
    }
}

//
func main() {
    fmt.Println("hello")

    ctx, cancel := context.WithTimeout(context.TODO(), 2*time.Second)
    defer cancel()

    if err := BusyTaskContext(ctx, "some task..."); err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println("done")
}

【讨论】:

  • 我认为关键是他们想要“一劳永逸”,特别是等待 goroutine 完成,但担心可能导致 goroutine 泄漏。跨度>
  • 没错!设置上下文比等待 goroutine 完成要好。
猜你喜欢
  • 2015-02-12
  • 2015-12-07
  • 2016-02-14
  • 2022-01-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多