Mise à jour: Le contenu de cet article ne s’applique qu’aux versions Acumatica 2021 R1 et antérieures.
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:
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:
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:
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.