【发布时间】:2022-01-11 18:14:53
【问题描述】:
我试图开始工作的程序是一维细胞自动图像的生成器,它需要足够强大以处理数百万个单个细胞的订单的超大型模拟,因此多线程图像生成过程是必要的。我之所以选择 Go 是因为 go-routines 将使 CPU 的工作分配问题变得更加容易和高效。现在,因为用单独的 go-routine 编写每个单元格根本不会非常高效,我决定创建一个函数来调用图像对象并负责生成一整行单元格。该函数引用了一个二维数组对象,其中包含要绘制的所有单元格的位切片 (see this) 数组,因此有许多循环,但这对于手头的问题并不重要。程序应该做的是简单地读取所有单独的位并将一个正方形写入图像矩形的正确位置,表示存在一个单元格(基于变量 pSize 表示正方形的边长)。这是那个函数...
func renderRow(wg *sync.WaitGroup, img *image.RGBA, i int, pSize int) {
defer wg.Done()
var lpc = 0
for j := 0; j < 64; j++ {
for k := range sim[i] {
for l := lpc * pSize; l <= (lpc*pSize)+pSize; l++ {
for m := i * pSize; m <= (i*pSize)+pSize; m++ {
if getBit(sim[i][k], j) == 1 {
img.Set(l, m, black)
} else {
img.Set(l, m, white)
}
}
}
lpc++
}
}
}
现在我很高兴地说,这里的函数在一个线程上按顺序运行时按预期执行。这里是非并行函数调用(忽略等待组)
img = image.NewRGBA(image.Rectangle{Min: upLeft, Max: lowRight})
for i := range sim {
renderRow(&wg, img, i, pSize)
}
f, _ := os.Create("export/image.png")
_ = png.Encode(f, img)
现在,另一方面,当我们对并发实现进行简单更改时,输出会出现几个单独的像素错误,并且随着每次运行的错误数量的变化,似乎会随机缩小和扩展某些行。这是并发函数调用。这是并发函数调用...
img = image.NewRGBA(image.Rectangle{Min: upLeft, Max: lowRight})
for i := range sim {
go renderRow(&wg, img, i, pSize) // TODO make multithreaded again
}
wg.Wait()
f, _ := os.Create("export/image.png")
_ = png.Encode(f, img)
现在这两个各自实现的输出是什么样的?
使用这些起始条件{0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1} 和11 (pSize 2) 的演化空间。我们将其作为单线程实现的输出...
现在,如果您放大该图像,您会发现所有正方形都在垂直和水平方向上均匀分布,没有异常。不过现在让我们看看并发输出。
这个版本似乎有几个异常,很多行都被缩小了,很多地方都有单独的像素错误,虽然它正确地遵循了模拟的一般模式,但它肯定在视觉上并不令人愉悦。当我在调查这个问题时,我寻找与并发相关的问题,所以我认为图像包中像素数组的动态分配可能会导致某种冲突,所以我调查了看起来像这样的img.Set()。 .
func (p *NRGBA) Set(x, y int, c color.Color) {
if !(Point{x, y}.In(p.Rect)) {
return
}
i := p.PixOffset(x, y)
c1 := color.NRGBAModel.Convert(c).(color.NRGBA)
s := p.Pix[i : i+4 : i+4] // Small cap improves performance, see https://golang.org/issue/27857
s[0] = c1.R
s[1] = c1.G
s[2] = c1.B
s[3] = c1.A
}
但是,当我看到这个时,它似乎没有任何意义。看起来img.Pix 元素将所有像素数据存储在表示颜色的顺序一维整数数组中,但如果在 .Pix 切片中已经找到传递给它的 (x,y) 元素,.Set() 函数会立即返回.但更奇怪的是,这似乎是某种隐式赋值(这在 Go 中从未见过),其中 .Pix 切片的 4 个元素被取出代表单个像素的颜色并分配给 s。最奇怪的部分是s、c1 和i 再也不会被引用、返回或存储在内存中,只是被扔到垃圾收集器中。但不知何故,这个函数似乎是按顺序工作的,所以我决定让它做它的事情,看看并发和非并发实现之间的.Pix 切片有什么区别。
现在这里是指向四个粘贴箱的链接,它们包含 2 个独立试验的 img.Pix 对象数据,每行属于单个像素的颜色,从每个图像的左上角开始向下移动。进行两次试验的原因是为了验证单线程方法的一致性,这似乎是一致的,但您可以通过访问像 diffchecker.com 这样的网站观察到,多线程测试显示它们之间的差异和单线程输出。
现在我将在这里分享一些关于这些数据的观察。
- 不同的多线程和单线程测试之间存在差异和不同数量的差异
- 在单线程和多线程之间存在相同数量的添加和删除,这意味着所有数据都存在并且只是顺序错误。
现在这些观察结果可能意味着,当我们调用 Set 函数时,线程在 Pix 数组中的某些索引上相互碰撞,但是从查看 set 函数来看,每个像素都应该在数组中具有不同的位置,即根据提供的矩形的长度和宽度预先分配,这应该使线程之间的排序绝对和冲突不可能。这是负责创建图像对象的函数...
// NewRGBA returns a new RGBA image with the given bounds.
func NewRGBA(r Rectangle) *RGBA {
return &RGBA{
Pix: make([]uint8, pixelBufferLength(4, r, "RGBA")),
Stride: 4 * r.Dx(),
Rect: r,
}
}
所以总而言之,我真的不知道发生了什么。由于多个 go-routines 访问同一个切片,图像包似乎产生了一些奇怪的行为,但是由于切片的索引在理论上是绝对的(意味着每个变量都是唯一的),所以不应该有任何排序问题。我能想到的唯一可能的问题是,尽管切片是以某种方式定义的,但它正在以某种方式被该 set 函数调整大小,或者至少四处移动导致碰撞。非常感谢任何帮助找出问题所在或有关可能导致问题的任何理论。干杯!
【问题讨论】:
-
首先验证的最简单的事情是检查竞争检测器,它可以直接指向错误。你确定没有数据竞赛吗? (另外,语言是called Go)
-
@JimB 好的,这里肯定存在数据竞争,这是它们都来自多线程方法controlc.com/996aba72 的输出。知道是什么原因造成的吗? (改为 Go 而不是 golang)
-
在您的示例代码中,您无需等待渲染结束后再将图像写出。看看使用 WaitGroup。另外,对使用比赛检测器 +1。
-
@jdizzle 抱歉,我忘记在此处包含该位。这些程序在
wg的等待条件下运行,该帖子已被编辑以反映这一点 -
有什么方法可以提取 MCVE 以便我们运行并查看?
标签: multithreading image go debugging slice