一個死鎖引發的思考
阿新 • • 發佈:2018-12-11
筆者在轉到 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 程式設計