画像ファイルから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などで非同期処理で使えるかと思います。
追記:20250706
逆に遅くなっていました。
メモリ⇒bitmapオブジェクトの変換(デコード)をDecodeThread()で実行しています。
こちらをCPUのコア分スレッドを立ち上げて並列処理をしていますが、どうやらシングルスレッドで実行した方が速いようです。
原因までは追い切れていませんが、バックエンドの処理がマルチスレッドに対応していないと考えられます。
コードはそのままにしておきますが、以下のループ部分をオミットすることをお勧めします。
変更前
for (int i = 0; i < Environment.ProcessorCount; i++)
{
_workers.Add(Task.Run(() => DecodeThread(_cts.Token)));
}
変更後
_workers.Add(Task.Run(() => DecodeThread(_cts.Token)));
並列処理は純粋にメモリを操作する場合であれば効果がありそうですが、それ以外は逆効果になる可能性があるので使う場合は良く試してからの方が良さそうです。バックエンドで処理するワーカー用のスレッドを1つ立ち上げて、シングルスレッドで順次処理をするくらいにとどめた方が無難そうです。
コメント