Introduction
Mise à jour: Le contenu de cet article ne s’applique qu’aux versions Acumatica 2021 R1 et antérieures.
Dans cette série de deux articles, je souhaite vous expliquer le fonctionnement des opérations asynchrones et synchrones , ainsi que du multithreading, au sein du cadre Acumatica en C#. Je vais vous montrer comment optimiser les performances (ce qui fonctionne et ce qui ne fonctionne pas), et comment la mise en cache peut améliorer les performances nativement, sans nécessiter d'optimisations multithread dans votre code. Ce premier article porte sur les opérations synchrones et asynchrones. Le multithreading sera traité dans le prochain article, qui paraîtra dans quelques jours.
Opérations synchrones et asynchrones
Il est fréquent de devoir créer des requêtes personnalisées basées sur des agrégations complexes au sein de la base de données. Supposons que vous deviez calculer les totaux de toutes les commandes de vente enregistrées dans la base de données, en fonction de la quantité commandée, du total de la commande et du total des taxes . Vous pouvez implémenter cette requête de manière initialement synchrone, puis asynchrone, 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 trace, les détails suivants:

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 serait une bonne idée de réécrire nos demandes personnalisées pour les versions asynchrones de notre code. Cependant, ne vous laissez pas berner 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 trace :

La version de synchronisation a pris 27.366 ms pour exécuter, et asynchrone seulement 423 (64 fois plus rapide). On dirait qu’il est temps de réécrire les demandes personnalisées pour notre version asynchrone. Mais ne vous précipitez pas pour tirer des conclusions, car c’est 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 qu’avant, mais prenez note des résultats:

Des résultats surprenants, assurément. La sommation synchrone n'a pris que 12 millisecondes, contre 399 millisecondes pour la méthode asynchrone ! Cette amélioration s'explique par le mécanisme de cache d'Acumatica. La boucle Foreach initiale, qui avait énuméré toutes les commandes, les a placées dans le cache d'Acumatica, et il est possible que le serveur SQL ait également effectué une mise en cache. Le résultat de la sommation synchrone a ainsi été obtenu en seulement 12 millisecondes, contre 27 366 pour la méthode initiale.
Ainsi, l’un des points à renombrer ici pourrait être la ré-énumération des enregistrements peut améliorer les performances. En effet, il place les enregistrements dans le cache et élimine tout aller-retour dans la base de données. Ou, si vous voulez être sûr, il suffit de lire tous les enregistrements de la base de données dans un morceau de mémoire, et d’exécuter les calculs là-bas.
Soit dit en passant, si vous êtes malchanceux, vous pouvez obtenir un message d’erreur:
Sans creuser dans les détails, ce message d’erreur est causé par le fait que les graphiques Acumatica par défaut ne sont pas sécurisés pour les threads. Par conséquent, si vous souhaitez é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 principale est que chaque thread aura son propre graphe, évitant ainsi les conflits entre les threads. J'ai donc ajouté la création d'un graphique au code de synchronisation, et voici les résultats de la trace de pile observée :

Vous observez qu’avec une création de graphique séparée (asynchrone vs synchronisation), 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 d’une telle « optimisation ». Je l’ai présenté juste pour vous montrer une comparaison juste des pommes aux pommes.
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 pouvons-nous atteindre un graphique pour les calculs de totaux de synchronisation, et un graphique pour les calculs asynchrones. Après réflexion, je suis venu avec 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 sur le code, j’ai la base revenir à la version de synchronisation, et chaque méthode a sa propre instance de graphique. En outre, afin d’imiter une grande quantité de données (la base de données de démonstration des ventes n’a que 3,348 commandes client), j’ai également introduit cela par cycle.
Et voici les résultats - 100 cycles (égal à 300 000 enregistrements):

1 000 cycles (équivalant à 3 000 000 d’enregistrements) :

Notez que 100 000 cycles (égal à 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 436503/237619 ≈ 1,837 Afin de continuer, je veux ajouter quelques ajouts supplémentaires Si conditions pour rendre les calculs logiques plus complexes, et voir si cela aura un effet notable. Ci-dessous donne 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 sur le code, j’ai ajouté des modifications relativement petites, mais jetez un oeil quel effet il a 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 juste un peu de logique supplémentaire, vous pouvez voir que cela vous donne la différence de performances dans le temps d’exécution de la version sync / async 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 à l’aide de tâches asynchrones. Dans la deuxième partie, j’illustrerai certaines approches multitâche / multi-threading. 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.