You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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)
	// 可添加打印链表的代码验证反转结果
}

优化说明

  1. 解决数据竞争:用sync.Mutex保护lastNode的读写操作,确保同一时间只有一个Goroutine修改它;给LinkedList添加读写锁,保护Insert和Reverse方法中的链表访问。
  2. 修复nil指针风险:在遍历链表和操作节点的关键位置增加了非空判断,避免因链表提前结束导致的panic。
  3. 完善链表连接:在reverse函数末尾添加了反转后子链表与前一段的连接逻辑,确保整个链表反转后是完整的(原代码缺失此逻辑,会导致链表断裂)。

内容的提问来源于stack exchange,提问作者jerryL

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 06:49:18