C# WinFormsで画像読み込みをバックグラウンド処理する(スレッド分離と非同期実装)

コンピュータ

画像ファイルからBitmapオブジェクト取得するコードなのですが、ファイルの読み込みとデコードを別スレッドで行いたいと思い作りました。
スレッドセーフなキューに画像ファイルのパスと、結果を処理するコールバックのコードを渡すと、処理されるようなコードになっています。

ファイル名:ImageProcessor.cs

using System.Collections.Concurrent;
using System.Diagnostics;
class ImageProcessor: IDisposable
{
    private readonly BlockingCollection<(string path, Func<Bitmap, Task> callback)> _fileQueue = new();
    private readonly BlockingCollection<(MemoryStream ms, Func<Bitmap, Task> callback)> _imageQueue = new();
    private readonly CancellationTokenSource _cts = new();
    private readonly List<Task> _workers = [];

    public void EnqueueFile(string path, Func<Bitmap, Task> callback)
        => _fileQueue.Add((path, callback));

    public void Start()
    {
        _workers.Add(Task.Run(() => ReaderThread(_cts.Token)));
        for (int i = 0; i < Environment.ProcessorCount; i++)
        {
            _workers.Add(Task.Run(() => DecodeThread(_cts.Token)));
        }
    }

    public void Stop()
    {
        _fileQueue.CompleteAdding();
        _imageQueue.CompleteAdding();
        _cts.Cancel();
    }

    public void Dispose()
    {
        Stop();
        Task.WaitAll(_workers.ToArray(), TimeSpan.FromSeconds(5));
        _cts.Dispose();
        _fileQueue.Dispose();
        _imageQueue.Dispose();
    }

    private async Task ReaderThread(CancellationToken token)
    {
        foreach (var (path, callback) in _fileQueue.GetConsumingEnumerable(token))
        {
            try
            {
                using var fs = new FileStream(path, FileMode.Open, FileAccess.Read);
                // zip対応予定
                var ms = new MemoryStream();
                await fs.CopyToAsync(ms, token);
                ms.Seek(0, SeekOrigin.Begin);
                _imageQueue.Add((ms, callback), token);
            }
            catch (Exception ex)
            {
                Debug.Print($"[Read] {path} => {ex.Message}");
                throw;
            }
        }
    }

    private void DecodeThread(CancellationToken token)
    {
        try
        {
            foreach (var (ms, callback) in _imageQueue.GetConsumingEnumerable(token))
            {
                try
                {
                    using var clone = new MemoryStream(ms.ToArray());
                    using var bmp = new Bitmap(clone);
                    callback(bmp).Wait(token);
                }
                catch (Exception ex)
                {
                    Debug.Print($"[Decode] {ex.Message}");
                    throw;
                }
                finally
                {
                    ms.Dispose();
                }
            }
        }
        catch (OperationCanceledException)
        {
            // 正常終了(ログなしでもOK)
        }
    }
}

ファイル名:Form1.cs


namespace GraphicUtilitesLib;

public partial class Form1 : Form
{
    // イメージプロセッサ
    ImageProcessor _imageProcessor = new();

    PictureBox picbox1 = new()
    {
        Dock = DockStyle.Fill,
        SizeMode = PictureBoxSizeMode.Zoom,
    };

    // コンストラクタ
    public Form1()
    {
        InitializeComponent();

        this.Controls.Add(picbox1);

        _imageProcessor.Start();
    }
    // フォームロード
    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);

        

        _imageProcessor.EnqueueFile(@"C:\Users\karet\Pictures\2025-06-28105457.png", async bmp =>
        {
            this.Invoke(() =>
            {
                picbox1.Image?.Dispose(); // 古い画像を破棄(メモリリーク防止)
                picbox1.Image = (Bitmap)bmp.Clone(); // Cloneして所有権を移す
            });
            await Task.Delay(10);
        });

    }
    // フォームクローズド
    protected override void OnClosed(EventArgs e)
    {
        base.OnClosed(e);

        _imageProcessor.Dispose();
    }
}

ここまでやっても、速くなるわけでも無いのがC#の辛いところですが、GUIなどで非同期処理で使えるかと思います。

コメント