使用图像包并发的奇怪异常行为

Strange anomalous behavior using concurrency with image package

我正在尝试运行的程序是一维细胞自动图像生成器,它需要足够强大以处理数百万个单个细胞数量级的超大型模拟,因此 multi-threading 图像生成过程是必要的。出于这个原因,我选择了 Go,因为 go-routines 将使 CPU 的工作分配问题变得更加容易和高效。现在因为用单独的 go-routine 编写每个单元格根本不会非常高效,所以我决定创建一个函数来调用图像 object 并负责生成一整行单元格。此函数引用一个二维数组 object,其中包含要绘制的所有单元格的位切片 (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 的演化 space (pSize 2)。我们将其作为单线程实现的输出...

现在,如果您放大该图像,您会发现所有正方形在垂直和水平方向上均匀 spaced,没有异常。但是现在让我们看一下并发输出。

这个版本似乎有几个异常,很多行都被 sh运行k 在许多地方有个别像素错误,虽然它正确地遵循了模拟的一般模式,但肯定在视觉上并不令人愉快。当我调查这个问题时,我寻找与并发相关的问题,所以我认为图像包中像素数组的动态分配可能会导致某种冲突,所以我调查了 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 元素将所有像素数据存储在表示颜色的连续一维整数数组中,但是 .Set() 函数立即 returns 如果传递给它的 (x,y) 元素已经在 .Pix 切片中找到。但更奇怪的是,似乎是某种隐式赋值(我在 Go 中从未见过),其中 .Pix 切片的 4 个元素被取出来代表单个像素的颜色并分配给 s。最奇怪的部分是 sc1i 再也不会被引用、返回或存储在内存中,只是被扔到垃圾收集器中。但是不知何故,这个函数似乎是按顺序工作的,所以我只是决定让它做它自己的事情,看看并发和非并发实现之间的 .Pix 切片有什么区别。

现在这里是四个粘贴箱的链接,它们包含 2 个单独试验的 img.Pix objects 数据,每行从左上角开始排列,每行属于一个单独的像素颜色图像并向下移动。进行两次试验的原因是为了验证单线程方法的一致性,这似乎是一致的,但正如您通过访问像 diffchecker.com 这样的网站可以观察到的,多线程测试都显示出它们与单线程之间的差异输出。

Multithreaded Test 1

Single-threaded Test 1

Multithreaded Test 2

Single-threaded Test 2

现在我将分享一些关于这个数据的观察结果。

现在这些观察结果可能意味着,当我们调用 Set 函数时,线程在 Pix 数组中的某些索引上相互冲突,但是从 set 函数来看,每个像素都应该在数组中具有不同的位置它是根据提供的矩形的长度和宽度预先分配的,这应该使线程之间绝对排序和冲突不可能。这是负责创建图像的函数 object...

// 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 访问同一个切片,图像包似乎出现了一些奇怪的行为,但由于切片的索引在理论上是绝对的(意味着每个变量都是唯一的),因此不应该有任何排序问题。我能想到的唯一可能的问题是,尽管切片是以它原来的方式定义的,但它正在以某种方式被该设置函数调整大小,或者至少四处移动导致碰撞。非常感谢任何帮助找出问题所在或任何关于可能导致问题的理论的帮助。干杯!

上面的代码产生了许多竞争冲突,这些冲突是由 go-routines 试图写入 .Pix 对象中的相同像素坐标引起的。修复是在 renderRow 函数中,由于 <= 而不是 '<',当前像素的宽度和高度的计算在每次迭代中重叠。这个故事的寓意是使用 -race 来查找冲突,并始终查找同一变量的覆盖或并发读取。感谢@rustyx。