You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.23 23:45:06