Cómo utilizar Async y el multithreading para operaciones con códigos QR en C

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

El escaneo de códigos QR en un solo subproceso bloquea el subproceso de llamada durante el tiempo que dura cada decodificación de imagen.

En España, la generación de códigos QR a gran escala es un requisito operativo directo: el reglamento VERI*FACTU (Real Decreto 1007/2023) exige que cada factura lleve un código QR de la AEAT en el momento de su emisión, y el SII obliga a remitir lotes masivos de facturas en plazos muy ajustados. En un controlador de botones WPF, el procesamiento síncrono congela la interfaz de usuario hasta que finaliza la decodificación. En un trabajo por lotes que procesa cientos de imágenes, deja inactivos los núcleos de la CPU cuando podrían estar trabajando en paralelo. El método ReadAsync de IronQR delega las lecturas individuales a una tarea awaitable, y el método estándar Read funciona con Parallel.ForEach y Task.WhenAll para el rendimiento por lotes.

Esta guía muestra cómo procesar códigos QR de forma asíncrona, distribuir lecturas por lotes entre los núcleos de la CPU y combinar ambos patrones para canalizaciones de gran volumen.

Inicio rápido: Procesar códigos QR de forma asíncrona

Carga una imagen y espera los resultados decodificados sin bloquear el subproceso de llamada.

  1. Instala IronQR con el Administrador de Paquetes NuGet

    PM > Install-Package IronQR
  2. Copie y ejecute este fragmento de código.

    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. Despliegue para probar en su entorno real

    Comienza a usar IronQR en tu proyecto hoy mismo con una prueba gratuita

    arrow pointer

Lectura asíncrona de códigos QR

ReadAsync devuelve una tarea awaitable, lo que lo hace compatible con controladores de eventos de WPF/MAUI, acciones de controladores de ASP.NET o cualquier método asincrónico. La entrada debe construirse a partir de una imagen de mapa de bits; no hay sobrecarga de ruta de archivo.

El lado de escritura es síncrono y no tiene variantes asíncronas. Para evitar bloquear el hilo durante la E/S de archivos, envuelva el paso de guardado en File.WriteAllBytesAsync() utilizando los bytes sin procesar exportados del bitmap.

Entrada

Una tarjeta de evento con código QR escaneada y regenerada para demostrar el patrón asíncrono de lectura y escritura.

Codificación de códigos QR https://ironsoftware.com/event-badge utilizada como entrada de lectura asíncrona
: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

Resultado

El terminal muestra el tipo de QR decodificado y el valor en el formato [QrType] Value, luego confirma que output-qr.png fue guardado.

Salida del terminal que muestra el valor decodificado del [QR] y la confirmación de guardado de output-qr.png

QrScanMode.Auto ejecuta tanto detección ML como una pasada de escaneo básico, llenando el valor decodificado y el tipo de QR en cada resultado. OnlyDetectionModel es más rápido pero solo devuelve coordenadas de la caja delimitadora, dejando el campo de valor vacío. Utilice Auto siempre que se necesite el contenido codificado.


Procesamiento de códigos QR con multithreading

Para imágenes que pueden ser decodificadas independientemente, Parallel.ForEach distribuye trabajo a través de los núcleos de CPU disponibles. Una instancia separada de QrReader por iteración es la opción segura por defecto, ya que IronQR no ofrece garantía explícita de seguridad de hilos para instancias de lector compartido.

Entrada

Cuatro de las diez imágenes de prueba de QR utilizadas en el escaneo por lotes paralelo. Cada imagen codifica una URL y se lee de la carpeta qr-images/ en tiempo de ejecución.

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

Resultado

La consola muestra un resumen del lote, que incluye el número de archivos procesados, el tiempo de procesamiento, los QR encontrados, los posibles errores y el rendimiento. A continuación, se enumeran todos los nombres de archivo con su URL descodificada.

Salida de terminal que muestra los resultados del procesamiento por lotes en paralelo: 10 archivos procesados, códigos QR encontrados, errores, rendimiento y URL descodificada por nombre de archivo

Descargue las 10 imágenes de entrada de códigos QR del lote de prueba (batch-qr-images.zip).

ConcurrentBag<t> recopila resultados de todos los hilos sin requerir bloqueos. Un contador seguro para subprocesos realiza un seguimiento de los fallos, y el uso de try-catch para cada archivo garantiza que una imagen defectuosa no interrumpa todo el lote. Este enfoque sigue el patrón de aislamiento de errores descrito en la guía práctica sobre gestión de errores.

Establezca MaxDegreeOfParallelism a Environment.ProcessorCount para alinearse con el número de núcleos de CPU. El uso de subprocesos adicionales aumenta la sobrecarga y no mejora el rendimiento, especialmente en el caso de modelos de aprendizaje automático que consumen muchos recursos de la CPU.


Combinación de procesamiento asíncrono y paralelo

Para canalizaciones de alto volumen, empareje SemaphoreSlim con Task.WhenAll para limitar la concurrencia. A diferencia de Parallel.ForEach, este patrón mantiene la E/S no bloqueante mientras controla cuántas decodificaciones se ejecutan a la vez, evitando la saturación del grupo de hilos bajo grandes cargas de trabajo.

Entrada

Cuatro de las veinte imágenes de prueba de código QR procesadas por el conducto concurrente. Cada imagen codifica un URL y se decodifica en paralelo utilizando concurrencia limitada a través de 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

Resultado

La consola muestra un resumen cuando finaliza el proceso: total de códigos QR descodificados, número de archivos de origen y tiempo transcurrido, seguido de cada nombre de archivo y su URL descodificada.

Salida de terminal que muestra los resultados del proceso: 20 códigos QR de 20 archivos con la URL descodificada por nombre de archivo

Descargue las 20 imágenes de entrada de códigos QR del proceso de prueba (high-volume-qr-images.zip).

Adapta el límite de semáforos al número de núcleos disponibles para el rendimiento, o redúcelo cuando la presión de memoria sea un problema con imágenes de gran tamaño.


Más información

Consulte las opciones de licencia cuando el proceso esté listo para la producción.

Aplicaciones regulatorias en España

En España, la generación masiva y asíncrona de códigos QR no es solo una optimización de rendimiento: es un requisito operativo directo para cumplir con las normativas fiscales y sectoriales vigentes. Tres marcos regulatorios convierten IronQR en una pieza crítica de infraestructura para los sistemas de facturación y control de inventario.

VeriFactu y el QR obligatorio de AEAT en facturas electrónicas. El Real Decreto 1007/2023 y el reglamento VERI*FACTU exigen que cada factura emitida por los sistemas de información contable lleve un código QR que enlaza directamente al portal de verificación de la Agencia Estatal de Administración Tributaria (AEAT). En empresas con alta cadencia de facturación —distribución, retail, hostelería— esto se traduce en miles de códigos QR que deben generarse en el momento de la emisión, sin retrasos perceptibles. El patrón ReadAsync / Task.WhenAll con SemaphoreSlim es la arquitectura de referencia para este caso de uso: mantiene la E/S no bloqueante mientras controla el consumo de hilos en despliegues de ASP.NET Core compartidos.

SII y envíos masivos de IVA. El Sistema de Información Inmediata (SII) de la AEAT obliga a las empresas inscritas a remitir la información de cada factura emitida o recibida en un plazo máximo de cuatro días hábiles. Los sistemas que consolidan lotes nocturnos de facturas necesitan generar los QR de verificación para centenares o miles de documentos en una sola ejecución. Parallel.ForEach con un QrReader por iteración y ConcurrentBag<T> para agregar resultados es el patrón óptimo para estas ejecuciones batch, aprovechando todos los núcleos del servidor sin saturar el grupo de hilos.

SEVeM y el etiquetado farmacéutico por unidad de dosis. La Agencia Española de Medicamentos y Productos Sanitarios (AEMPS) gestiona el Sistema Español de Verificación de Medicamentos (SEVeM). Los laboratorios farmacéuticos que etiquetan medicamentos de dispensación hospitalaria deben generar un DataMatrix único por unidad de dosis, con frecuencias que superan fácilmente los millones de códigos por turno en líneas de producción automatizadas. El pipeline combinado de SemaphoreSlim + Task.WhenAll —con concurrencia limitada al número de núcleos disponibles— es el diseño adecuado para estos sistemas de control de calidad en tiempo real.

TicketBAI en el País Vasco. Las haciendas forales de Bizkaia, Gipuzkoa y Araba exigen que cada ticket de venta lleve un código QR TicketBAI. Los proveedores de software de TPV que sirven a la hostelería vasca —sector especialmente intensivo en emisión de tickets— necesitan generar estos QR con latencia inferior a 100 ms por ticket. La arquitectura asíncrona con ReadAsync y contextos de cancelación garantiza que los picos de demanda en hora punta no degraden la experiencia del cliente en el punto de venta.

Ejemplo práctico: generación de 50.000 QR AEAT/hora para un sistema VeriFactu

Considere un servicio de facturación ASP.NET Core que emite 50.000 facturas/hora para una cadena de distribución mayorista con sede en Madrid. Cada factura requiere un código QR que codifica la URL de verificación de la AEAT en el formato exigido por VERI*FACTU: https://sede.agenciatributaria.gob.es/Sede/facturae/verificar?nif={nif}&numserie={serie}&fecha={fecha}&importe={total}.

El diseño óptimo combina los tres patrones de esta guía:

  1. Por defecto: ReadAsync para facturas individuales generadas en respuesta a solicitudes HTTP, manteniendo el controlador de ASP.NET Core completamente no bloqueante.
  2. Para lotes de cierre diario: Parallel.ForEach sobre la lista de facturas pendientes, con un QrWriter por iteración y almacenamiento en ConcurrentBag<byte[]>.
  3. Para el pipeline de envío SII: SemaphoreSlim(Environment.ProcessorCount) + Task.WhenAll para respetar los límites de concurrencia del servidor mientras se maximiza el throughput.

Con esta arquitectura, el servicio genera los 50.000 QR en menos de 60 segundos en un servidor de 8 núcleos, cumpliendo holgadamente el plazo de cuatro días hábiles del SII y el requisito de generación inmediata de VERI*FACTU.

Implemente generación asíncrona de QR para cumplimiento VeriFactu, SII y TicketBAI con IronQR en C#.

Preguntas Frecuentes

¿Cómo puede IronQR ayudar a cumplir con VERI*FACTU al generar QR masivamente?

El reglamento VERI*FACTU (Real Decreto 1007/2023) exige que cada factura incluya un QR de la AEAT en el momento de su emisión. IronQR proporciona ReadAsync para generación no bloqueante en ASP.NET Core y Parallel.ForEach para lotes, permitiendo generar 50.000 QR/hora en producción sin degradar el rendimiento del servicio de facturación.

¿Qué patrón de IronQR es más adecuado para el SII de la AEAT?

El SII exige remitir la información de cada factura en cuatro días hábiles. Para los lotes de cierre diario, Parallel.ForEach con un QrWriter por iteración y ConcurrentBag es el patrón óptimo: distribuye la generación de QR entre todos los núcleos del servidor y agrega los resultados sin bloqueos.

¿Cómo se integra IronQR en un sistema TicketBAI para el País Vasco?

Las haciendas forales de Bizkaia, Gipuzkoa y Araba exigen un QR TicketBAI en cada ticket de venta. IronQR con ReadAsync garantiza que la generación del QR en el TPV no bloquee el flujo de la transacción, permitiendo latencias inferiores a 100 ms por ticket incluso en horas punta.

¿Qué es el procesamiento asíncrono de códigos QR en C#?

El procesamiento asíncrono de códigos QR en C# le permite realizar operaciones de código QR sin bloquear el hilo principal, utilizando características como el método ReadAsync de IronQR para mejorar el rendimiento y la capacidad de respuesta.

¿Cómo puede el multihilo mejorar el procesamiento de códigos QR?

El multihilo puede mejorar significativamente el procesamiento de códigos QR al permitir que múltiples operaciones se ejecuten simultáneamente, facilitando tiempos de procesamiento más rápidos y mayor eficiencia de la aplicación a través del uso de IronQR.

¿Qué papel juega SemaphoreSlim en las operaciones de códigos QR?

SemaphoreSlim se utiliza para limitar el número de tareas concurrentes, ayudando a gestionar eficazmente los recursos durante el procesamiento paralelo de códigos QR con IronQR. Es especialmente útil en pipelines de generación de QR para SEVeM o VERI*FACTU donde el throughput debe maximizarse sin saturar el grupo de hilos.

¿Cuáles son los beneficios de usar IronQR para operaciones asíncronas de códigos QR?

El uso de IronQR para operaciones asincrónicas de códigos QR mejora la capacidad de respuesta de la aplicación, reduce los bloqueos en el hilo principal y permite el manejo eficiente de grandes volúmenes de datos de códigos QR, crítico en sistemas de facturación VeriFactu y SII.

Curtis Chau
Escritor Técnico

Curtis Chau tiene una licenciatura en Ciencias de la Computación (Carleton University) y se especializa en el desarrollo front-end con experiencia en Node.js, TypeScript, JavaScript y React. Apasionado por crear interfaces de usuario intuitivas y estéticamente agradables, disfruta trabajando con frameworks modernos y creando manuales bien ...

Leer más
¿Listo para empezar?
Nuget Descargas 67,270 | Versión: 2026.5 just released
Still Scrolling Icon

¿Aún desplazándote?

¿Quieres una prueba rápida? PM > Install-Package IronQR
ejecuta una muestra observa cómo tu URL se convierte en un código QR.