1. 程式人生 > >一個死鎖引發的思考

一個死鎖引發的思考

筆者在轉到 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 程式設計

參考資料