C#에서 QR 코드 스캔 작업에 Async 및 멀티 스레드를 사용하는 방법

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와 함께 작동합니다.

이 가이드는 QR 코드를 비동기적으로 처리하는 방법, 배치 읽기 작업을 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

QR 코드 비동기 읽기

ReadAsync는 awaitable 작업을 반환하므로 WPF/MAUI 이벤트 핸들러, ASP.NET 컨트롤러 액션 또는 모든 비동기 메서드와 호환됩니다. 입력은 이미지 비트맵으로 구성되어야 하며, 파일 경로에 대한 오버로드는 없습니다.

쓰기 작업은 동기식이며 비동기식 변형은 없습니다. 파일 I/O 중에 스레드가 차단되는 것을 방지하려면 비트맵에서 내보낸 원시 바이트를 사용하여 저장 단계를 File.WriteAllBytesAsync()로 묶으세요.

입력

QR 코드 이벤트 배지를 스캔하고 다시 생성하여 비동기 읽기/쓰기 패턴을 시연합니다.

QR 코드 인코딩(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

산출

단말기는 디코딩된 QR 코드 유형과 값을 [QrType] Value 형식으로 표시한 다음, output-qr.png이 저장되었음을 확인합니다.

터미널 출력 화면에 [QRCode] 디코딩 값과 output-qr.png 저장 확인 메시지가 표시됩니다.

QrScanMode.Auto는 머신러닝 감지와 기본 스캔 패스를 모두 실행하고, 각 결과에 디코딩된 값과 QR 유형을 표시합니다. OnlyDetectionModel는 더 빠르지만 경계 상자 좌표만 반환하고 값 필드를 비워둡니다. 인코딩된 콘텐츠가 필요할 때마다 Auto를 사용하십시오.


멀티스레딩을 이용한 QR 코드 처리

독립적으로 디코딩할 수 있는 이미지의 경우, Parallel.ForEach는 사용 가능한 CPU 코어에 작업을 분산합니다. IronQR 공유 리더 인스턴스에 대한 명시적인 스레드 안전성 보장을 제공하지 않으므로, 반복마다 별도의 QrReader 인스턴스를 사용하는 것이 안전한 기본값입니다.

입력

병렬 배치 스캔에 사용된 10개의 QR 코드 테스트 이미지 중 4개입니다. 각 이미지는 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

산출

콘솔에는 처리된 파일 수, 처리 시간, 발견된 QR 코드 수, 오류 발생 여부 및 처리량을 포함한 배치 요약 정보가 표시됩니다. 그런 다음 각 파일 이름과 해당 파일의 디코딩된 URL을 나열합니다.

병렬 배치 처리 결과가 표시되는 터미널 출력: 처리된 파일 10개, 발견된 QR 코드, 실패 횟수, 처리량 및 파일 이름별 디코딩된 URL

테스트용 QR 코드 입력 이미지 10개가 포함된 배치 파일(batch-qr-images.zip)을 다운로드하세요.

ConcurrentBag<t>는 잠금을 요구하지 않고 모든 스레드의 결과를 수집합니다. 스레드 안전 카운터는 오류를 추적하고, 각 파일에 대해 try-catch를 사용하여 하나의 불량 이미지가 전체 배치 작업을 중단시키지 않도록 합니다. 이 접근 방식은 오류 처리 방법 에서 설명하는 오류 격리 패턴을 따릅니다.

CPU 코어 수에 맞춰 MaxDegreeOfParallelismEnvironment.ProcessorCount로 설정하십시오. 추가 스레드를 사용하면 오버헤드가 증가하고 성능이 향상되지 않으며, 특히 CPU 사용량이 많은 머신러닝 모델의 경우 더욱 그렇습니다.


비동기 처리와 병렬 처리의 결합

대용량 파이프라인의 경우 동시성을 제한하려면 SemaphoreSlimTask.WhenAll를 함께 사용하십시오. Parallel.ForEach와 달리 이 패턴은 I/O를 비차단 방식으로 유지하면서 동시에 실행되는 디코딩 수를 제어하여 대규모 작업 부하에서 스레드 풀 포화를 방지합니다.

입력

동시 처리 파이프라인에서 처리된 20개의 QR 코드 테스트 이미지 중 4개입니다. 각 이미지는 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

산출

파이프라인이 완료되면 콘솔에 요약 정보가 표시됩니다. 총 디코딩된 QR 코드 수, 소스 파일 수, 소요 시간, 그리고 각 파일 이름과 디코딩된 URL이 차례로 표시됩니다.

파이프라인 결과가 표시되는 터미널 출력: 20개 파일에서 추출한 20개의 QR 코드와 파일 이름별로 디코딩된 URL

테스트 파이프라인용 QR 코드 입력 이미지 20개 전체를 다운로드하세요(high-volume-qr-images.zip).

처리량을 높이려면 세마포어 제한을 사용 가능한 코어 수에 맞추거나, 대용량 이미지로 인해 메모리 부족이 우려되는 경우에는 세마포어 제한을 낮추십시오.


추가 자료

라이선스 옵션 보기 when the pipeline is ready for production.

A PHP Error was encountered

Severity: Warning

Message: Illegal string offset 'name'

Filename: sections/author_component.php

Line Number: 18

Backtrace:

File: /var/www/ironpdf.com/application/views/main/sections/author_component.php
Line: 18
Function: _error_handler

File: /var/www/ironpdf.com/application/libraries/Render.php
Line: 63
Function: view

File: /var/www/ironpdf.com/application/views/products/sections/three_column_docs_page_structure.php
Line: 64
Function: main_view

File: /var/www/ironpdf.com/application/libraries/Render.php
Line: 88
Function: view

File: /var/www/ironpdf.com/application/views/products/how-to/index.php
Line: 2
Function: view

File: /var/www/ironpdf.com/application/libraries/Render.php
Line: 88
Function: view

File: /var/www/ironpdf.com/application/libraries/Render.php
Line: 552
Function: view

File: /var/www/ironpdf.com/application/controllers/Products/Howto.php
Line: 31
Function: render_products_view

File: /var/www/ironpdf.com/index.php
Line: 292
Function: require_once

A PHP Error was encountered

Severity: Warning

Message: Illegal string offset 'title'

Filename: sections/author_component.php

Line Number: 38

Backtrace:

File: /var/www/ironpdf.com/application/views/main/sections/author_component.php
Line: 38
Function: _error_handler

File: /var/www/ironpdf.com/application/libraries/Render.php
Line: 63
Function: view

File: /var/www/ironpdf.com/application/views/products/sections/three_column_docs_page_structure.php
Line: 64
Function: main_view

File: /var/www/ironpdf.com/application/libraries/Render.php
Line: 88
Function: view

File: /var/www/ironpdf.com/application/views/products/how-to/index.php
Line: 2
Function: view

File: /var/www/ironpdf.com/application/libraries/Render.php
Line: 88
Function: view

File: /var/www/ironpdf.com/application/libraries/Render.php
Line: 552
Function: view

File: /var/www/ironpdf.com/application/controllers/Products/Howto.php
Line: 31
Function: render_products_view

File: /var/www/ironpdf.com/index.php
Line: 292
Function: require_once

A PHP Error was encountered

Severity: Warning

Message: Illegal string offset 'comment'

Filename: sections/author_component.php

Line Number: 48

Backtrace:

File: /var/www/ironpdf.com/application/views/main/sections/author_component.php
Line: 48
Function: _error_handler

File: /var/www/ironpdf.com/application/libraries/Render.php
Line: 63
Function: view

File: /var/www/ironpdf.com/application/views/products/sections/three_column_docs_page_structure.php
Line: 64
Function: main_view

File: /var/www/ironpdf.com/application/libraries/Render.php
Line: 88
Function: view

File: /var/www/ironpdf.com/application/views/products/how-to/index.php
Line: 2
Function: view

File: /var/www/ironpdf.com/application/libraries/Render.php
Line: 88
Function: view

File: /var/www/ironpdf.com/application/libraries/Render.php
Line: 552
Function: view

File: /var/www/ironpdf.com/application/controllers/Products/Howto.php
Line: 31
Function: render_products_view

File: /var/www/ironpdf.com/index.php
Line: 292
Function: require_once

시작할 준비 되셨나요?
Nuget 다운로드 63,625 | 버전: 2026.4 방금 출시되었습니다
Still Scrolling Icon

아직도 스크롤하고 계신가요?

빠른 증거를 원하시나요? PM > Install-Package IronQR
샘플을 실행하세요 URL이 QR 코드로 바뀌는 것을 확인해 보세요.