一个死锁引发的思考

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/phantom_111/article/details/82830879

笔者在转到 golang 之后使用最多的就是 Grpc 的库,这次裸写 tcp 的 client ,由于 client 的 write 阻塞间接导致了代码死锁,在此处记录下。

client write 的分类

写成功

「写成功」指的是 write 调用返回的 n 与预期要写入的数据长度相等,且 error 为 nil 。函数原型如下:

  • func (c *TCPConn) Write(b []byte) (int, error)

写阻塞

tcp 连接建立后操作系统会为该连接保存数据缓冲区,当其中某一端调用 write 后,数据实际上是写入系统的缓冲区中,缓冲区分为发送缓冲区和接收缓冲区。当发送方将对方的接收缓冲区和自己的发送缓冲区均写满后,write 操作就会阻塞。笔者写了一个例子,效果见下图:

在这里插入图片描述

  • server 端在开始的前 10 秒不会从缓冲区中读取任何数据,但 client 端在持续不断的将数据写入缓冲区,在双方的缓冲区均满了以后就会出现上述写阻塞的效果
  • server 端开始以 10s 的固定间隔读取数据,使缓冲区重新进入可写的状态,client 端就可以继续写入数据。

写入部分数据

write 存在发送方写入部分数据后被强制中断的情况,这种情况下接收方收到的就是发送方写入的部分数据,对于写入部分数据的情况接收方需要做特定的处理。

在这里插入图片描述### 写入超时

笔者就是因为上述的写阻塞间接导致代码里产生了一个死锁,大致可描述为 client.Write 操作需要获取上读锁的资源,但是同时存在一个后台的 goroutine 定期的会去获取写锁更新该资源的状态,由于 client.Write 阻塞间接导致读锁的资源不会被释放,导致代码死锁。

解决上述的问题有几个方式:

  • 一个是给 client.Write 操作加上一个超时
  • 一个是在 client 和 server 端使用连接池,一个连接的缓冲区不够大的话,就是用多个呗,三个臭皮匠顶一个诸葛亮(这个大家都会,就不介绍了)
  • server 一个消费者不能跟发送方的生产者匹配的话,也可以使用多个消费者同时消费
    • 需要确认是否是线程安全的
    • tcp 是字节流的多个消费者同时消费是否会导致消费的信息错乱

给 client.Write 操作加上一个超时,就是调用 SetWriteDeadLine方法,在 client.go 的 Write 之前加上一行 timeout 的设置代码:

conn.SetWriteDeadline(time.Now().Add(time.Microsecond * 10))

在这里插入图片描述

测试代码

server

package main

import (
	"fmt"
	"net"
	"time"
)

func handle(conn net.Conn) {
	defer conn.Close()
	for {
		//read data from connection
		time.Sleep(10 * time.Second)
		buf := make([]byte, 65536)
		fmt.Println("begin read data")
		n, err := conn.Read(buf)
		if err != nil {
			fmt.Printf("time %v, conn read %d bytes, error: %s", time.Now().Format(time.RFC3339), n, err)
			continue
		}
		fmt.Printf("time %v, read %d bytes, content is %s\n", time.Now().Format(time.RFC3339), n, string(buf[:n]))

	}

}

func main() {
	l, err := net.Listen("tcp", ":9090")
	if err != nil {
		fmt.Println("error listen:", err)
		return
	}
	fmt.Println("listen success")
	for {
		conn, err := l.Accept()
		if err != nil {
			fmt.Println("error accept", err)
			return
		}
		go handle(conn)
	}
}

client

package main

import (
	"fmt"
	"net"
	"time"
)

func main() {
	conn, err := net.Dial("tcp", ":9090")
	if err != nil {
		fmt.Println("error dial", err)
		return
	}
	defer conn.Close()
	fmt.Println("dial ok")
	data := make([]byte, 65536)
	var total int
	for {
		conn.SetWriteDeadline(time.Now().Add(time.Microsecond * 100))
		n, err := conn.Write(data)
		if err != nil {
			total += n
			fmt.Printf("time %v, write %d bytes, error: %s\n", time.Now().Format(time.RFC3339), n, err)
			break
		}

		total += n
		fmt.Printf("time %v, write %d bytes this time, total bytes is %d\n", time.Now().Format(time.RFC3339), n, total)

	}
}

总结

学习知识重要的是举一反三的能力, write 操作有这么多种情况, 那么 read 操作呢? accept 操作呢?详细解释见参考资料 Go 语言 TCP Socket 编程

参考资料

猜你喜欢

转载自blog.csdn.net/phantom_111/article/details/82830879