J'essaie de comprendre quelle sera la meilleure façon de travailler avec une file d'attente. J'ai un processus qui renvoie un DataTable. Chaque DataTable, à son tour, est fusionné avec le DataTable précédent. Il y a un problème, trop d'enregistrements à conserver jusqu'au BulkCopy final (OutOfMemory).
J'ai donc décidé de traiter immédiatement chaque DataTable entrant. En pensant à ConcurrentQueue<T>
... mais je ne vois pas comment la méthode WriteQueuedData()
saurait retirer une table et l'écrire dans la base de données.
Par exemple:
public class TableTransporter
{
private ConcurrentQueue<DataTable> tableQueue = new ConcurrentQueue<DataTable>();
public TableTransporter()
{
tableQueue.OnItemQueued += new EventHandler(WriteQueuedData); // no events available
}
public void ExtractData()
{
DataTable table;
// perform data extraction
tableQueue.Enqueue(table);
}
private void WriteQueuedData(object sender, EventArgs e)
{
BulkCopy(e.Table);
}
}
Ma première question est, mis à part le fait que je n'ai aucun événement auquel m'abonner, si j'appelle ExtractData()
de manière asynchrone, est-ce tout ce dont j'ai besoin? Deuxièmement, y a-t-il quelque chose qui me manque dans la façon dont ConcurrentQueue<T>
Fonctionne et a besoin d'une sorte de déclencheur pour fonctionner de manière asynchrone avec les objets en file d'attente?
pdate Je viens de dériver une classe de ConcurrentQueue<T>
Qui a un gestionnaire d'événement OnItemQueued. Ensuite:
new public void Enqueue (DataTable Table)
{
base.Enqueue(Table);
OnTableQueued(new TableQueuedEventArgs(Table));
}
public void OnTableQueued(TableQueuedEventArgs table)
{
EventHandler<TableQueuedEventArgs> handler = TableQueued;
if (handler != null)
{
handler(this, table);
}
}
Des préoccupations concernant cette mise en œuvre?
De ma compréhension du problème, vous manquez quelques choses.
La file d'attente simultanée est une structure de données conçue pour accepter la lecture et l'écriture de plusieurs threads dans la file d'attente sans que vous ayez à verrouiller explicitement la structure de données. (Tout ce que le jazz est pris en charge dans les coulisses, ou la collection est mise en œuvre de telle sorte qu'elle n'a pas besoin de prendre un verrou.)
Dans cet esprit, il semble que le modèle que vous essayez d'utiliser soit le "Produce/Consumer". Tout d'abord, vous avez certaines tâches pour produire du travail (et ajouter des éléments à la file d'attente). Et deuxièmement, vous avez une deuxième tâche Consommer des choses de la file d'attente (et retirer des éléments).
Donc, vous voulez vraiment deux threads: un ajout d'éléments et un second suppression d'éléments. Étant donné que vous utilisez une collection simultanée, vous pouvez avoir plusieurs threads ajoutant des éléments et plusieurs threads supprimant des éléments. Mais évidemment, plus vous avez de conflits sur la file d'attente simultanée, plus vite cela deviendra le goulot d'étranglement.
Je pense que ConcurrentQueue
n'est utile que dans très peu de cas. Son principal avantage est qu'il n'est pas verrouillé. Cependant, le ou les threads producteurs doivent généralement informer le ou les threads consommateurs qu'il existe des données à traiter. Cette signalisation entre les threads nécessite des verrous et annule l'avantage d'utiliser ConcurrentQueue
. Le moyen le plus rapide de synchroniser les threads utilise Monitor.Pulse()
, qui ne fonctionne que dans un verrou. Tous les autres outils de synchronisation sont encore plus lents.
Bien sûr, le consommateur peut simplement vérifier en permanence s'il y a quelque chose dans la file d'attente, qui fonctionne sans verrouillage, mais représente un énorme gaspillage de ressources processeur. Un peu mieux si le consommateur attend entre deux vérifications.
Augmenter un thread lors de l'écriture dans la file d'attente est une très mauvaise idée. L'utilisation de ConcurrentQueue
pour économiser peut-être 1 microseconde sera complètement gaspillée en exécutant eventhandler
, ce qui pourrait prendre 1000 fois plus de temps.
Si tout le traitement est effectué dans un gestionnaire d'événements ou un appel asynchrone, la question est pourquoi encore une file d'attente est-elle nécessaire? Mieux vaut transmettre les données directement au gestionnaire et ne pas utiliser de file d'attente du tout.
Veuillez noter que l'implémentation de ConcurrentQueue
est assez compliquée pour permettre la simultanéité. Dans la plupart des cas, il vaut mieux utiliser un Queue<>
et verrouille chaque accès à la file d'attente. Étant donné que l'accès à la file d'attente n'a besoin que de quelques microsecondes, il est extrêmement peu probable que 2 threads accèdent à la file d'attente dans la même microseconde et il n'y aura pratiquement jamais de retard en raison du verrouillage. Utilisation d'un Queue<>
avec verrouillage se traduira souvent par une exécution de code plus rapide que ConcurrentQueue
.
Ceci est la solution complète pour ce que j'ai trouvé:
public class TableTransporter
{
private static int _indexer;
private CustomQueue tableQueue = new CustomQueue();
private Func<DataTable, String> RunPostProcess;
private string filename;
public TableTransporter()
{
RunPostProcess = new Func<DataTable, String>(SerializeTable);
tableQueue.TableQueued += new EventHandler<TableQueuedEventArgs>(tableQueue_TableQueued);
}
void tableQueue_TableQueued(object sender, TableQueuedEventArgs e)
{
// do something with table
// I can't figure out is how to pass custom object in 3rd parameter
RunPostProcess.BeginInvoke(e.Table,new AsyncCallback(PostComplete), filename);
}
public void ExtractData()
{
// perform data extraction
tableQueue.Enqueue(MakeTable());
Console.WriteLine("Table count [{0}]", tableQueue.Count);
}
private DataTable MakeTable()
{ return new DataTable(String.Format("Table{0}", _indexer++)); }
private string SerializeTable(DataTable Table)
{
string file = Table.TableName + ".xml";
DataSet dataSet = new DataSet(Table.TableName);
dataSet.Tables.Add(Table);
Console.WriteLine("[{0}]Writing {1}", Thread.CurrentThread.ManagedThreadId, file);
string xmlstream = String.Empty;
using (MemoryStream memstream = new MemoryStream())
{
XmlSerializer xmlSerializer = new XmlSerializer(typeof(DataSet));
XmlTextWriter xmlWriter = new XmlTextWriter(memstream, Encoding.UTF8);
xmlSerializer.Serialize(xmlWriter, dataSet);
xmlstream = UTF8ByteArrayToString(((MemoryStream)xmlWriter.BaseStream).ToArray());
using (var fileStream = new FileStream(file, FileMode.Create))
fileStream.Write(StringToUTF8ByteArray(xmlstream), 0, xmlstream.Length + 2);
}
filename = file;
return file;
}
private void PostComplete(IAsyncResult iasResult)
{
string file = (string)iasResult.AsyncState;
Console.WriteLine("[{0}]Completed: {1}", Thread.CurrentThread.ManagedThreadId, file);
RunPostProcess.EndInvoke(iasResult);
}
public static String UTF8ByteArrayToString(Byte[] ArrBytes)
{ return new UTF8Encoding().GetString(ArrBytes); }
public static Byte[] StringToUTF8ByteArray(String XmlString)
{ return new UTF8Encoding().GetBytes(XmlString); }
}
public sealed class CustomQueue : ConcurrentQueue<DataTable>
{
public event EventHandler<TableQueuedEventArgs> TableQueued;
public CustomQueue()
{ }
public CustomQueue(IEnumerable<DataTable> TableCollection)
: base(TableCollection)
{ }
new public void Enqueue (DataTable Table)
{
base.Enqueue(Table);
OnTableQueued(new TableQueuedEventArgs(Table));
}
public void OnTableQueued(TableQueuedEventArgs table)
{
EventHandler<TableQueuedEventArgs> handler = TableQueued;
if (handler != null)
{
handler(this, table);
}
}
}
public class TableQueuedEventArgs : EventArgs
{
#region Fields
#endregion
#region Init
public TableQueuedEventArgs(DataTable Table)
{this.Table = Table;}
#endregion
#region Functions
#endregion
#region Properties
public DataTable Table
{get;set;}
#endregion
}
En tant que preuve de concept, cela semble assez bien fonctionner. Au plus, j'ai vu 4 fils de travail.