如何在 C# 中使用 Async 和多執行緒進行 QR code 掃描與處理

This article was translated from English: Does it need improvement?
Translated
View the article in English

單執行緒 QR 碼掃描會在每次影像解碼期間阻塞呼叫執行緒。

在 WPF 按鈕處理程序中,這將凍結 UI,直到解碼完成。 在批量處理數百張圖像時,CPU 核心會閒置,而它們本可以並行工作。 IronQR 的 ReadAsync 方法將單一讀取操作卸載到可等待的任務中,而標準的 Read 方法與 Parallel.ForEachTask.WhenAll 使用,以實現批量匹配。

本指南示範如何非同步處理二維碼、將批次讀取任務分配到 CPU 核心上,以及如何將這兩種模式結合起來以實現高容量管線。

快速入門:異步處理 QR 碼

載入圖像並等待解碼結果,而不阻塞呼叫線程。

  1. 使用NuGet套件管理器安裝https://www.nuget.org/packages/IronQR

    PM > Install-Package IronQR
  2. 複製並運行這段程式碼。

    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);
  3. 部署到您的生產環境進行測試

    今天就在您的專案中開始使用免費試用IronQR

    arrow pointer

非同步讀取二維碼

ReadAsync 傳回一個可等待的任務,使其與 WPF/MAUI 事件處理程序、 ASP.NET控制器操作或任何非同步方法相容。 輸入必須由影像點陣圖構成;不支援檔案路徑重載。

寫入端是同步的,沒有非同步變體。 為了避免在檔案 I/O 期間阻塞線程,請使用從點陣圖匯出的原始位元組將保存步驟包裝在 File.WriteAllBytesAsync() 中。

輸入

掃描並重新產生二維碼活動徽章,以演示非同步讀寫模式。

二維碼編碼 https://ironsoftware.com/event-badge 用作非同步讀取輸入
: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)
$vbLabelText   $csharpLabel

輸出

終端以 [QrType] Value 的格式顯示解碼後的二維碼類型和值,然後確認 output-qr.png 已儲存。

終端輸出顯示 [QRCode] 解碼值和 output-qr.png 儲存確認

QrScanMode.Auto 執行 ML 偵測和基本掃描過程,並在每個結果中填入解碼值和 QR 類型。 OnlyDetectionModel 速度較快,但僅傳回邊界框座標,值欄位為空。 需要編碼內容時,請使用 Auto


使用多線程處理二維碼

對於可以獨立解碼的圖像,Parallel.ForEach 將工作分配到可用的 CPU 核心。 每次迭代使用單獨的 QrReader 實例是安全的預設做法,因為IronQR沒有對共享的讀取器實例做出明確的線程安全保證。

輸入

並行批次掃描中使用的十個二維碼測試影像中有四個。 每張圖片都編碼了一個 URL,並在運行時從 qr-images/ 資料夾中讀取。

Batch QR code input image 1 of 10 encoding https://ironsoftware.com/batch-1
Batch QR code input image 2 of 10 encoding https://ironsoftware.com/batch-2
Batch QR code input image 3 of 10 encoding https://ironsoftware.com/batch-3
Batch QR code input image 4 of 10 encoding https://ironsoftware.com/batch-4
: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")
$vbLabelText   $csharpLabel

輸出

控制台顯示批次摘要,包括已處理的檔案數、處理時間、找到的二維碼、任何故障和吞吐量。 然後它會列出每個檔案名稱及其解碼後的 URL。

終端輸出顯示平行批次結果:已處理 10 個文件,已找到二維碼,存在失敗,吞吐量為 [數量],每個文件名已解碼 URL

下載全部 10 個測試批次的二維碼輸入影像(batch-qr-images.zip)。

ConcurrentBag<t> 從所有執行緒收集結果,而無需加鎖。 線程安全的計數器會追蹤故障,並且對每個檔案使用 try-catch 可以確保一個錯誤的映像不會中斷整個批次。 這種方法遵循錯誤處理指南中所述的錯誤隔離模式。

MaxDegreeOfParallelism 設定為 Environment.ProcessorCount,以與 CPU 核心數保持一致。 使用額外的執行緒會增加開銷,並且不會提高效能,特別是對於 CPU 密集型機器學習模型而言。


結合異步和平行處理

對於高容量管道,將 SemaphoreSlimTask.WhenAll 配對,以限制並發性。 與 Parallel.ForEach 不同,這種模式保持 I/O 非阻塞,同時控制一次運行的解碼數量,防止在大工作負載下線程池飽和。

輸入

並發管線處理的二十個二維碼測試影像中的四個。每個圖像編碼一個 URL,並透過 SemaphoreSlim 使用有界並發並行解碼。

Pipeline QR code input image 1 of 20
Pipeline QR code input image 2 of 20
Pipeline QR code input image 3 of 20
Pipeline QR code input image 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
$vbLabelText   $csharpLabel

輸出

控制台會在管道完成後顯示摘要:解碼的二維碼總數、來源檔案數量和經過的時間,以及每個檔案名稱及其解碼後的 URL。

終端輸出顯示管道結果:從 20 個檔案產生 20 個二維碼,每個檔案名稱都包含解碼後的 URL。

下載全部 20 個測試流程 QR 碼輸入影像(high-volume-qr-images.zip)。

為了提高吞吐量,應使信號量限制與可用核心數相符;如果處理大型影像時記憶體壓力較大,則應降低信號量限制。


延伸閱讀

當管道準備就緒可投入生產時,請查看授權選項

Curtis Chau
技術作家

Curtis Chau 擁有卡爾頓大學計算機科學學士學位,專注於前端開發,擅長於 Node.js、TypeScript、JavaScript 和 React。Curtis 熱衷於創建直觀且美觀的用戶界面,喜歡使用現代框架並打造結構良好、視覺吸引人的手冊。

除了開發之外,Curtis 對物聯網 (IoT) 有著濃厚的興趣,探索將硬體和軟體結合的創新方式。在閒暇時間,他喜愛遊戲並構建 Discord 機器人,結合科技與創意的樂趣。

準備好開始了嗎?
Nuget 下載 63,625 | 版本: 2026.4 剛剛發布
Still Scrolling Icon

還在捲動嗎?

想要快速證明? PM > Install-Package IronQR
執行範例 觀看您的 URL 變成 QR code。