C#多线程场景下如何安全共享Bitmap对象避免访问异常?
问题根因
共享的currentImage、imageWithRectangles两个Bitmap对象未做正确的同步保护:虽然定义了锁对象但未实际使用,且Clone操作本身不是线程安全的,如果Clone执行过程中原Bitmap正在被其他线程读写,依然会抛出GDI+相关异常。
多线程访问共享资源通用规则
- 最小化共享可变资源:尽可能不跨线程共享可修改对象,优先使用局部变量、参数传递的方式传输数据,减少共享场景
- 全路径加锁:所有访问共享可变资源的位置,无论读操作还是写操作,都必须使用同一把锁对象包裹代码块,不能仅为部分访问路径加锁
- 锁内完成安全复制:如果需要将共享对象拿到线程内独立使用,必须在锁的保护下完成对象复制,复制得到的本地副本可在当前线程自由操作,无需额外加锁
- 缩小锁范围:锁内仅保留共享资源读写的核心逻辑,不要将耗时计算、延时、IO操作放入锁内,避免降低并发性能
- UI线程隔离:WinForm控件的所有访问操作必须在创建控件的UI线程执行,禁止在工作线程直接操作UI控件
针对当前代码的修改方案
1. 统一锁对象并规范写入逻辑
新增统一的锁对象保护所有图片资源的读写,写入时主动释放旧Bitmap避免GDI资源泄漏:
// 新增统一锁对象,替换原有的imageWithRectanglesLocker private readonly object _imageLocker = new object(); private void GetCurrentImage() { var sc = new ScreenCapture(); var newCurrent = sc.GetScreenshot(windowHandler); var newImageWithRect = newCurrent.Clone() as Bitmap; lock (_imageLocker) { // 释放旧资源 сurrentImage?.Dispose(); сurrentImage = newCurrent; imageWithRectangles?.Dispose(); imageWithRectangles = newImageWithRect; } }
2. 修正识别线程的读逻辑
在锁内完成共享图片的复制,所有后续操作使用本地副本,同时修复Task.Delay未加await导致的空转问题:
private async void FindCookie(string targetsFolder) { List<string> targetsPaths = ImageHelper.GetImagesFromFolderList(targetsFolder, false); List<Image<Bgr, byte>> targets = new List<Image<Bgr, byte>>(); foreach (string targetsPath in targetsPaths) { targets.Add(new Image<Bgr, byte>(targetsPath)); } while (!clickerCancelationToken.IsCancellationRequested) { // 修复未await的问题 await Task.Delay(300); Bitmap localCurrent = null; lock (_imageLocker) { if (сurrentImage != null) { localCurrent = сurrentImage.Clone() as Bitmap; } } if (localCurrent == null) continue; // 后续仅使用本地副本,不访问共享资源 using (localCurrent) using (var source = localCurrent.ToImage<Bgr, byte>()) { bool isMatchFound = false; Rectangle rectangleToClick = new Rectangle(); Parallel.ForEach(targets, (target, state) => { if (ImageHelper.IsMatching(source, target, out var matchingRectangle)) { rectangleToClick = matchingRectangle; isMatchFound = true; state.Break(); } }); if (isMatchFound) { ClickOnCookie(rectangleToClick); // 绘制矩形时加锁保护共享资源 lock (_imageLocker) { using (var sourceImg = imageWithRectangles.ToImage<Bgr, byte>()) { sourceImg.Draw(rectangleToClick, new Bgr(Color.Red), 3); var newImgWithRect = sourceImg.ToBitmap(); imageWithRectangles.Dispose(); imageWithRectangles = newImgWithRect; } } } } } // 释放模板资源 foreach (var target in targets) { target.Dispose(); } }
3. 修正UI更新逻辑
加锁获取图片副本,同时确保所有UI操作回到UI线程执行:
private void ShowImageWithRectangles() { Bitmap imageToShow = null; lock (_imageLocker) { if (imageWithRectangles != null) { imageToShow = imageWithRectangles.Clone() as Bitmap; } } if (imageToShow == null) return; // 跨线程UI操作调度 if (workViewer.InvokeRequired) { workViewer.Invoke(new Action(() => ShowImageWithRectangles())); imageToShow.Dispose(); return; } workViewer.CopyWindowSize(windowHandler); // 注意:SetPictureBoxImage将图片赋值给PictureBox后不要释放imageToShow,需在PictureBox下次赋值新图片时释放旧图片避免内存泄漏 workViewer.SetPictureBoxImage(imageToShow); }
其他优化建议
- 现有逻辑中截图线程和两个识别线程都是300ms轮询,可改为截图更新后通知识别线程执行,减少无效轮询降低CPU占用
- 可将识别结果放入线程安全队列,由单独的线程统一处理点击和绘制逻辑,进一步降低锁冲突概率
内容的提问来源于stack exchange,提问作者Sar.Dmitry
相关产品推荐
相关产品推荐

