How to Use Async and Multithreading for QR Code Operations in C#
Single-threaded QR scanning blocks the calling thread for the duration of every image decode.
In a WPF button handler, this freezes the UI until decoding completes. In a batch job processing hundreds of images, it leaves CPU cores idle when they could be working in parallel. IronQR's ReadAsync method offloads individual reads to an awaitable task, and the standard Read method works with Parallel.ForEach and Task.WhenAll for batch throughput.
This guide demonstrates how to process QR codes asynchronously, distribute batch reads across CPU cores, and combine both patterns for high-volume pipelines.
Quickstart: Process QR Codes Asynchronously
Load an image and await the decoded results without blocking the calling thread.
-
Install IronQR with NuGet Package Manager
PM > Install-Package IronQR -
Copy and run this code snippet.
using IronQr; using IronSoftware.Drawing; var input = new QrImageInput(AnyBitmap.FromFile("ticket.png")); IEnumerable<QrResult> results = await new QrReader().ReadAsync(input); Console.WriteLine(results.First().Value); -
Deploy to test on your live environment
Start using IronQR in your project today with a free trial
Minimal Workflow (5 steps)
- Download the IronQR C# library for async QR code processing
- Use
ReadAsyncfor non-blocking single reads - Use
Parallel.ForEachfor CPU-bound batch processing - Combine with
SemaphoreSlimfor bounded concurrency pipelines - Collect results from
IEnumerable<QrResult>and print the decoded values
Reading QR Codes Asynchronously
ReadAsync returns an awaitable task, making it compatible with WPF/MAUI event handlers, ASP.NET controller actions, or any async method. The input must be constructed from an image bitmap; there is no file-path overload.
The write side is synchronous and has no async variants. To avoid blocking the thread during file I/O, wrap the save step in File.WriteAllBytesAsync() using the raw bytes exported from the bitmap.
Input
A QR code event badge scanned and regenerated to demonstrate the async read-and-write pattern.
:path=/static-assets/qr/content-code-examples/how-to/async-and-multithreading/async-read-write.cs
using IronQr;
using IronQr.Enum;
using IronSoftware.Drawing;
// --- Async read: non-blocking QR decode ---
var inputBmp = AnyBitmap.FromFile("event-badge.png");
var imageInput = new QrImageInput(inputBmp, QrScanMode.OnlyDetectionModel);
var reader = new QrReader();
IEnumerable<QrResult> results = await reader.ReadAsync(imageInput);
foreach (QrResult result in results)
{
Console.WriteLine($"[{result.QrType}] {result.Value}");
}
// --- Async-wrapped save: QrWriter.Write() and QrCode.Save() are synchronous ---
QrCode qrCode = QrWriter.Write("https://ironsoftware.com");
AnyBitmap qrImage = qrCode.Save();
// Save the bitmap bytes asynchronously (not an IronQR API — standard .NET async I/O)
byte[] pngBytes = qrImage.ExportBytes();
await File.WriteAllBytesAsync("output-qr.png", pngBytes);
Imports IronQr
Imports IronQr.Enum
Imports IronSoftware.Drawing
Imports System.IO
' --- Async read: non-blocking QR decode ---
Dim inputBmp = AnyBitmap.FromFile("event-badge.png")
Dim imageInput = New QrImageInput(inputBmp, QrScanMode.OnlyDetectionModel)
Dim reader = New QrReader()
Dim results As IEnumerable(Of QrResult) = Await reader.ReadAsync(imageInput)
For Each result As QrResult In results
Console.WriteLine($"[{result.QrType}] {result.Value}")
Next
' --- Async-wrapped save: QrWriter.Write() and QrCode.Save() are synchronous ---
Dim qrCode As QrCode = QrWriter.Write("https://ironsoftware.com")
Dim qrImage As AnyBitmap = qrCode.Save()
' Save the bitmap bytes asynchronously (not an IronQR API — standard .NET async I/O)
Dim pngBytes As Byte() = qrImage.ExportBytes()
Await File.WriteAllBytesAsync("output-qr.png", pngBytes)
Output
The terminal displays the decoded QR type and value in the format [QrType] Value, then confirms that output-qr.png was saved.
QrScanMode.Auto runs both ML detection and a basic scan pass, populating the decoded value and QR type in each result. OnlyDetectionModel is faster but returns bounding box coordinates only, leaving the value field empty. Use Auto whenever the encoded content is needed.
Processing QR Codes with Multithreading
For images that can be decoded independently, Parallel.ForEach distributes work across available CPU cores. A separate QrReader instance per iteration is the safe default, as IronQR makes no explicit thread-safety guarantee for shared reader instances.
Input
Four of the ten QR code test images used in the parallel batch scan. Each image encodes a URL and is read from the qr-images/ folder at runtime.
Image 1 (Batch 1 of 10)
Image 2 (Batch 2 of 10)
Image 3 (Batch 3 of 10)
Image 4 (Batch 4 of 10)
:path=/static-assets/qr/content-code-examples/how-to/async-and-multithreading/parallel-batch.cs
using IronQr;
using IronQr.Enum;
using IronSoftware.Drawing;
using System.Collections.Concurrent;
using System.Diagnostics;
string[] files = Directory.GetFiles("qr-images/", "*.png");
var allResults = new ConcurrentBag<(string File, string Value)>();
int failCount = 0;
var sw = Stopwatch.StartNew();
Parallel.ForEach(files, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }, file =>
{
try
{
var input = new QrImageInput(
AnyBitmap.FromFile(file),
QrScanMode.OnlyDetectionModel);
// Per-thread QrReader instance — safe default
var results = new QrReader().Read(input);
foreach (QrResult result in results)
{
allResults.Add((Path.GetFileName(file), result.Value));
}
}
catch (Exception ex)
{
Interlocked.Increment(ref failCount);
Console.Error.WriteLine($"[ERROR] {Path.GetFileName(file)}: {ex.Message}");
}
});
sw.Stop();
Console.WriteLine($"Processed {files.Length} files in {sw.Elapsed.TotalSeconds:F1}s");
Console.WriteLine($"QR codes found: {allResults.Count} | Failures: {failCount}");
Console.WriteLine($"Throughput: {files.Length / sw.Elapsed.TotalSeconds:F1} files/sec");
Imports IronQr
Imports IronQr.Enum
Imports IronSoftware.Drawing
Imports System.Collections.Concurrent
Imports System.Diagnostics
Dim files As String() = Directory.GetFiles("qr-images/", "*.png")
Dim allResults As New ConcurrentBag(Of (File As String, Value As String))()
Dim failCount As Integer = 0
Dim sw As Stopwatch = Stopwatch.StartNew()
Parallel.ForEach(files, New ParallelOptions With {.MaxDegreeOfParallelism = Environment.ProcessorCount}, Sub(file)
Try
Dim input As New QrImageInput(AnyBitmap.FromFile(file), QrScanMode.OnlyDetectionModel)
' Per-thread QrReader instance — safe default
Dim results = New QrReader().Read(input)
For Each result As QrResult In results
allResults.Add((Path.GetFileName(file), result.Value))
Next
Catch ex As Exception
Interlocked.Increment(failCount)
Console.Error.WriteLine($"[ERROR] {Path.GetFileName(file)}: {ex.Message}")
End Try
End Sub)
sw.Stop()
Console.WriteLine($"Processed {files.Length} files in {sw.Elapsed.TotalSeconds:F1}s")
Console.WriteLine($"QR codes found: {allResults.Count} | Failures: {failCount}")
Console.WriteLine($"Throughput: {files.Length / sw.Elapsed.TotalSeconds:F1} files/sec")
Output
The console displays a batch summary, including the number of files processed, processing time, QR codes found, any failures, and throughput. It then lists each filename with its decoded URL.
Download all 10 test batch QR code input images (batch-qr-images.zip).
ConcurrentBag<T> gathers results from all threads without requiring locks. A thread-safe counter tracks failures, and using try-catch for each file ensures that one bad image does not interrupt the entire batch. This approach follows the error-isolation pattern described in the error handling how-to.
Set MaxDegreeOfParallelism to Environment.ProcessorCount to align with the number of CPU cores. Using additional threads increases overhead and does not improve performance, particularly for CPU-intensive ML models.
Combining Async and Parallel Processing
For high-volume pipelines, pair SemaphoreSlim with Task.WhenAll to bound concurrency. Unlike Parallel.ForEach, this pattern keeps I/O non-blocking while controlling how many decodes run at once, preventing thread pool saturation under large workloads.
Input
Four of the twenty QR code test images processed by the concurrent pipeline. Each image encodes a URL and is decoded in parallel using bounded concurrency via SemaphoreSlim.
Image 1 (Pipeline 1 of 20)
Image 2 (Pipeline 2 of 20)
Image 3 (Pipeline 3 of 20)
Image 4 (Pipeline 4 of 20)
:path=/static-assets/qr/content-code-examples/how-to/async-and-multithreading/semaphore-pipeline.cs
using IronQr;
using IronQr.Enum;
using IronSoftware.Drawing;
using System.Collections.Concurrent;
using System.Diagnostics;
string[] files = Directory.GetFiles("high-volume/", "*.png");
var results = new ConcurrentBag<(string File, string Value)>();
int maxConcurrency = Environment.ProcessorCount;
using var semaphore = new SemaphoreSlim(maxConcurrency);
var sw = Stopwatch.StartNew();
var tasks = files.Select(async file =>
{
await semaphore.WaitAsync();
try
{
var bmp = AnyBitmap.FromFile(file);
// OnlyDetectionModel: fastest per-image — critical at scale
var input = new QrImageInput(bmp, QrScanMode.OnlyDetectionModel);
var qrResults = await new QrReader().ReadAsync(input);
foreach (var qr in qrResults)
{
results.Add((Path.GetFileName(file), qr.Value));
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"{{\"file\":\"{Path.GetFileName(file)}\",\"error\":\"{ex.Message}\"}}");
}
finally
{
semaphore.Release();
}
});
await Task.WhenAll(tasks);
sw.Stop();
Console.WriteLine($"Pipeline complete: {results.Count} QR codes from {files.Length} files in {sw.Elapsed.TotalSeconds:F1}s");
Imports IronQr
Imports IronQr.Enum
Imports IronSoftware.Drawing
Imports System.Collections.Concurrent
Imports System.Diagnostics
Module Program
Sub Main()
Dim files As String() = Directory.GetFiles("high-volume/", "*.png")
Dim results As New ConcurrentBag(Of (File As String, Value As String))()
Dim maxConcurrency As Integer = Environment.ProcessorCount
Using semaphore As New SemaphoreSlim(maxConcurrency)
Dim sw As Stopwatch = Stopwatch.StartNew()
Dim tasks = files.Select(Function(file) Task.Run(Async Function()
Await semaphore.WaitAsync()
Try
Dim bmp = AnyBitmap.FromFile(file)
' OnlyDetectionModel: fastest per-image — critical at scale
Dim input As New QrImageInput(bmp, QrScanMode.OnlyDetectionModel)
Dim qrResults = Await (New QrReader()).ReadAsync(input)
For Each qr In qrResults
results.Add((Path.GetFileName(file), qr.Value))
Next
Catch ex As Exception
Console.Error.WriteLine($"{{""file"":""{Path.GetFileName(file)}"",""error"":""{ex.Message}""}}")
Finally
semaphore.Release()
End Try
End Function))
Task.WhenAll(tasks).Wait()
sw.Stop()
Console.WriteLine($"Pipeline complete: {results.Count} QR codes from {files.Length} files in {sw.Elapsed.TotalSeconds:F1}s")
End Using
End Sub
End Module
Output
The console displays a summary when the pipeline finishes: total QR codes decoded, number of source files, and elapsed time, followed by each filename and its decoded URL.
Download all 20 test pipeline QR code input images (high-volume-qr-images.zip).
Match the semaphore limit to the available core count for throughput, or lower it when memory pressure is a concern with large images.
Further Reading
- ML Scanning Example: scan mode comparison with code samples.
- Reading QR Codes How-To: input construction and basic read patterns.
- QR Code Generator Tutorial: generation with styling.
- QrReader API Reference: method signatures and remarks.
- QrWriter API Reference: all write overloads.
- Error Handling How-To: per-file error isolation and logging patterns.
View licensing options when the pipeline is ready for production.

