【问题标题】:golang problem with channels ( buffered vs unbuffered )通道的 golang 问题(缓冲与无缓冲)
【发布时间】:2020-09-03 04:48:21
【问题描述】:

我正在开发一个个人项目,该项目将在 Raspberry Pi 上运行,并附有一些传感器。

它由两个程序组成:

  1. 每 X 秒从传感器读取数据的服务器
  2. 在 sqlite 数据库中保存数据并可以发送一些命令的客户端

服务器可以:

  • 从传感器读取数据,将它们写入套接字,以便客户端可以将其保存在数据库中
  • 监听套接字,这样当客户端发送一些命令时,它可以执行它并将响应发送回客户端

从传感器读取的函数和处理套接字连接的函数在不同的 goroutine 中执行,因此,为了在从传感器读取数据时在套接字上发送数据,我在其中创建了一个 []byte 通道main 函数,将其传递给 goroutines。

当从传感器收集数据时(如果有客户端连接)被写入通道,所以另一个函数将它写入套接字并且客户端接收它。

我的问题出现在这里:如果我连续执行多次写入,则只有第一个数据到达客户端,而其他数据不会。但是,如果我在写入通道的函数中添加一点 time.sleep,所有数据都会正确到达客户端。

不管怎样,这是这个小程序的简化版:

package main

import (
    "net"
    "os"
    "sync"
    "time"
)

const socketName string = "./test_socket"

// create to the socket and launch the accept client routine
func launchServerUDS(ch chan []byte) {
    if err := os.RemoveAll(socketName); err != nil {
        return
    }
    l, err := net.Listen("unix", socketName)
    if err != nil {
        return
    }
    go acceptConnectionRoutine(l, ch)
}

// accept incoming connection on the socket and
// 1) launch the routine to handle commands from the client
// 2) launch the routine to send data when the server reads from the sensors
func acceptConnectionRoutine(l net.Listener, ch chan []byte) {
    defer l.Close()
    for {
        conn, err := l.Accept()
        if err != nil {
            return
        }
        go commandsHandlerRoutine(conn, ch)
        go autoSendRoutine(conn, ch)

    }
}

// routine that sends data to the client
func autoSendRoutine(c net.Conn, ch chan []byte) {
    for {
        data := <-ch
        if string(data) == "exit" {
            return
        }
        c.Write(data)
    }
}

// handle client connection and calls functions to execute commands
func commandsHandlerRoutine(c net.Conn, ch chan []byte) {
    for {
        buf := make([]byte, 1024)
        n, err := c.Read(buf)
        if err != nil {
            ch <- []byte("exit")
            break
        }
        // now, for sake of simplicity , only echo commands back to the client
        _, err = c.Write(buf[:n])
        if err != nil {
            ch <- []byte("exit")
            break
        }
    }
}

// write on the channel to the autosend routine so the data are written on the socket
func sendDataToClient(data []byte, ch chan []byte) {
    select {
    case ch <- data:
        // if i put a little sleep here, no problems
        // i i remove the sleep, only data1 is sent to the client
        // time.Sleep(1 * time.Millisecond)
    default:
    }
}

func dummyReadDataRoutine(ch chan []byte) {
    for {
        // read data from the sensors every 5 seconds
        time.Sleep(5 * time.Second)
        // read first data and send it
        sendDataToClient([]byte("dummy data1\n"), ch)
        // read second data and send it
        sendDataToClient([]byte("dummy data2\n"), ch)
        // read third data and send it
        sendDataToClient([]byte("dummy data3\n"), ch)
    }
}

func main() {
    ch := make(chan []byte)
    wg := sync.WaitGroup{}
    wg.Add(2)
    go dummyReadDataRoutine(ch)
    go launchServerUDS(ch)
    wg.Wait()
}

我认为我错过了一些东西,我不想用睡眠来写作,因为我认为这样做不是正确的方法。是否有一些错误,或者更好的方法来做到这一点?唯一必须像我一样保持的是套接字处理和读取数据函数必须在不同的 goroutine 中执行。

【问题讨论】:

  • 你可能没有找到任何搜索,因为你有一堆不相关的问题。始终尝试尽可能地简化来创建您的minimal reproducible example,因为这些都与unix 套接字无关。你有 goroutines 调用 goroutines 无缘无故调用 goroutines。你的 WaitGroup 什么都不做,因为没有 goroutine 调用 Done() (他们也不能,因为它不在他们的范围内)。通过通道发送数据不必要地在一个单独的函数中,在您选择 default 的情况下没有明显的原因。
  • 通道是无缓冲的,你用一个选择和一个不做任何事情的默认值写入通道。要修复您的代码,您应该向通道添加缓冲并删除选择。然后只需将数据写入通道。
  • @JimB 嗨,谢谢你的建议,这个例子对我来说是简化的,因为我的程序比这个大,我在这里添加了等待组只是为了启动例程,所以我没有放无论如何,任何 wg.Done() 出于这个原因,非常感谢您的建议
  • @chmike 我会尝试你的解决方案并编辑我的问题,也谢谢你
  • @Leonardo,留下这样无意义的代码意味着我们无法区分留下不完整的内容和您不理解的内容。 default 的情况会丢失发送数据,但这显然是错误的(就像额外的 goroutine 或未使用的等待组显然是错误的一样),所以我们首先需要上下文来了解它为什么存在。

标签: sockets go concurrency channels writing


【解决方案1】:

JimB 给出了很好的解释,所以我认为他的答案更好。

我在这个答案中包含了我的部分解决方案。

我在想我的代码是清晰和简化的,但正如 Jim 所说,我可以做得更简单、更清晰。我将我的旧代码发布出来,以便人们更好地理解如何发布更简单的代码,而不是像我那样搞得一团糟。

正如 chmike 所说,我的问题与我想的那样与套接字无关,而仅与通道有关。在无缓冲通道上写入是问题之一。将无缓冲通道更改为缓冲通道后,问题得到解决。无论如何,这段代码不是“好代码”,可以按照 JimB 在他的回答中写的原则进行改进。

所以这是新代码:

package main

import (
    "net"
    "os"
    "sync"
    "time"
)

const socketName string = "./test_socket"

// create the socket and accept clients connections
func launchServerUDS(ch chan []byte, wg *sync.WaitGroup) {
    defer wg.Done()
    if err := os.RemoveAll(socketName); err != nil {
        return
    }
    l, err := net.Listen("unix", socketName)
    if err != nil {
        return
    }
    defer l.Close()
    for {
        conn, err := l.Accept()
        if err != nil {
            return
        }
        // this goroutine are launched when a client is connected
        // routine that listen and echo commands
        go commandsHandlerRoutine(conn, ch)
        // routine to send data read from the sensors to the client
        go autoSendRoutine(conn, ch)
    }
}

// routine that sends data to the client
func autoSendRoutine(c net.Conn, ch chan []byte) {
    for {
        data := <-ch
        if string(data) == "exit" {
            return
        }
        c.Write(data)
    }
}

// handle commands received from the client
func commandsHandlerRoutine(c net.Conn, ch chan []byte) {
    for {
        buf := make([]byte, 1024)
        n, err := c.Read(buf)
        if err != nil {
            // if i can't read send an exit command to autoSendRoutine and exit
            ch <- []byte("exit")
            break
        }
        // now, for sake of simplicity , only echo commands back to the client
        _, err = c.Write(buf[:n])
        if err != nil {
            // if i can't write back send an exit command to autoSendRoutine and exit
            ch <- []byte("exit")
            break
        }
    }
}

// this goroutine reads from the sensors and write to the channel , so data are sent
// to the client if a client is connected
func dummyReadDataRoutine(ch chan []byte, wg *sync.WaitGroup) {
    x := 0
    for x < 100 {
        // read data from the sensors every 5 seconds
        time.Sleep(1 * time.Second)
        // read first data and send it
        ch <- []byte("data1\n")
        // read second data and send it
        ch <- []byte("data2\n")
        // read third data and send it
        ch <- []byte("data3\n")
        x++
    }
    wg.Done()
}


func main() {
    // create a BUFFERED CHANNEL
    ch := make(chan []byte, 1)
    wg := sync.WaitGroup{}
    wg.Add(2)
    // launch the goruotines that handle the socket connections
    // and read data from the sensors
    go dummyReadDataRoutine(ch, &wg)
    go launchServerUDS(ch, &wg)
    wg.Wait()
}

【讨论】:

  • 问题不在于无缓冲通道,问题在于select 中的default 导致您在通道未立即准备好时放弃写入该通道的尝试。跨度>
  • @halfer 我希望现在没问题,我很抱歉这些混乱的家伙,感谢所有的帮助
【解决方案2】:

主要问题出在函数中:

func sendDataToClient(data []byte, ch chan []byte) {
    select {
    case ch <- data:
        // if I put a little sleep here, no problems
        // if I remove the sleep, only data1 is sent to the client
        // time.Sleep(1 * time.Millisecond)
    default:
}

如果在调用函数时通道ch 尚未准备好,则将采用default 的情况并且永远不会发送data。这种情况你应该去掉这个函数,直接发送到channel。

缓冲通道与手头的问题正交,并且应该出于与缓冲 IO 类似的原因来完成,即为无法立即进行的写入提供“缓冲区”。如果代码在没有缓冲区的情况下无法进行,那么添加一个只会延迟可能的死锁。

您在这里也不需要exit sentinel 值,因为您可以在通道上进行范围并在完成后关闭它。然而,这仍然会忽略写入错误,但同样需要重新设计。

for data := range ch {
    c.Write(data)
}

您还应该小心在通道上传递切片,因为很容易忘记哪个逻辑进程拥有所有权并且将修改后备数组。我不能从给出的信息中说通过通道传递读+写数据是否会改进架构,但这不是你在大多数 go 网络代码中会发现的模式。

【讨论】:

  • 非常感谢!我很高兴你帮助了我,我想我会在阅读完之后重新审视我的架构
猜你喜欢
  • 2016-08-30
  • 2014-01-30
  • 2018-07-03
  • 2019-10-26
  • 2014-09-26
  • 2018-09-27
  • 2017-07-22
  • 2020-08-09
  • 1970-01-01
相关资源
最近更新 更多