Go协程实现矩阵乘法时遇索引越界错误求助
嘿,我来帮你捋捋这个问题哈!你遇到的并行时索引越界、串行就正常的情况,其实是Go协程使用中两个非常常见的坑——循环变量的引用陷阱,还有主协程没等子协程跑完就提前溜了,而且你用defer来“避免等待协程”的思路完全跑偏了,defer和协程同步半毛钱关系都没有哦。
一、为啥会出问题?
1. 循环变量是“共享引用”的坑
Go的for循环里,循环变量(比如你用来遍历行/列的i、j)是复用同一个内存地址的。如果你直接在循环里启动协程,并且在协程里用这些变量,那所有协程都会盯着同一个变量的地址看。等循环快速跑起来,协程真正开始执行的时候,i、j的值早已经变了——比如循环到最后一次,i变成了矩阵的行数,这时候你用这个值去访问矩阵,可不就索引越界了嘛!
举个直观的错误例子:
rows := 3 for i := 0; i < rows; i++ { go func() { // 这里的i是引用,大概率拿到的是3,访问matrix[i]直接越界 fmt.Println(matrix[i]) }() }
2. 主协程没等子协程就跑路了
你说用defer来避免等待协程,这完全是误解了defer的作用——defer只是让函数在当前函数退出前延迟执行,它根本不会阻塞主协程等子协程完成。主协程跑完main函数就直接退出了,这时候所有还在干活的子协程会被强制终止,要是终止前刚好在访问矩阵索引,就很容易因为变量值乱掉触发越界。
二、怎么修复?
针对这两个坑,咱们一步一步来解决:
1. 给协程传循环变量的“副本”
启动协程的时候,把当前i、j的值作为参数传给协程函数,这样每个协程拿到的都是独立的副本,不会被后续循环的变量值影响。
比如你的协程调用可以改成这样:
for i := 0; i < aRows; i++ { for j := 0; j < bCols; j++ { // 把当前的i和j值作为参数传给匿名函数,每个协程拿自己的副本 go func(i, j int) { pmultiply(a, aRows, aCols, b, bRows, bCols, i, j, c) }(i, j) } }
这样每个协程手里的i、j都是启动时的正确值,不会出现越界的情况。
2. 用sync.WaitGroup让主协程等一等
要确保主协程等所有子协程都干完活再退出,咱们得用sync.WaitGroup来做同步:
import "sync" func multiply(a [][]int, b [][]int) [][]int { aRows := len(a) bCols := len(b[0]) // 先初始化结果矩阵 c := make([][]int, aRows) for i := range c { c[i] = make([]int, bCols) } var wg sync.WaitGroup // 先告诉WaitGroup要等多少个协程 wg.Add(aRows * bCols) for i := 0; i < aRows; i++ { for j := 0; j < bCols; j++ { go func(i, j int) { defer wg.Done() // 协程干完活就告诉WaitGroup我完成了 pmultiply(a, aRows, aCols, b, bRows, bCols, i, j, c) }(i, j) } } wg.Wait() // 主协程在这里阻塞,直到所有协程都调用了Done() return c }
这样主协程就会老老实实地等所有子协程都把矩阵乘法的计算做完,再返回结果,不会提前跑路导致子协程被打断。
3. 检查pmultiply内部的索引逻辑
另外,你也得确认下pmultiply函数里的索引计算对不对——比如矩阵乘法要求a的列数等于b的行数,还有计算c[i][j]时的累加循环,有没有出现超出矩阵维度的情况,这也是可能导致越界的小细节。
三、总结一下
你的问题就是Go协程使用中两个经典陷阱的组合,解决起来也很直接:
- 给每个协程传循环变量的副本,避免共享引用踩坑
- 用
sync.WaitGroup同步主协程和子协程,确保所有任务完成再退出 - 核对矩阵维度和内部索引的合法性
内容的提问来源于stack exchange,提问作者drainzerrr

