Mise à jour : Le contenu de cet article ne s'applique qu'aux versions 2021 R1 et antérieures d'Acumatica.
Introduction
Dans cette série d'articles en deux parties, je souhaite partager avec vous la manière dont lesopérations asynchrones/synchrones et le multithreading fonctionnent dans le cadre d'Acumatica en utilisant le langage C#. Je vous expliquerai comment vous pouvez améliorer les performances - ce qui fonctionne et ce qui ne fonctionne pas, ainsi que la façon dont la mise en cache peut vous aider à améliorer les performances dès le départ sans nécessiter d'optimisation du multithreading dans votre code. Tout d'abord, les opérations synchrones et asynchrones. Je couvrirai le multithreading dans mon prochain article qui sera publié dans quelques jours.
Opérations synchrones et asynchrones
Il est souvent nécessaire de créer des requêtes personnalisées basées sur des agrégations sophistiquées dans la base de données. Supposons que vous ayez une tâche qui consiste à calculer les totaux de toutes les commandes clients dans la base de données pour la quantité commandée, le total de la commande et le total de la taxe. Vous pouvez l'implémenter de manière synchrone dans un premier temps, puis asynchrone dans un second temps, comme suit :
public PXAction<SOOrder> DifferentTests;
[PXButton]
[PXUIField(DisplayName = "Async test")]
protected virtual IEnumerable differentTests(PXAdapter adapter)
{
PXLongOperation.StartOperation(Base, delegate
{
ExecuteTests();
});
return adapter.Get();
}
private void ExecuteTests()
{
var sw = new Stopwatch();
sw.Start();
var r1 = GetAllCuryOrderTotal1();
var r2 = GetAllCuryTaxTotalTotal1();
var r3 = GetAllOrderQty1();
sw.Stop();
PXTrace.WriteInformation($"Milliseconds passed for sync: {sw.ElapsedMilliseconds}, r1 ={r1}, r2={r2}, r3 = {r3}");
var sw1 = new Stopwatch();
sw1.Start();
var t1 = GetAllCuryOrderTotal2();
var t2 = GetAllCuryTaxTotalTotal2();
var t3 = GetAllOrderQty2();
Task.WhenAll(t1, t2, t3).GetAwaiter().GetResult();
sw1.Stop();
PXTrace.WriteInformation($"Milliseconds passed for async: {sw1.ElapsedMilliseconds}, r1 ={t1.Result}, " +
$"r2={t2.Result}, r3 = {t3.Result}");
}
public decimal GetAllCuryOrderTotal1()
{
decimal sum = 0.0m;
foreach (var soOrder in PXSelect<SOOrder>.Select(Base).ToList())
{
sum += soOrder.GetItem<SOOrder>().CuryOrderTotal ?? 0.0m;
}
return sum;
}
public decimal GetAllCuryTaxTotalTotal1()
{
decimal sum = 0.0m;
foreach (var soOrder in PXSelect<SOOrder>.Select(Base).ToList())
{
sum += soOrder.GetItem<SOOrder>().CuryTaxTotal ?? 0.0m;
}
return sum;
}
public decimal GetAllOrderQty1()
{
decimal sum = 0.0m;
foreach (var soOrder in PXSelect<SOOrder>.Select(Base).ToList())
{
sum += soOrder.GetItem<SOOrder>().OrderQty ?? 0.0m;
}
return sum;
}
public async Task<decimal> GetAllCuryOrderTotal2()
{
decimal sum = 0.0m;
await Task.Run(
() =>
{
foreach (var soOrder in PXSelect<SOOrder>.Select(Base).ToList())
{
sum += soOrder.GetItem<SOOrder>().CuryOrderTotal ?? 0.0m;
}
}
);
return sum;
}
public async Task<decimal> GetAllCuryTaxTotalTotal2()
{
decimal sum = 0.0m;
await Task.Run(
() =>
{
foreach (var soOrder in PXSelect<SOOrder>.Select(Base).ToList())
{
sum += soOrder.GetItem<SOOrder>().CuryTaxTotal ?? 0.0m;
}
}
);
return sum;
}
public async Task<decimal> GetAllOrderQty2()
{
decimal sum = 0.0m;
await Task.Run(
() =>
{
foreach (var soOrder in PXSelect<SOOrder>.Select(Base).ToList())
{
sum += soOrder.GetItem<SOOrder>().OrderQty ?? 0.0m;
}
}
);
return sum;
}
Ensuite, comme vous pouvez le voir dans la capture d'écran de la fenêtre de traçage, les détails suivants apparaissent :
Notez que la version synchrone a pris 27 366 ms pour s'exécuter, et le code asynchrone seulement 423 ms (64 fois plus rapide). Il semblerait que ce soit une bonne idée de réécrire nos requêtes personnalisées pour les versions asynchrones de notre code. Cependant, ne vous laissez pas abuser, car ce serait une mauvaise idée. Dans le fragment de code ci-dessous, je pense que vous comprendrez pourquoi :
private void ExecuteTests()
{
foreach (var soOrder in PXSelect<SOOrder>.Select(Base).ToList())
{
}
var sw = new Stopwatch();
sw.Start();
var r1 = GetAllCuryOrderTotal1();
Voici ce que nous voyons dans la fenêtre de traçage :
La version synchrone a mis 27 366 ms à s'exécuter, et la version asynchrone seulement 423 (64 fois plus rapide). Il semble qu'il soit temps de réécrire les requêtes personnalisées pour notre version asynchrone. Mais ne tirez pas de conclusions hâtives, car il s'agit d'une erreur. Je pense que le fragment de code ci-dessous sera explicite :
private void ExecuteTests()
{
foreach (var soOrder in PXSelect<SOOrder>.Select(Base).ToList())
{
}
var sw = new Stopwatch();
sw.Start();
var r1 = GetAllCuryOrderTotal1();
Le reste du code est le même que précédemment, mais prenez note des résultats :
Les résultats sont surprenants. La sommation synchrone n'a pris que 12 millisecondes, alors que la sommation asynchrone a pris 399 millisecondes ! La raison d'une telle amélioration est le mécanisme de mise en cache d'Acumatica. Le Foreach initial qui a énuméré toutes les commandes clients a mis ces commandes clients dans le cache d'Acumatica et peut-être que le serveur SQL a également fait un peu de mise en cache ici. Le résultat de la sommation synchrone n'a pris que 12 millisecondes au lieu des 27 366 millisecondes initiales.
Ainsi, l'un des points à retenir ici pourrait être que la ré-énumération des enregistrements peut améliorer les performances. En effet, elle place les enregistrements dans le cache et élimine les allers-retours avec la base de données. Ou, si vous voulez être sûr, lisez simplement tous les enregistrements de la base de données dans une partie de la mémoire et exécutez les calculs à cet endroit.
Par ailleurs, si vous n'avez pas de chance, vous pouvez obtenir un message d'erreur :
Sans entrer dans les détails, ce message d'erreur est dû au fait que les graphes Acumatica ne sont pas, par défaut, thread safe. Par conséquent, si vous voulez éviter de tels messages d'erreur, vous devrez modifier toutes les méthodes asynchrones que vous pourriez avoir de la manière suivante :
public async Task<decimal> GetAllCuryOrderTotal2()
{
decimal sum = 0.0m;
await Task.Run(
() =>
{
var gr = PXGraph.CreateInstance<SOOrderEntry>();
foreach (var soOrder in PXSelect<SOOrder>.Select(gr).ToList())
{
sum += soOrder.GetItem<SOOrder>().CuryOrderTotal ?? 0.0m;
}
}
);
return sum;
}
L'idée de base ici est que le changement pour chaque thread obtiendra son propre graphique, et donc les threads ne causeront pas de collisions dans votre code. J'ai pensé ajouter une création de graphe au code de synchronisation, et voici les résultats de la trace de pile que j'ai observée :
Vous observez qu'avec une création de graphe séparée (asynchrone vs synchrone), nous remarquons que l'asynchrone est plus rapide - mais il y a un problème. La version du code de synchronisation n'a pas besoin de cette "optimisation". Je l'ai introduite pour vous montrer une comparaison équitable.
Voici le code source complet de cette approche :
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using PX.Data;
using PX.Objects.SO;
namespace MultiThreadingAsyncDemo
{
public class SOOrderEntryExt : PXGraphExtension<SOOrderEntry>
{
public PXAction<SOOrder> MultiThreadingTest;
[PXButton]
[PXUIField(DisplayName = "Multi threading test")]
protected virtual IEnumerable multiThreadingTest(PXAdapter adapter)
{
return adapter.Get();
}
public PXAction<SOOrder> DifferentTests;
[PXButton]
[PXUIField(DisplayName = "Async test")]
protected virtual IEnumerable differentTests(PXAdapter adapter)
{
PXLongOperation.StartOperation(Base, delegate
{
ExecuteTests();
});
return adapter.Get();
}
private void ExecuteTests()
{
foreach (var soOrder in PXSelect<SOOrder>.Select(Base).ToList())//this foreach intended for eliminating effect of caching of records in Acumatica
{
}
var sw = new Stopwatch();
sw.Start();
var r1 = GetAllCuryOrderTotal1();
var r2 = GetAllCuryTaxTotalTotal1();
var r3 = GetAllOrderQty1();
sw.Stop();
PXTrace.WriteInformation($"Milliseconds passed for sync: {sw.ElapsedMilliseconds}, r1 ={r1}, r2={r2}, r3 = {r3}");
var sw1 = new Stopwatch();
sw1.Start();
var t1 = GetAllCuryOrderTotal2();
var t2 = GetAllCuryTaxTotalTotal2();
var t3 = GetAllOrderQty2();
Task.WhenAll(t1, t2, t3).GetAwaiter().GetResult();
sw1.Stop();
PXTrace.WriteInformation($"Milliseconds passed for async: {sw1.ElapsedMilliseconds}, r1 ={t1.Result}, " +
$"r2={t2.Result}, r3 = {t3.Result}");
}
public decimal GetAllCuryOrderTotal1()
{
decimal sum = 0.0m;
var gr = PXGraph.CreateInstance<SOOrderEntry>();
foreach (var soOrder in PXSelect<SOOrder>.Select(gr).ToList())
{
sum += soOrder.GetItem<SOOrder>().CuryOrderTotal ?? 0.0m;
}
return sum;
}
public decimal GetAllCuryTaxTotalTotal1()
{
decimal sum = 0.0m;
var gr = PXGraph.CreateInstance<SOOrderEntry>();
foreach (var soOrder in PXSelect<SOOrder>.Select(gr).ToList())
{
sum += soOrder.GetItem<SOOrder>().CuryTaxTotal ?? 0.0m;
}
return sum;
}
public decimal GetAllOrderQty1()
{
decimal sum = 0.0m;
var gr = PXGraph.CreateInstance<SOOrderEntry>();
foreach (var soOrder in PXSelect<SOOrder>.Select(gr).ToList())
{
sum += soOrder.GetItem<SOOrder>().OrderQty ?? 0.0m;
}
return sum;
}
public async Task<decimal> GetAllCuryOrderTotal2()
{
decimal sum = 0.0m;
await Task.Run(
() =>
{
var gr = PXGraph.CreateInstance<SOOrderEntry>();
foreach (var soOrder in PXSelect<SOOrder>.Select(gr).ToList())
{
sum += soOrder.GetItem<SOOrder>().CuryOrderTotal ?? 0.0m;
}
}
);
return sum;
}
public async Task<decimal> GetAllCuryTaxTotalTotal2()
{
decimal sum = 0.0m;
await Task.Run(
() =>
{
var gr = PXGraph.CreateInstance<SOOrderEntry>();
foreach (var soOrder in PXSelect<SOOrder>.Select(gr).ToList())
{
sum += soOrder.GetItem<SOOrder>().CuryTaxTotal ?? 0.0m;
}
}
);
return sum;
}
public async Task<decimal> GetAllOrderQty2()
{
decimal sum = 0.0m;
await Task.Run(
() =>
{
var gr = PXGraph.CreateInstance<SOOrderEntry>();
foreach (var soOrder in PXSelect<SOOrder>.Select(gr).ToList())
{
sum += soOrder.GetItem<SOOrder>().OrderQty ?? 0.0m;
}
}
);
return sum;
}
}
}
Une question logique que l'on pourrait se poser : comment obtenir un graphique pour les calculs de totaux synchronisés, et un graphique pour les calculs asynchrones. Après réflexion, j'ai trouvé le code suivant :
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using PX.Data;
using PX.Objects.SO;
namespace MultiThreadingAsyncDemo
{
public class SOOrderEntryExt : PXGraphExtension<SOOrderEntry>
{
public PXAction<SOOrder> MultiThreadingTest;
[PXButton]
[PXUIField(DisplayName = "Multi threading test")]
protected virtual IEnumerable multiThreadingTest(PXAdapter adapter)
{
return adapter.Get();
}
public PXAction<SOOrder> DifferentTests;
[PXButton]
[PXUIField(DisplayName = "Async test")]
protected virtual IEnumerable differentTests(PXAdapter adapter)
{
PXLongOperation.StartOperation(Base, delegate
{
ExecuteTests();
});
return adapter.Get();
}
private void ExecuteTests()
{
int numberOfIterations = 100;
foreach (var soOrder in PXSelect<SOOrder>.Select(Base).ToList())//this foreach intended for eliminating effect of caching of records in Acumatica
{
}
var sw = new Stopwatch();
sw.Start();
decimal r1, r2, r3;
for(int i = 0; i < numberOfIterations; i++)
{
r1 = GetAllCuryOrderTotal1();
r2 = GetAllCuryTaxTotalTotal1();
r3 = GetAllOrderQty1();
}
sw.Stop();
PXTrace.WriteInformation($"Milliseconds passed for sync: {sw.ElapsedMilliseconds}, r1 ={r1}, r2={r2}, r3 = {r3}");
var sw1 = new Stopwatch();
sw1.Start();
Task<decimal> t1 = null, t2 = null, t3 = null;
var g1 = PXGraph.CreateInstance<SOOrderEntry>();
var g2 = PXGraph.CreateInstance<SOOrderEntry>();
var g3 = PXGraph.CreateInstance<SOOrderEntry>();
for (int i = 0; i < numberOfIterations; i++)
{
t1 = GetAllCuryOrderTotal2(g1);
t2 = GetAllCuryTaxTotalTotal2(g2);
t3 = GetAllOrderQty2(g3);
Task.WhenAll(t1, t2, t3).GetAwaiter().GetResult();
}
sw1.Stop();
PXTrace.WriteInformation($"Milliseconds passed for async: {sw1.ElapsedMilliseconds}, r1 ={t1.Result}, " +
$"r2={t2.Result}, r3 = {t3.Result}");
}
public decimal GetAllCuryOrderTotal1()
{
decimal sum = 0.0m;
foreach (var soOrder in PXSelect<SOOrder>.Select(Base).ToList())
{
sum += soOrder.GetItem<SOOrder>().CuryOrderTotal ?? 0.0m;
}
return sum;
}
public decimal GetAllCuryTaxTotalTotal1()
{
decimal sum = 0.0m;
foreach (var soOrder in PXSelect<SOOrder>.Select(Base).ToList())
{
sum += soOrder.GetItem<SOOrder>().CuryTaxTotal ?? 0.0m;
}
return sum;
}
public decimal GetAllOrderQty1()
{
decimal sum = 0.0m;
foreach (var soOrder in PXSelect<SOOrder>.Select(Base).ToList())
{
sum += soOrder.GetItem<SOOrder>().OrderQty ?? 0.0m;
}
return sum;
}
public async Task<decimal> GetAllCuryOrderTotal2(SOOrderEntry gr)
{
decimal sum = 0.0m;
await Task.Run(
() =>
{
foreach (var soOrder in PXSelect<SOOrder>.Select(gr).ToList())
{
sum += soOrder.GetItem<SOOrder>().CuryOrderTotal ?? 0.0m;
}
}
);
return sum;
}
public async Task<decimal> GetAllCuryTaxTotalTotal2(SOOrderEntry gr)
{
decimal sum = 0.0m;
await Task.Run(
() =>
{
foreach (var soOrder in PXSelect<SOOrder>.Select(gr).ToList())
{
sum += soOrder.GetItem<SOOrder>().CuryTaxTotal ?? 0.0m;
}
}
);
return sum;
}
public async Task<decimal> GetAllOrderQty2(SOOrderEntry gr)
{
decimal sum = 0.0m;
await Task.Run(
() =>
{
foreach (var soOrder in PXSelect<SOOrder>.Select(gr).ToList())
{
sum += soOrder.GetItem<SOOrder>().OrderQty ?? 0.0m;
}
}
);
return sum;
}
}
}
Comme vous pouvez le voir dans le code, la base revient à la version de synchronisation et chaque méthode possède sa propre instance de graphique. De plus, afin d'imiter une grande quantité de données (la base de données de la démonstration des ventes ne contient que 3 348 commandes), j'ai également introduit ce cycle.
Et voici les résultats - 100 cycles (équivalant à 300 000 enregistrements) :
1 000 cycles (soit 3 000 000 d'enregistrements) :
Notez que 100 000 cycles (équivalant à 30 millions d'enregistrements) :
Comme vous pouvez le voir sur la capture d'écran, à 30 millions d'enregistrements, la différence de performance entre sync/async. En représentation numérique, cette différence est de 436503/237619 ≈ 1,837. Si J'ai donc décidé de modifier les conditions de calcul pour rendre les calculs logiques plus complexes, et de voir si cela aura un effet notable. Vous trouverez ci-dessous un échantillon des modifications que j'ai apportées :
public bool IsMultipleOf2(string str)
{
try
{
char last = str[str.Length - 1];
int number = int.Parse(last.ToString());
return number % 2 == 0;
}
catch
{
return true;
}
}
public decimal GetAllCuryOrderTotal1()
{
decimal sum = 0.0m;
foreach (var soOrder in PXSelect<SOOrder>.Select(Base).ToList())
{
if (IsMultipleOf2(soOrder.GetItem<SOOrder>().OrderNbr))
{
sum += soOrder.GetItem<SOOrder>().CuryOrderTotal ?? 0.0m;
}
else
{
var number = soOrder.GetItem<SOOrder>().CuryOrderTotal ?? 0.0m;
sum += number * number;
}
}
return sum;
}
Comme vous pouvez le voir dans le code, j'ai apporté des modifications relativement mineures, mais jetez un coup d'œil à l'effet qu'elles ont sur la différence de performance :
913838 / 430728 ≈ 2.12
Voici à nouveau le code source complet :
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using PX.Data;
using PX.Objects.SO;
namespace MultiThreadingAsyncDemo
{
public class SOOrderEntryExt : PXGraphExtension<SOOrderEntry>
{
public PXAction<SOOrder> MultiThreadingTest;
[PXButton]
[PXUIField(DisplayName = "Multi threading test")]
protected virtual IEnumerable multiThreadingTest(PXAdapter adapter)
{
return adapter.Get();
}
public PXAction<SOOrder> DifferentTests;
[PXButton]
[PXUIField(DisplayName = "Async test")]
protected virtual IEnumerable differentTests(PXAdapter adapter)
{
PXLongOperation.StartOperation(Base, delegate
{
ExecuteTests();
});
return adapter.Get();
}
private void ExecuteTests()
{
int numberOfIterations = 100000;
foreach (var soOrder in PXSelect<SOOrder>.Select(Base).ToList())//this foreach intended for eliminating effect of caching of records in Acumatica
{
}
var sw = new Stopwatch();
sw.Start();
decimal r1 = 0, r2 = 0, r3 = 0;
for(int i = 0; i < numberOfIterations; i++)
{
r1 = GetAllCuryOrderTotal1();
r2 = GetAllCuryTaxTotalTotal1();
r3 = GetAllOrderQty1();
}
sw.Stop();
PXTrace.WriteInformation($"Milliseconds passed for sync: {sw.ElapsedMilliseconds}, r1 ={r1}, r2={r2}, r3 = {r3}");
var sw1 = new Stopwatch();
sw1.Start();
Task<decimal> t1 = null, t2 = null, t3 = null;
var g1 = PXGraph.CreateInstance<SOOrderEntry>();
var g2 = PXGraph.CreateInstance<SOOrderEntry>();
var g3 = PXGraph.CreateInstance<SOOrderEntry>();
for (int i = 0; i < numberOfIterations; i++)
{
t1 = GetAllCuryOrderTotal2(g1);
t2 = GetAllCuryTaxTotalTotal2(g2);
t3 = GetAllOrderQty2(g3);
Task.WhenAll(t1, t2, t3).GetAwaiter().GetResult();
}
sw1.Stop();
PXTrace.WriteInformation($"Milliseconds passed for async: {sw1.ElapsedMilliseconds}, r1 ={t1.Result}, " +
$"r2={t2.Result}, r3 = {t3.Result}");
}
public bool IsMultipleOf2(string str)
{
try
{
char last = str[str.Length - 1];
int number = int.Parse(last.ToString());
return number % 2 == 0;
}
catch
{
return true;
}
}
public decimal GetAllCuryOrderTotal1()
{
decimal sum = 0.0m;
foreach (var soOrder in PXSelect<SOOrder>.Select(Base).ToList())
{
if (IsMultipleOf2(soOrder.GetItem<SOOrder>().OrderNbr))
{
sum += soOrder.GetItem<SOOrder>().CuryOrderTotal ?? 0.0m;
}
else
{
var number = soOrder.GetItem<SOOrder>().CuryOrderTotal ?? 0.0m;
sum += number * number;
}
}
return sum;
}
public decimal GetAllCuryTaxTotalTotal1()
{
decimal sum = 0.0m;
foreach (var soOrder in PXSelect<SOOrder>.Select(Base).ToList())
{
if (IsMultipleOf2(soOrder.GetItem<SOOrder>().OrderNbr))
{
sum += soOrder.GetItem<SOOrder>().CuryTaxTotal ?? 0.0m;
}
else
{
var number = soOrder.GetItem<SOOrder>().CuryTaxTotal ?? 0.0m;
sum += number * number;
}
}
return sum;
}
public decimal GetAllOrderQty1()
{
decimal sum = 0.0m;
foreach (var soOrder in PXSelect<SOOrder>.Select(Base).ToList())
{
if (IsMultipleOf2(soOrder.GetItem<SOOrder>().OrderNbr))
{
sum += soOrder.GetItem<SOOrder>().OrderQty ?? 0.0m;
}
else
{
var number = soOrder.GetItem<SOOrder>().OrderQty ?? 0.0m;
sum += number * number;
}
}
return sum;
}
public async Task<decimal> GetAllCuryOrderTotal2(SOOrderEntry gr)
{
decimal sum = 0.0m;
await Task.Run(
() =>
{
foreach (var soOrder in PXSelect<SOOrder>.Select(gr).ToList())
{
if (IsMultipleOf2(soOrder.GetItem<SOOrder>().OrderNbr))
{
sum += soOrder.GetItem<SOOrder>().CuryOrderTotal ?? 0.0m;
}
else
{
var number = soOrder.GetItem<SOOrder>().CuryOrderTotal ?? 0.0m;
sum += number * number;
}
}
}
);
return sum;
}
public async Task<decimal> GetAllCuryTaxTotalTotal2(SOOrderEntry gr)
{
decimal sum = 0.0m;
await Task.Run(
() =>
{
foreach (var soOrder in PXSelect<SOOrder>.Select(gr).ToList())
{
if (IsMultipleOf2(soOrder.GetItem<SOOrder>().OrderNbr))
{
sum += soOrder.GetItem<SOOrder>().CuryTaxTotal ?? 0.0m;
}
else
{
var number = soOrder.GetItem<SOOrder>().CuryTaxTotal ?? 0.0m;
sum += number * number;
}
}
}
);
return sum;
}
public async Task<decimal> GetAllOrderQty2(SOOrderEntry gr)
{
decimal sum = 0.0m;
await Task.Run(
() =>
{
foreach (var soOrder in PXSelect<SOOrder>.Select(gr).ToList())
{
if (IsMultipleOf2(soOrder.GetItem<SOOrder>().OrderNbr))
{
sum += soOrder.GetItem<SOOrder>().OrderQty ?? 0.0m;
}
else
{
var number = soOrder.GetItem<SOOrder>().OrderQty ?? 0.0m;
sum += number * number;
}
}
}
);
return sum;
}
}
}
Avec un peu de logique supplémentaire, vous pouvez voir que cela vous donne la différence de performance dans le temps d'exécution de la version synchrone/asynchrone avec l'amélioration marquée de la version asynchrone du code.
Résumé
Dans cet article de blog, j'ai décrit l'une des deux façons d'accélérer les performances en utilisant des tâches asynchrones. Dans la deuxième partie, j'illustrerai certaines approches multitâches/multithreading. Ces deux approches peuvent améliorer les performances de manière significative, mais pas dans 100 % des cas. Les performances ne seront réellement améliorées que si vous avez de grandes quantités de données à importer, à manipuler ou à masser. Nous parlons ici de millions d'enregistrements.