Accueil Blog (en) Amélioration des performances - Deuxième partie: Opérations de multithreading C # dans Acumatica

Amélioration des performances - Deuxième partie : Opérations de multithreading C # dans Acumatica

Yuriy Zaletskyy | 30 juin 2023

Mise à jour: Le contenu de cet article ne s’applique qu’aux versions Acumatica 2021 R1 et antérieures.

Amélioration des performances - Deuxième partie : Opérations de multithreading C # dans Acumatica

Introduction

Dans mon dernier article de blog, j’ai partagé avec vous comment fonctionne uneopération synchrone / sychronous dans le cadre Acumatica en utilisant C #.  Aujourd’hui, je vais poursuivre la discussion sur les performances en me concentrant sur les optimisations multithreading dans votre code.

Multithreading dans Acumatica

Pour l’un de mes clients, ils voulaient avoir quelque chose qui fonctionne plus rapidement que les appels d’API WEB. Une telle optimisation peut être réalisée avec l’utilisation de multithreading.

Pour y parvenir, j’ai envisagé un cas synthétique d’importation de 18 249 enregistrements à l’intérieur d’Acumatica.  Les documents ont été tirés d’ici:

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

. Imaginez que pour chaque ligne de cet ensemble de données, vous devez générer une commande client. Vous avez quelques approches d’un point de vue de code C # à votre disposition: monothread et multi-threaded.  Une approche monothread est assez simple. Vous lisez simplement à partir de la source, et un par un, persister la commande client.

Pour commencer, j’ai créé trois articles d’inventaire à Acumaitca: A4770, A4225, A4046. De plus, j’ai créé un reçu d’achat pour 1 000 000 d’articles commandés pour chacun des articles en stock.

Avant de continuer, je veux vous montrer mon gestionnaire de tâches, onglet Performance afin de l’utiliser comme base de référence:

 

Onglet Gestionnaire des tâches et performances.

 

Et maintenant, je vais exécuter des insertions monothread des commandes client dans Acumatica. Voici le code source pour cela:

 


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);
}

}
}
}

 

Jetons maintenant un coup d’œil à la façon dont le gestionnaire de tâches est affecté après l’exécution du code monothread par défaut:

 

Amélioration des performances - Deuxième partie : Opérations de multithreading C # dans Acumatica

 

Observez qu’après le début du chargement à l’importation, le processeur n’a pas changé du tout. En fait, même devenir plus petit, ce qui signifie que 40 cœurs ne seront pas utilisés à son plein potentiel.  Après deux heures et 45 minutes, j’ai eu 3,746 Commandes client créées. Pas mal, mais toujours pas quelque chose d’être particulièrement fier.

Ensuite, j’ai créé du code multi-threading:

 


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);
			}
 
		}
	}
}

Dans l’exemple de code, portez une attention particulière à la pièce avec verrouillage:


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

 

Cela est nécessaire pour la synchronisation persistante des commandes client dans la base de données. Sans ce verrou, peu de graphiques tentent simultanément de créer des enregistrements dans la base de données, et comme résultat, cela bloque le mécanisme persistant d’Acumatica, qui n’est pas sans danger pour les threads. Je crois que cela peut être lié au fait que les numéros de commandes client dépendent des éléments précédemment générés dans la base de données, et c’est pourquoi j’ai considéré le verrou comme nécessaire.

J’ai restauré la base de données à partir de la sauvegarde et exécuté le code multithreading, ou pour être plus précis - le code multitâche. Jetez un oeil à quel point notre gestionnaire de tâches ressemble à différent maintenant:

 

Recherchez Le Gestionnaire des tâches de performance différent.

 

Et regardez - seulement 7% de charge plus élevée! Mais qu’en est-il de la vitesse de création?

Il convient de noter qu’en 2 heures, 35 minutes et 26 secondes, j’ai pu créer les 18 247 commandes client. Cela signifie que notre approche monothread a obtenu 22 commandes client créées par minute. Et notre approche multithread nous a donné 117 commandes client créées par minute ou 5 fois plus vite! Comme autre point d’optimisation, il est possible d’avoir deux machines, l’une sur laquelle Acumatica est installé et s’exécute, et l’autre, sur laquelle MS SQL Server s’exécute. Et pour MS SQL Server, vous devriez envisager de fractionner le fichier de base de données sur quelques disques durs, ainsi que de placer le fichier journal sur le troisième disque dur.

Résumé

Dans cet article de blog, j’ai décrit l’une des deux façons d’accélérer les performances à l’aide du multithreading.  Dans la première partie, j’ai discuté des approches asynchrones/synchrones. Ces deux approches peuvent améliorer considérablement les performances, mais pas dans 100% des cas. Les augmentations de performances réelles ne seront gagnées que si vous avez des quantités significatives de données à importer, manipuler ou masser.  Ce dont nous parlons ici, ce sont des données dans les millions d’enregistrements.

Avant d’ajouter async / multitâche / multi threading à votre code, envisagez d’ajouter la mise en cache (c’est-à-dire une simple énumération des éléments avant le corps principal du cycle). Si la mise en cache n’aide pas les performances, envisagez de déplacer vos calculs logiques vers SQL Server. Si cela ne fournit toujours pas de gains de performances significatifs, vous n’êtes probablement pas engorgement en raison de la quantité d’enregistrements de données dans votre processus de test.  L’ajout d’asynchrones / multitâche / multi threading peut améliorer les performances et l’améliorer considérablement, mais cela nécessite souvent l’utilisation de sections critiques (pour C #, vous utilisez la fonction lock() - ce qui n’est pas toujours simple.
J’espère que ces deux messages vous fournir, vous, le développeur, une compréhension des techniques que vous pouvez appliquer pour améliorer les performances lorsque vous travaillez avec de grandes quantités d’enregistrements de données.

 

Auteur du blog

Yuriy a commencé à programmer en 2003 en utilisant C ++ et FoxPro, puis est passé à .Net en 2006. Depuis 2013, il développe activement des applications à l’aide du cadre Acumatica xRP, développant des solutions pour de nombreux clients au fil des ans. Il a un blog personnel, bien nommé Yuriy Zaletskyy’s Blog, où il a documenté les problèmes de programmation qu’il a rencontrés au cours des six dernières années - partageant librement ses observations et ses solutions avec d’autres développeurs Acumatica.

Recevez des mises à jour de blog dans votre boîte de réception.