使用WaitGroup同步仍遇Goroutine执行异常,链表反转触发空指针panic求助
多Goroutine并行反转链表触发nil指针panic的问题与解决
我最近在尝试用多个Goroutine并行反转链表的子部分时遇到了一个棘手的运行时错误,折腾了好几天都没搞定。我的思路是先把原链表拆分成若干不破坏链接的子链表,再给每个子链表分配一个Goroutine去反转,但每次运行都会触发nil指针解引用panic。后来终于找到问题根源了——数据竞争导致的内存损坏,已经用读写锁解决了!
遇到的错误信息
panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x8 pc=0x458db5] goroutine 21 [running]: main.reverse(0xc4200820a0, 0xc420096000, 0xc420098000) /home/user/go/src/local/stackoverflow/tmp.go:69 +0x75 created by main.(*LinkedList).Reverse /home/user/go/src/local/stackoverflow/tmp.go:85 +0x104
原始问题代码
package main import "sync" type node struct { data int next *node } type LinkedList struct { head *node size int } type splitResult struct { beforeHead, head, tail *node } func splitList(head *node, sizoflst, sizofsublst int) <-chan *splitResult { nGoroutines := sizoflst / sizofsublst if sizoflst < sizofsublst { nGoroutines++ } else { if (sizoflst % sizofsublst) >= 6 { nGoroutines++ } } ch := make(chan *splitResult, nGoroutines) go func() { defer close(ch) var beforeHead *node tail := head ct := 0 for i := 0; i < nGoroutines; i++ { for ct < sizofsublst-1 && tail.next != nil { tail = tail.next ct++ } if i == nGoroutines-1 { testTail := tail for testTail.next != nil { testTail = testTail.next } ch <- &splitResult{beforeHead, head, testTail} break } ch <- &splitResult{beforeHead, head, tail} beforeHead = tail head = tail.next tail = head ct = 0 } }() return ch } func reverse(split *splitResult, ln **node, wg *sync.WaitGroup) { defer wg.Done() move := split.head prev := split.beforeHead if split.tail.next == nil { *ln = split.tail } for move != split.tail.next { temp := move.next move.next = prev prev = move move = temp } } func (ll *LinkedList) Reverse(sizofsublst int) { var lastNode *node var wg sync.WaitGroup if ll.head == nil || ll.head.next == nil { return } splitCh := splitList(ll.head, ll.size, sizofsublst) for split := range splitCh { wg.Add(1) go reverse(split, &lastNode, &wg) } wg.Wait() ll.head = lastNode } func (ll *LinkedList) Insert(data int) { newNode := new(node) newNode.data = data newNode.next = ll.head ll.head = newNode ll.size++ } func main() { ll := &LinkedList{} sli := []int{19, 30, 7, 23, 24, 0, 12, 28, 3, 11, 18, 1, 31, 14, 21, 2, 9, 16, 4, 26, 10, 25} for _, v := range sli { ll.Insert(v) } ll.Reverse(8) }
问题根源分析
原代码的核心问题在于无同步的并发读写:多个Goroutine在reverse函数中同时操作lastNode指针——当某个子链表是最后一段时,会执行*ln = split.tail,这个操作没有任何锁保护,多个Goroutine可能同时读写该变量,导致内存访问冲突,进而引发nil指针解引用或其他内存损坏问题。
另外代码还存在几处潜在的nil指针风险:比如遍历链表时没有判断当前节点是否为nil,可能在链表提前结束时触发访问错误。
改进后的代码(解决数据竞争+修复nil指针问题)
package main import ( "sync" ) type node struct { data int next *node } type LinkedList struct { head *node size int mu sync.RWMutex // 保护链表的并发读写 } type splitResult struct { beforeHead, head, tail *node } func splitList(head *node, sizoflst, sizofsublst int) <-chan *splitResult { nGoroutines := sizoflst / sizofsublst if sizoflst < sizofsublst { nGoroutines++ } else { if (sizoflst % sizofsublst) >= 6 { nGoroutines++ } } ch := make(chan *splitResult, nGoroutines) go func() { defer close(ch) var beforeHead *node tail := head ct := 0 for i := 0; i < nGoroutines; i++ { // 增加tail非空判断,避免nil访问 for ct < sizofsublst-1 && tail != nil && tail.next != nil { tail = tail.next ct++ } if i == nGoroutines-1 { testTail := tail // 增加testTail非空判断 for testTail != nil && testTail.next != nil { testTail = testTail.next } ch <- &splitResult{beforeHead, head, testTail} break } // 确保子链表节点非空再发送 if head != nil && tail != nil { ch <- &splitResult{beforeHead, head, tail} } beforeHead = tail if tail != nil { head = tail.next tail = head } else { break // 链表已遍历完,提前退出循环 } ct = 0 } }() return ch } func reverse(split *splitResult, ln **node, wg *sync.WaitGroup, mu *sync.Mutex) { defer wg.Done() // 空值判断,避免无效操作 if split == nil || split.head == nil { return } move := split.head prev := split.beforeHead // 操作lastNode时加锁,避免并发读写冲突 mu.Lock() if split.tail != nil && split.tail.next == nil { *ln = split.tail } mu.Unlock() // 增加move非空判断 for move != nil && move != split.tail.next { temp := move.next move.next = prev prev = move move = temp } // 修复子链表连接:把反转后的子链表和前一段连接起来 if prev != nil && split.beforeHead != nil { split.beforeHead.next = prev } } func (ll *LinkedList) Reverse(sizofsublst int) { var lastNode *node var wg sync.WaitGroup var mu sync.Mutex // 专门保护lastNode的读写 // 加锁保护整个反转过程中的链表访问 ll.mu.Lock() defer ll.mu.Unlock() if ll.head == nil || ll.head.next == nil { return } splitCh := splitList(ll.head, ll.size, sizofsublst) for split := range splitCh { wg.Add(1) go reverse(split, &lastNode, &wg, &mu) } wg.Wait() ll.head = lastNode } func (ll *LinkedList) Insert(data int) { ll.mu.Lock() defer ll.mu.Unlock() newNode := new(node) newNode.data = data newNode.next = ll.head ll.head = newNode ll.size++ } func main() { ll := &LinkedList{} sli := []int{19, 30, 7, 23, 24, 0, 12, 28, 3, 11, 18, 1, 31, 14, 21, 2, 9, 16, 4, 26, 10, 25} for _, v := range sli { ll.Insert(v) } ll.Reverse(8) // 可添加打印链表的代码验证反转结果 }
优化说明
- 解决数据竞争:用
sync.Mutex保护lastNode的读写操作,确保同一时间只有一个Goroutine修改它;给LinkedList添加读写锁,保护Insert和Reverse方法中的链表访问。 - 修复nil指针风险:在遍历链表和操作节点的关键位置增加了非空判断,避免因链表提前结束导致的panic。
- 完善链表连接:在
reverse函数末尾添加了反转后子链表与前一段的连接逻辑,确保整个链表反转后是完整的(原代码缺失此逻辑,会导致链表断裂)。
内容的提问来源于stack exchange,提问作者jerryL
相关产品推荐
相关产品推荐

