【问题标题】:Using Golang channels to handle HTTP requests使用 Golang 通道处理 HTTP 请求
【发布时间】:2014-11-01 11:38:56
【问题描述】:

我正在尝试构建一个简单的 Golang/Appengine 应用程序,它使用一个通道来处理每个 http 请求。原因是我希望每个请求都执行合理的大内存计算,并且每个请求都以线程安全的方式执行很重要(即来自并发请求的计算不会混合)。

基本上我需要一个同步队列,它一次只处理一个请求,并且通道看起来很合适。

Is it possible to use Go's buffered channel as a thread-safe queue?

但是,我无法让我的简单 hello world 示例正常工作。它似乎在'go process(w,cr)'行上失败了;我从服务器收到 200 响应,但没有内容。如果我从这一行中删除“go”,效果很好,但我猜我没有正确调用频道。

有人指出我哪里错了吗?

谢谢!

// curl -X POST "http://localhost:8080/add" -d "{\"A\":1, \"B\":2}"

package hello

import (
    "encoding/json"
    "net/http"  
)

type MyStruct struct {
    A, B, Total int64
}

func (s *MyStruct) add() {
    s.Total = s.A + s.B
}

func process(w http.ResponseWriter, cr chan *http.Request) {
    r := <- cr
    var s MyStruct
    json.NewDecoder(r.Body).Decode(&s)
    s.add()
    json.NewEncoder(w).Encode(s)
}

func handler(w http.ResponseWriter, r *http.Request) {  
    cr := make(chan *http.Request, 1)
    cr <- r
    go process(w, cr) // doesn't work; no response :-(
    // process(w, cr) // works, but blank response :-(
}

func init() {
    http.HandleFunc("/add", handler)
}

【问题讨论】:

  • 仅供参考 - 你说的那个返回空白响应对我来说很好。不过,您的问题是,Go 将刷新响应,因为它认为它已经完成了它的工作。在此处使用go 来触发goroutine 将意味着您的处理代码在 请求被刷新之后运行。也就是说,ListenAndServe 将为您处理并发。它实际上会在每个请求的循环中触发一个 goroutine(也就是说,您的处理程序已作为 goroutine 被触发)。那么您还想添加什么?
  • ListenAndServe 听起来可能就是我要找的东西;但是它为每个请求线程触发的 goroutine 是否安全?还是我需要将它们与频道一起使用?
  • 不,它们不是线程安全的——它们只是触发 goroutine 并继续监听。如果您共享全局状态,这实际上只是一个问题 - 在上面的示例中您似乎没有这样做。也就是说,您的大型计算可能有一些全局状态。所以这对您来说可能是个问题。
  • 非常有用,谢谢。没有全局状态,但 calc 包含一些我知道不是线程安全的映射;将 http.HandleFunc 与 calc 结合使用似乎会导致线程错误,即 calc 结果都是错误的/混淆了。您认为使用 ListenAndServer/goroutines 可能会解决问题吗? [也许是因为 http.HandleFunc 在同一个线程/goroutine 中做所有事情?]
  • 我认为你让这变得比它需要的更复杂。为什么不只使用请求处理程序的本地映射?在这种情况下,同步访问全局状态似乎并不能真正阻止事情变得混乱。

标签: google-app-engine concurrency go


【解决方案1】:

如果大型计算不使用共享可变状态,则编写一个普通的处理程序。不需要频道等等。

好的,大型计算确实使用共享可变状态。如果只有一个应用程序实例在运行,则使用 sync.Mutex 来控制对可变状态的访问。与将工作改组到单个 goroutine 来一次处理一个计算相比,这很简单。

您是否在 App Engine 上运行?您可能无法保证只有一个应用程序实例正在运行。您需要将数据存储或内存缓存用于可变状态。如果计算可以离线完成(在请求完成后),那么您可以使用 App Engine 任务队列一次处理一个计算。

附注:标题提出了问题正文中所述问题的解决方案。最好直接说明问题。我会在上面对此发表评论,但我没有所需的果汁。

【讨论】:

    【解决方案2】:

    不确定这是不是正确的设计,但我怀疑问题在于您从哪里开始第二个 go 例程,第一个 go 例程继续并完成写入连接等。

    要停止这种情况,您可以使用等待组 (http://golang.org/pkg/sync/#WaitGroup) 让第一个例程等待。

    这停止了您为什么尝试将其放入线程背后的整个推理(因此我认为您遇到了设计问题)。

    这里有一些未经测试的代码,应该可以工作,或者至少有助于朝着正确的方向发展。

    package main
    
    import (
        "encoding/json"
        "net/http"
        "sync"  
    )
    
    type MyStruct struct {
        A, B, Total int64
    }
    
    func (s *MyStruct) add() {
        s.Total = s.A + s.B
    }
    
    func process(w http.ResponseWriter, cr chan *http.Request) {
        r := <- cr
        var s MyStruct
        json.NewDecoder(r.Body).Decode(&s)
        s.add()
        json.NewEncoder(w).Encode(s)
    }
    
    func handler(w http.ResponseWriter, r *http.Request) {  
        cr := make(chan *http.Request, 1)
        cr <- r
        var pleasewait sync.WaitGroup
        pleasewait.Add(1)
    
        go func() {
            defer pleasewait.Done()
            process(w, cr) // doesn't work; no response :-(
        }()
        // process(w, cr) // works, but blank response :-(
    
        pleasewait.Wait()
    }
    
    func main() {
        http.HandleFunc("/add", handler)
    }
    

    【讨论】:

    • 这不是让它同步吗?该处理程序将已经通过net/http 包在goroutine 中被触发。此外,这里的通道访问只是从通道中阻塞拉取..
    • 是的。这是对错误解决方案的正确答案。我认为这种事情的“企业”解决方案是使用消息队列。另一种方法是发出请求并且响应是 200 感谢请求发回 id。客户端然后用 id 轮询服务器,当响应准备好时,它会发送结果,如果它还没有准备好,那么它会发送,请稍后再试..
    • 消息队列肯定不是您用来服务简单 GET 请求的东西。至少我希望不会。
    • 问题不是简单的 GET 请求,而是取决于正在进行的大型计算。什么会阻止您启动 100 个客户端连接并让服务器屈服?我仍然坚持认为这是不正确的设计。
    • 对于设计帮助,我会通读:nesv.github.io/golang/2014/02/25/worker-queues-in-go.html
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-06-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多