Golang中image/draw是否线程安全?

Golang中image/draw是否线程安全?

// CompositeImage numSizeRatio is num + '#' size_ratio
func CompositeImage(numSizeRatio string, imageUrls []string) ([]byte, error) {
	compositeImageParameter := CompositeImageParameterMap[numSizeRatio]
	basemap := image.NewRGBA(image.Rect(0, 0, compositeImageParameter.BaseImageWidth, compositeImageParameter.BaseImageHeight))
	var wg sync.WaitGroup
	for i := 0; i < len(imageUrls); i++ {
		wg.Add(1)
		go func(i int) {
			defer func() {
				if err := recover(); err != nil {
					log.Errorf("panic stack: %s", string(debug.Stack()))
					log.Error("CompositeImage panic:", err)
				}
				wg.Done()
			}()
			imageBytes, err := util.GetRaw(imageUrls[i])
			if err != nil {
				log.Errorf("GetRaw image %d error: %v", i, err)
				return
			}

			buffer := bytes.NewBuffer(imageBytes)
			imageDecode, err := webp.Decode(buffer)
			if err != nil {
				imageDecode, _, err = image.Decode(bytes.NewBuffer(imageBytes))
				if err != nil {
					log.Errorf("Decode image %d error: %v", i, err)
					return
				}
			}
			draw.Draw(basemap, image.Rect(compositeImageParameter.Offset[i][0], compositeImageParameter.Offset[i][1], compositeImageParameter.Offset[i][2], compositeImageParameter.Offset[i][3]),
				imageDecode, imageDecode.Bounds().Min, draw.Over)
		}(i)
	}
	wg.Wait()

	outputImageBytes := bytes.NewBuffer([]byte{})
	if err := png.Encode(outputImageBytes, basemap); err != nil {
		return nil, errors.Wrap(err, "png Encode error")
	}

	return outputImageBytes.Bytes(), nil
}

我使用了 go test -race 进行测试,没有发现错误。


更多关于Golang中image/draw是否线程安全?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

我不会假设它是安全的,除非文档明确指出这一点,或者你已经检查了源代码以确认它具备适当的同步机制。

竞态检测器只能发现运行时发生的竞态条件

你的 goroutine 中包含许多其他耗时的操作,因此 Draw 没有被同时调用可能只是运气好。考虑到你涉及 I/O 和内存分配操作,无论如何你都无法获得太多的并行性。你的多个 goroutine 主要有助于实现异步 I/O。因此,为了安全起见,请将你的 Draw 函数放在另一个独立的 goroutine 中,并通过一个 chan 向其发送数据(前三条 Go 谚语与此相关:https://go-proverbs.github.io/),或者使用互斥锁来保护它。

更多关于Golang中image/draw是否线程安全?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


image/draw 包中的 Draw 函数不是线程安全的。在你的代码中,多个 goroutine 并发地对同一个 basemap 进行绘制操作,这会导致数据竞争。虽然 go test -race 可能没有立即报告错误,但并发写入同一图像内存区域是不安全的。

下面是修复后的线程安全版本:

func CompositeImage(numSizeRatio string, imageUrls []string) ([]byte, error) {
	compositeImageParameter := CompositeImageParameterMap[numSizeRatio]
	basemap := image.NewRGBA(image.Rect(0, 0, compositeImageParameter.BaseImageWidth, compositeImageParameter.BaseImageHeight))
	
	// 使用互斥锁保护对 basemap 的并发访问
	var mu sync.Mutex
	var wg sync.WaitGroup
	errCh := make(chan error, len(imageUrls))
	
	for i := 0; i < len(imageUrls); i++ {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			
			imageBytes, err := util.GetRaw(imageUrls[idx])
			if err != nil {
				errCh <- fmt.Errorf("GetRaw image %d error: %v", idx, err)
				return
			}

			buffer := bytes.NewBuffer(imageBytes)
			imageDecode, err := webp.Decode(buffer)
			if err != nil {
				imageDecode, _, err = image.Decode(bytes.NewBuffer(imageBytes))
				if err != nil {
					errCh <- fmt.Errorf("Decode image %d error: %v", idx, err)
					return
				}
			}
			
			// 加锁执行绘制操作
			mu.Lock()
			draw.Draw(basemap, 
				image.Rect(
					compositeImageParameter.Offset[idx][0],
					compositeImageParameter.Offset[idx][1],
					compositeImageParameter.Offset[idx][2],
					compositeImageParameter.Offset[idx][3],
				),
				imageDecode, 
				imageDecode.Bounds().Min, 
				draw.Over,
			)
			mu.Unlock()
		}(i)
	}
	
	wg.Wait()
	close(errCh)
	
	// 检查是否有错误发生
	for err := range errCh {
		if err != nil {
			log.Error(err)
		}
	}

	outputImageBytes := bytes.NewBuffer([]byte{})
	if err := png.Encode(outputImageBytes, basemap); err != nil {
		return nil, fmt.Errorf("png Encode error: %w", err)
	}

	return outputImageBytes.Bytes(), nil
}

或者,如果每个 goroutine 绘制的是图像的不同区域(没有重叠),可以使用更细粒度的锁:

func CompositeImage(numSizeRatio string, imageUrls []string) ([]byte, error) {
	compositeImageParameter := CompositeImageParameterMap[numSizeRatio]
	basemap := image.NewRGBA(image.Rect(0, 0, compositeImageParameter.BaseImageWidth, compositeImageParameter.BaseImageHeight))
	
	// 为每个绘制区域创建独立的锁
	locks := make([]sync.Mutex, len(imageUrls))
	var wg sync.WaitGroup
	
	for i := 0; i < len(imageUrls); i++ {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			
			imageBytes, err := util.GetRaw(imageUrls[idx])
			if err != nil {
				log.Errorf("GetRaw image %d error: %v", idx, err)
				return
			}

			buffer := bytes.NewBuffer(imageBytes)
			imageDecode, err := webp.Decode(buffer)
			if err != nil {
				imageDecode, _, err = image.Decode(bytes.NewBuffer(imageBytes))
				if err != nil {
					log.Errorf("Decode image %d error: %v", idx, err)
					return
				}
			}
			
			// 只锁定当前绘制的区域
			locks[idx].Lock()
			draw.Draw(basemap, 
				image.Rect(
					compositeImageParameter.Offset[idx][0],
					compositeImageParameter.Offset[idx][1],
					compositeImageParameter.Offset[idx][2],
					compositeImageParameter.Offset[idx][3],
				),
				imageDecode, 
				imageDecode.Bounds().Min, 
				draw.Over,
			)
			locks[idx].Unlock()
		}(i)
	}
	
	wg.Wait()

	outputImageBytes := bytes.NewBuffer([]byte{})
	if err := png.Encode(outputImageBytes, basemap); err != nil {
		return nil, fmt.Errorf("png Encode error: %w", err)
	}

	return outputImageBytes.Bytes(), nil
}

关键点:image/draw.Draw() 会修改目标图像的像素数据,当多个 goroutine 同时操作同一个图像时,必须使用同步机制来保护这些操作。

回到顶部