一个死锁引发的思考

本文探讨了在Golang中使用TCP进行通信时遇到的写阻塞问题,特别是当客户端持续写入数据而服务器端未能及时处理时,可能导致的死锁情况。文章通过实例分析了写成功、写阻塞及写入部分数据的不同场景,并提出了设置写超时和使用连接池作为解决方案。

笔者在转到 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 编程

参考资料

<think>好的,用户提到“cleanongo死锁”,我需要先理解这个术语的具体含义。首先,“cleanongo”可能是指某个特定的系统、软件或框架中的功能模块,比如清洁流程管理或者某种自动化任务调度系统。由于用户之前的问题涉及代码中的`InitCleanOnGoPMs`方法,可能这个“cleanongo”是相关的一部分,可能与清洁操作或资源管理有关。 接下来,“死锁”是计算机科学中常见的并发问题,当两个或多个进程互相等待对方释放资源时,就会发生死锁。用户可能在使用或开发某个系统时遇到了死锁问题,特别是在与cleanongo相关的功能中。 我需要考虑用户可能的背景。他们可能是开发人员,正在调试一个多线程或分布式系统,遇到了死锁问题,尤其是在初始化清洁操作时。用户需要了解死锁的原因以及如何解决它。 用户的需求可能包括: 1. 理解cleanongo系统中导致死锁的具体原因。 2. 获取解决死锁的方法或步骤。 3. 预防死锁的最佳实践。 可能的深层需求是用户希望确保系统的稳定性和可靠性,避免在关键操作(如清洁流程)中出现死锁,从而影响整体系统运行。他们可能需要具体的解决方案,而不仅仅是理论上的解释。 接下来,我需要组织回答,涵盖死锁的四个必要条件(互斥、持有并等待、非抢占、循环等待),并结合cleanongo的具体场景,比如资源竞争、锁的使用不当等。同时,要提供实际例子,比如代码中的锁机制,如何导致死锁,并给出解决方案,如按顺序获取锁、使用超时机制、资源分级等。 还需要考虑用户可能没有明确提到的点,比如日志记录和监控的重要性,以及如何通过工具检测死锁。此外,用户可能对代码示例感兴趣,比如如何修改之前的`InitCleanOnGoPMs`方法来避免死锁。 最后,确保回答结构清晰,分点列出原因和解决方案,并在末尾提出相关问题,帮助用户进一步思考或验证解决方案的有效性。</think>在涉及清洁流程(如`cleanongo`)的系统中,死锁通常由多线程/多进程间的资源竞争引发。以下是常见原因及解决方案: --- ### **1. 死锁的典型场景** - **资源互斥**:多个线程同时请求清洁所需的独占资源(如腔室、设备锁)。 - **循环等待**:例如,线程A持有腔室1的锁并等待腔室2,线程B持有腔室2的锁并等待腔室1。 - **锁顺序不一致**:代码中未统一锁的获取顺序,导致交叉依赖。 --- ### **2. 结合`cleanongo`的具体分析** 假设`cleanongo`是一个自动化清洁调度系统,死锁可能发生在以下场景: - **场景示例**: 1. 线程1锁定腔室A,尝试获取腔室B的锁以执行联合清洁。 2. 线程2锁定腔室B,尝试获取腔室A的锁。 3. 双方无限等待对方释放锁,导致死锁。 - **代码关联**: 若`InitCleanOnGoPMs`方法中涉及多个锁(如`mCleanOnGoPMs`和`mCleaningOnGoPMs`),且未统一加锁顺序,可能触发循环等待。 --- ### **3. 解决方案** #### **(1) 锁顺序标准化** - **规则**:所有线程必须按固定顺序获取锁(如先腔室A后腔室B)。 - **代码修改**: ```csharp // 统一按腔室ID排序后加锁 foreach (var pm in cleanOnGoPMs.OrderBy(p => p.ID)) { lock (pm.Lock) { /* 操作 */ } } ``` #### **(2) 超时机制** - 使用带超时的锁(如`Monitor.TryEnter`),避免无限等待: ```csharp if (Monitor.TryEnter(lockObj, 5000)) { // 等待5秒 try { /* 操作 */ } finally { Monitor.Exit(lockObj); } } else { Logger.Error("获取锁超时,可能发生死锁!"); } ``` #### **(3) 资源分级** - 将腔室分为不同优先级层级,高优先级资源必须先于低优先级资源加锁。 #### **(4) 死锁检测与恢复** - 定期检查线程是否长时间持有锁,或使用工具(如Visual Studio诊断工具)分析线程状态。 --- ### **4. 预防性设计** - **减少锁粒度**:用细粒度锁(如每个腔室独立锁)替代全局锁。 - **无锁编程**:使用原子操作(如`Interlocked`类)或并发集合(如`ConcurrentQueue`)。 - **日志追踪**:记录锁的获取/释放顺序,便于事后分析。 --- ### **示例:`InitCleanOnGoPMs`的潜在风险** 原代码中同时操作`mCleanOnGoPMs`和`mCleaningOnGoPMs`,若其他线程也在修改这两个列表,可能因锁顺序不一致导致死锁。改进方法: 1. 合并锁对象,或确保所有操作使用同一锁。 2. 使用读写锁(`ReaderWriterLockSlim`)优化并发性能。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值