如何处理缺少线程安全的ZMQ套接字?

时间:2016-04-05 21:57:36

标签: sockets go zeromq

我一直在某些Python应用程序中使用ZMQ,但直到最近我才决定在Go中重新实现其中一个,我意识到ZMQ套接字不是线程安全的。

原始Python实现使用如下所示的事件循环:

while running:
    socks = dict(poller.poll(TIMEOUT))
    if socks.get(router) == zmq.POLLIN:
        client_id = router.recv()
        _ = router.recv()
        data = router.recv()
        requests.append((client_id, data))

    for req in requests:
        rep = handle_request(req)
        if rep:
            replies.append(rep)
            requests.remove(req)

    for client_id, data in replies:
        router.send(client_id, zmq.SNDMORE)
        router.send(b'', zmq.SNDMORE)
        router.send(data)
        del replies[:]

问题是第一次传递时回复可能没有准备好,所以每当我有待处理的请求时,我必须以非常短的超时进行轮询,否则客户端将等待超过它们的应用程序,并且应用程序结束使用大量的CPU进行轮询。

当我决定在Go中重新实现它时,我认为它会像这样简单,通过在轮询上使用无限超时来避免问题:

for {
    sockets, _ := poller.Poll(-1) 
    for _, socket := range sockets {
        switch s := socket.Socket; s {
        case router:
            msg, _ := s.RecvMessage(0)
            client_id := msg[0]
            data := msg[2]
            go handleRequest(router, client_id, data)                
        }
    }
}

但是,这种理想的实现仅在我连接单个客户端或轻负载时才有效。在重负载下,我在libzmq中得到随机断言错误。我尝试了以下方法:

  1. zmq4 docs之后,我尝试在所有套接字操作上添加sync.Mutex并锁定/解锁。它失败。我认为这是因为ZMQ使用自己的线程进行刷新。

  2. 创建一个用于轮询/接收的goroutine和一个用于发送的goroutine,并使用与我在Python版本中使用req / rep队列相同的方式使用通道。它失败了,因为我还在共享套接字。

  3. 与2相同,但设置为GOMAXPROCS=1。它失败了,吞吐量非常有限,因为回复被阻止,直到Poll()调用返回。

  4. 使用2中的req / rep通道,但使用runtime.LockOSThread将所有套接字操作保持在与套接字相同的线程中。有与上面相同的问题。它没有失败,但吞吐量非常有限。

  5. 与4相同,但使用Python版本中的轮询超时策略。它有效,但是Python版本也有同样的问题。

  6. 共享上下文而不是套接字并创建一个用于发送的套接字和一个用于在单独的goroutine中接收的套接字,与通道进行通信。它可以工作,但我必须重写客户端库以使用两个套接字而不是一个。

  7. 摆脱zmq并使用线程安全的原始TCP套接字。它工作得很好,但我还必须重写客户端库。

  8. 所以,看起来6是ZMQ真正打算如何使用,因为这是我与goroutines无缝协作的唯一方法,但我想知道是否还有其他方法我没有尝试过。有什么想法吗?

    更新

    通过这里的答案,我意识到我可以向轮询器添加一个inproc PULL套接字并进行goroutine连接并推送一个字节以突破无限等待。它不像这里提出的解决方案那样通用,但它可以工作,我甚至可以将它反向移植到Python版本。

2 个答案:

答案 0 :(得分:4)

我在1。5年前opened an issue向pebbe / zmq4引入了https://github.com/vaughan0/go-zmq/blob/master/channels.go的端口。最终作者决定反对它,但我们已经在生产中使用了这个(在非常繁重的工作量下)。

这是必须添加到pebbe / zmq4包的文件的gist(因为它将方法添加到Socket)。这可以通过这样的方式重写,即Socket接收器上的方法取而代之的是Socket作为参数,但由于我们无论如何都要提供代码,这是一个简单的方法。

基本用法是创建Socket正常情况(例如,将其称为s),然后您可以:

channels := s.Channels()
outBound := channels.Out()
inBound := channels.In()

现在你有两个类型[][]byte的通道可以在goroutine之间使用,但是在通道抽象中管理的单个goroutine负责管理Poller并与套接字通信。 / p>

答案 1 :(得分:1)

使用pebbe / zmq4执行此操作的有福方法是使用Reactor。反应堆能够监听Go频道,但你不想这样做,因为他们是通过使用轮询超时定期轮询频道来实现的,这会重新引入你遇到的同样问题在你的Python版本中。相反,您可以使用zmq inproc套接字,其一端由反应器保持,另一端由goroutine保持,该goroutine从通道传递数据。它复杂,冗长,令人不快,但我成功地使用了它。