Inicio Blog Mejora del rendimiento - Segunda parte: Operaciones multihilo de C# en Acumatica

Mejora del rendimiento - Segunda parte: Operaciones multihilo de C# en Acumatica

Yuriy Zaletskyy | 30 de junio de 2023

Actualización: El contenido de este artículo sólo es aplicable a las versiones 2021 R1 y anteriores de Acumatica.

Mejora del rendimiento - Segunda parte: Operaciones multihilo de C# en Acumatica

Introducción

En mi última entrada del blog, compartí con ustedes cómo funcionan lasoperaciones asíncronas/sicrónicas dentro del marco de Acumatica utilizando C#. Hoy, continuaré la discusión sobre el rendimiento centrándome en las optimizaciones multihilo en tu código.

Multiproceso en Acumatica

Para uno de mis clientes, querían tener algo que funciona más rápido que las llamadas a la API WEB. Tal optimización se puede lograr con el uso de multithreading.

Para ello, consideré un caso sintético de importación de 18.249 registros dentro de Acumatica. Los registros se tomaron de aquí:

https://www.kaggle.com/neuromusic/avocado-prices/kernels

. Imagine que para cada fila de este conjunto de datos necesita generar un pedido de venta. Desde el punto de vista del código C#, tiene a su disposición un par de enfoques: monohilo y multihilo. Un enfoque monohilo es bastante sencillo. Simplemente se lee desde la fuente, y uno por uno, se persiste en la orden de venta.

Para empezar, he creado tres artículos de inventario en Acumaitca: A4770, A4225, A4046. Además, creé un recibo de compra para 1.000.000 de artículos pedidos para cada uno de los artículos de inventario.

Antes de continuar, quiero mostrarte mi Administrador de Tareas, pestaña Rendimiento para usarlo como línea de base:

 

Administrador de tareas y pestaña Rendimiento.

 

Y ahora voy a ejecutar inserciones de un solo hilo de órdenes de venta en Acumatica. Aquí está el código fuente para ello:

 


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MultiThreadingAsyncDemo.DAC;
using PX.Data;
using PX.Objects.SO;

namespace MultiThreadingAsyncDemo
{
public class AvocadosImporter : PXGraph<AvocadosImporter>
{
public PXCancel<ImportAvocado> Cancel;

[PXFilterable]
public PXProcessing<ImportAvocado> NotImportedAvocados;

public override bool IsDirty => false;

private Object thisLock = new Object();

private const string AVOCADOS = "Avocados";

public AvocadosImporter()
{
NotImportedAvocados.SetProcessDelegate(ProcessImportAvocados);
}

public static void ProcessImportAvocados(List<ImportAvocado> importSettings)
{
var avocadosImporter = PXGraph.CreateInstance<AvocadosImporter>();

var avocadosRecords = PXSelect<Avocado, Where<Avocado.imported, Equal<False>>>.Select(avocadosImporter).Select(a => a.GetItem<Avocado>()).ToList();

 

var initGraph = PXGraph.CreateInstance<SOOrderEntry>();
var branchId = initGraph.Document.Insert().BranchID;

Object thisLck = new Object();

var soEntry = PXGraph.CreateInstance<SOOrderEntry>();
for (int i = 0; i < avocadosRecords.Count; i++)
{
var avocadosRecord = avocadosRecords[i];
CreateSalesOrder(soEntry, avocadosRecord, thisLck, branchId);
}

}

private static void CreateSalesOrder(SOOrderEntry sOEntry, Avocado avocadosRecord, Object thisLock, int? branchId)
{
try
{
sOEntry.Clear();

var newSOrder = new SOOrder();
newSOrder.OrderType = "SO";
newSOrder = sOEntry.Document.Insert(newSOrder);
newSOrder.BranchID = branchId;
newSOrder.OrderDate = avocadosRecord.Date;
newSOrder.CustomerID = 7016;
var newSOOrderExt = newSOrder.GetExtension<SOOrderExt>();
newSOOrderExt.Region = avocadosRecord.Region;
newSOOrderExt.Type = avocadosRecord.Type;

sOEntry.Document.Update(newSOrder);

var ln = sOEntry.Transactions.Insert();
ln.BranchID = branchId;
sOEntry.Transactions.SetValueExt<SOLine.inventoryID>(ln, "A4046");
ln.SubItemID = 123;
ln.OrderQty = avocadosRecord.A4046;
ln.CuryUnitPrice = avocadosRecord.AveragePrice;
sOEntry.Transactions.Update(ln);

ln = sOEntry.Transactions.Insert();
ln.BranchID = branchId;
ln.SubItemID = 123;
sOEntry.Transactions.SetValueExt<SOLine.inventoryID>(ln, "A4225");
ln.OrderQty = avocadosRecord.A4225;
ln.CuryUnitPrice = avocadosRecord.AveragePrice;
sOEntry.Transactions.Update(ln);

ln = sOEntry.Transactions.Insert();
ln.BranchID = branchId;
sOEntry.Transactions.SetValueExt<SOLine.inventoryID>(ln, "A4770");
ln.SubItemID = 123;
ln.OrderQty = avocadosRecord.A4770;
ln.CuryUnitPrice = avocadosRecord.AveragePrice;
sOEntry.Transactions.Update(ln);
newSOrder.OrderDesc = avocadosRecord.Date + avocadosRecord.AveragePrice.ToString();
sOEntry.Document.Update(newSOrder);

//lock (thisLock)
{
sOEntry.Actions.PressSave();
}

PXDatabase.Update<Avocado>(
new PXDataFieldAssign<Avocado.imported>(true),
new PXDataFieldRestrict<Avocado.id>(avocadosRecord.Id));
}
catch (Exception exception)
{
PXTrace.WriteError(exception);
}

}
}
}

 

Ahora, vamos a echar un vistazo, cómo el administrador de tareas se ve afectado después de la ejecución del código de un solo hilo por defecto:

 

Mejora del rendimiento - Segunda parte: Operaciones multihilo de C# en Acumatica

 

Observe que después de iniciar la carga de importación, el procesador no cambió en absoluto. En realidad, incluso se hizo más pequeño, lo que significa que los 40 núcleos no se utilizarán en todo su potencial. Después de dos horas y 45 minutos, tenía 3.746 pedidos de venta creados. No está mal, pero aún así no es algo de lo que estar especialmente orgulloso.

A continuación he creado un código multihilo:

 


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MultiThreadingAsyncDemo.DAC;
using PX.Data;
using PX.Objects.SO;
 
namespace MultiThreadingAsyncDemo
{
	public class AvocadosImporter : PXGraph<AvocadosImporter>
	{
		public PXCancel<ImportAvocado> Cancel;
 
		[PXFilterable]
		public PXProcessing<ImportAvocado> NotImportedAvocados;
 
		public override bool IsDirty => false;
 
		private Object thisLock = new Object();
 
		private const string AVOCADOS = "Avocados";
 
		public AvocadosImporter()
		{
			NotImportedAvocados.SetProcessDelegate(ProcessImportAvocados);
		}
 
		public static void ProcessImportAvocados(List<ImportAvocado> importSettings)
		{
			var avocadosImporter = PXGraph.CreateInstance<AvocadosImporter>();
 
			var avocadosRecords = PXSelect<Avocado, Where<Avocado.imported, Equal<False>>>.Select(avocadosImporter).Select(a => a.GetItem<Avocado>()).ToList();
 
			int numberOfLogicalCores = Environment.ProcessorCount;
			List<Task> tasks = new List<Task>(numberOfLogicalCores);
 
			int sizeOfOneChunk = (avocadosRecords.Count / numberOfLogicalCores) + 1;
 
			var initGraph = PXGraph.CreateInstance<SOOrderEntry>();
			var branchId = initGraph.Document.Insert().BranchID;
 
 
			Object thisLck = new Object();
 
			for (int i = 0; i < numberOfLogicalCores; i++)
			{
				int a = i;
 
				var tsk = new Task(
					() =>
					{
						try
						{
							using (new PXImpersonationContext(PX.Data.Update.PXInstanceHelper.ScopeUser))
							{
								using (new PXReadBranchRestrictedScope())
								{
									var portionsGroups = avocadosRecords.Skip(a * sizeOfOneChunk).Take(sizeOfOneChunk)
										.ToList();
 
									if (portionsGroups.Count != 0)
									{
										var sOEntry = PXGraph.CreateInstance<SOOrderEntry>();
										foreach (var avocadosRecord in portionsGroups)
										{
											CreateSalesOrder(sOEntry, avocadosRecord, thisLck, branchId);
										}
									}
								}
							}
						}
						catch (Exception ex)
						{
							PXTrace.WriteInformation(ex);
						}
 
					});
				tasks.Add(tsk);
			}
 
			foreach (var task in tasks)
			{
				task.Start();
			}
 
			Task.WaitAll(tasks.ToArray());
		}
 
		private static void CreateSalesOrder(SOOrderEntry sOEntry, Avocado avocadosRecord, Object thisLock, int? branchId)
		{
			try
			{
				sOEntry.Clear();
 
				var newSOrder = new SOOrder();
				newSOrder.OrderType = "SO";
				newSOrder = sOEntry.Document.Insert(newSOrder);
				newSOrder.BranchID = branchId;
				newSOrder.OrderDate = avocadosRecord.Date;
				newSOrder.CustomerID = 7016;
				var newSOOrderExt = newSOrder.GetExtension<SOOrderExt>();
				newSOOrderExt.Region = avocadosRecord.Region;
				newSOOrderExt.Type = avocadosRecord.Type;
 
				sOEntry.Document.Update(newSOrder);
 
				var ln = sOEntry.Transactions.Insert();
				ln.BranchID = branchId;
				sOEntry.Transactions.SetValueExt<SOLine.inventoryID>(ln, "A4046");
				ln.SubItemID = 123;
				ln.OrderQty = avocadosRecord.A4046;
				ln.CuryUnitPrice = avocadosRecord.AveragePrice;
				sOEntry.Transactions.Update(ln);
 
				ln = sOEntry.Transactions.Insert();
				ln.BranchID = branchId;
				ln.SubItemID = 123;
				sOEntry.Transactions.SetValueExt<SOLine.inventoryID>(ln, "A4225");
				ln.OrderQty = avocadosRecord.A4225;
				ln.CuryUnitPrice = avocadosRecord.AveragePrice;
				sOEntry.Transactions.Update(ln);
 
				ln = sOEntry.Transactions.Insert();
				ln.BranchID = branchId;
				sOEntry.Transactions.SetValueExt<SOLine.inventoryID>(ln, "A4770");
				ln.SubItemID = 123;
				ln.OrderQty = avocadosRecord.A4770;
				ln.CuryUnitPrice = avocadosRecord.AveragePrice;
				sOEntry.Transactions.Update(ln);
				newSOrder.OrderDesc = avocadosRecord.Date + avocadosRecord.AveragePrice.ToString();
				sOEntry.Document.Update(newSOrder);
 
				lock (thisLock)
				{
					sOEntry.Actions.PressSave();
				}
 
				PXDatabase.Update<Avocado>(
					new PXDataFieldAssign<Avocado.imported>(true),
					new PXDataFieldRestrict<Avocado.id>(avocadosRecord.Id));
			}
			catch (Exception exception)
			{
				PXTrace.WriteError(exception);
			}
 
		}
	}
}

En el ejemplo de código, presta especial atención a la parte con candado:


lock (thisLock)
{
	sOEntry.Actions.PressSave();
}

 

Esto es necesario para sincronizar la persistencia de las órdenes de venta en la base de datos. Sin este bloqueo, varios gráficos intentan simultáneamente crear registros en la base de datos, y como resultado esto bloquea el mecanismo de persistencia de Acumatica, que no es seguro para hilos. Creo que esto puede estar relacionado con el hecho de que los números de las órdenes de venta dependen de elementos generados previamente en la base de datos, y es por eso que consideré el bloqueo como necesario.

He restaurado la base de datos desde la copia de seguridad y he ejecutado el código multihilo o, para ser más precisos, el código multitarea. Echa un vistazo a lo diferente que se ve ahora nuestro Administrador de Tareas:

 

Aspecto diferente para el Administrador de tareas de rendimiento.

 

Y mira: ¡sólo un 7% más de carga! Pero, ¿y la velocidad de creación?

Cabe señalar que en 2 horas, 35 minutos y 26 segundos pude crear los 18.247 pedidos de ventas. Esto significa que nuestro enfoque de un único subproceso consiguió crear 22 pedidos de ventas por minuto. Y nuestro enfoque multihilo nos proporcionó 117 pedidos de ventas creados por minuto, es decir, ¡5 veces más rápido! Como otro punto de optimización, es posible tener dos máquinas, una en la que se instale y ejecute Acumatica y otra en la que se ejecute MS SQL Server. Y para MS SQL server, deberías considerar dividir el archivo de base de datos en un par de discos duros, así como colocar el archivo de registro en un tercer disco duro.

Resumen

En esta entrada del blog, he descrito una de las dos formas de acelerar el rendimiento utilizando multithreading. En la primera parte, hablé de enfoques asíncronos/síncronos. Ambos enfoques pueden mejorar el rendimiento de forma significativa, pero no en el 100% de los casos. Los aumentos reales de rendimiento sólo se obtendrán si tiene que importar, manipular o masajear cantidades significativamente grandes de datos. Estamos hablando de millones de registros.

Antes de añadir async/multitask/multi threading a tu código considera añadir caching ( i.e. simple enumeración de elementos antes del cuerpo principal del ciclo ). Si el almacenamiento en caché no ayuda con el rendimiento, considere mover sus cálculos lógicos a SQL Server. Si esto sigue sin proporcionar ninguna mejora significativa del rendimiento, es probable que no se esté produciendo un cuello de botella debido a la cantidad de registros de datos en su proceso de prueba. Añadir async/multitask/multi threading puede mejorar el rendimiento, y mejorarlo significativamente, pero esto a menudo requiere el uso de secciones críticas (para C#, se utiliza la función lock() ) - que no siempre es sencillo.
Espero que estas dos entradas le proporcionen a usted, el desarrollador, una comprensión de las técnicas que puede aplicar para aumentar el rendimiento cuando trabaje con grandes cantidades de registros de datos.

 

Autor del blog

Yuriy empezó a programar en 2003 utilizando C++ y FoxPro, para pasar después a .Net en 2006. A partir de 2013, ha estado desarrollando activamente aplicaciones utilizando Acumatica xRP Framework, desarrollando soluciones para muchos clientes a lo largo de los años. Tiene un blog personal, acertadamente llamado Blog de Yuriy Zaletskyy, donde ha estado documentando los problemas de programación que se ha encontrado en los últimos seis años, compartiendo sus observaciones y soluciones libremente con otros desarrolladores de Acumatica.

Reciba las actualizaciones del blog en su bandeja de entrada.