2009-08-29 5 views
2

Je développe une application en C# en utilisant National Instruments Daqmx pour effectuer des mesures sur certains matériels.Mise à jour de l'interface utilisateur avec plusieurs opérations simultanées

Ma configuration se compose de plusieurs détecteurs à partir desquels je dois obtenir des données pendant une période donnée, tout en mettant à jour mon interface utilisateur avec ces données.

public class APD : IDevice 
{ 
    // Some members and properties go here, removed for clarity. 

    public event EventHandler ErrorOccurred; 
    public event EventHandler NewCountsAvailable; 

    // Constructor 
    public APD(
     string __sBoardID, 
     string __sPulseGenCtr, 
     string __sPulseGenTimeBase, 
     string __sPulseGenTrigger, 
     string __sAPDTTLCounter, 
     string __sAPDInputLine) 
    { 
     // Removed for clarity. 
    } 

    private void APDReadCallback(IAsyncResult __iaresResult) 
    { 
     try 
     { 
      if (this.m_daqtskRunningTask == __iaresResult.AsyncState) 
      { 
       // Get back the values read. 
       UInt32[] _ui32Values = this.m_rdrCountReader.EndReadMultiSampleUInt32(__iaresResult); 

       // Do some processing here! 

       if (NewCountsAvailable != null) 
       { 
        NewCountsAvailable(this, new EventArgs()); 
       } 

       // Read again only if we did not yet read all pixels. 
       if (this.m_dTotalCountsRead != this.m_iPixelsToRead) 
       { 
        this.m_rdrCountReader.BeginReadMultiSampleUInt32(-1, this.m_acllbckCallback, this.m_daqtskAPDCount); 
       } 
       else 
       { 
        // Removed for clarity. 
       } 
      } 
     } 
     catch (DaqException exception) 
     { 
      // Removed for clarity. 
     } 
    } 


    private void SetupAPDCountAndTiming(double __dBinTimeMilisec, int __iSteps) 
    { 
     // Do some things to prepare hardware. 
    } 

    public void StartAPDAcquisition(double __dBinTimeMilisec, int __iSteps) 
    { 
     this.m_bIsDone = false; 

     // Prepare all necessary tasks. 
     this.SetupAPDCountAndTiming(__dBinTimeMilisec, __iSteps); 

     // Removed for clarity. 

     // Begin reading asynchronously on the task. We always read all available counts. 
     this.m_rdrCountReader.BeginReadMultiSampleUInt32(-1, this.m_acllbckCallback, this.m_daqtskAPDCount); 
    } 

    public void Stop() 
    { 
     // Removed for clarity. 
    } 
} 

L'objet représentant le détecteur appelle essentiellement une opération BeginXXX avec un rappel qui maintient la EndXXX en tire également un événement indiquant les données disponibles.

J'ai jusqu'à 4 de ces objets de détecteur en tant que membres de mon formulaire d'interface utilisateur. J'appelle la méthode Start() sur chacun d'eux dans l'ordre pour commencer ma mesure. Cela fonctionne et l'événement NewCountsAvailable se déclenche pour tous les quatre.

En raison de la nature de mon implémentation, la méthode BeginXXX est appelée sur le thread d'interface utilisateur et le rappel et l'événement se trouvent également sur ce thread d'interface utilisateur. Par conséquent, je ne peux pas utiliser une sorte de boucle while dans mon thread d'interface utilisateur pour mettre à jour constamment mon interface utilisateur avec les nouvelles données car les événements se déclenchent constamment (j'ai essayé). Je ne veux pas non plus utiliser une méthode UpdateUI() dans chacun des quatre gestionnaires d'événements NewCountsAvailable, car cela chargera trop mon système.

Depuis que je suis nouveau à la programmation threadée en C# je suis maintenant bloqué;

1) Quelle est la manière «appropriée» de gérer une situation comme celle-ci? 2) Mon implémentation de l'objet détecteur est-elle correcte? Dois-je appeler les méthodes Start() sur ces quatre objets de détecteur à partir d'un autre thread? 3) Puis-je utiliser une minuterie pour mettre à jour mon interface utilisateur toutes les quelques centaines de millisecondes, indépendamment de ce que font les 4 objets du détecteur?

Je n'ai vraiment aucune idée!

+0

Quelle version de .NET utilisez-vous? –

+1

Comment est le rappel de nidaq sur le thread UI? (sens: êtes-vous sûr?) – gimpf

+0

Combien de données avez-vous besoin de montrer dans l'interface graphique? Parlons-nous de la vidéo? Et je commente le commentaire de Gimpf, s'il vous plaît soyez certain. –

Répondre

4

J'utilise un système simple de mise à jour différée.

1) threads de travail du signal "données prêtes" en soulevant un événement

2) thread d'interface utilisateur écoute l'événement. Quand il est reçu, il définit simplement un indicateur de "mise à jour des données" et renvoie un traitement minimal sur l'événement lui-même.

3) L'unité d'exécution de l'interface utilisateur utilise un temporisateur (ou s'assoit sur les événements Application.Idle) pour vérifier l'indicateur «données à mettre à jour» et, si nécessaire, mettre à jour l'interface utilisateur. Dans de nombreux cas, l'interface utilisateur n'a besoin d'être mise à jour qu'une ou deux fois par seconde, ce qui évite de perdre beaucoup de temps processeur.

Cela permet à l'interface utilisateur de continuer à fonctionner normalement (tout en restant interactif pour l'utilisateur), mais dans un court laps de temps, certaines données étant prêtes, elles s'affichent dans l'interface utilisateur.

De plus, et surtout pour une bonne interface utilisateur, cette approche peut être utilisée pour permettre à plusieurs événements «prêts pour les données» de se déclencher et de se transformer en une seule mise à jour de l'interface utilisateur. Cela signifie que si 10 éléments de données sont complétés en succession rapprochée, l'interface utilisateur se met à jour une fois plutôt que votre fenêtre scintille pendant plusieurs secondes car l'interface utilisateur redessine (inutilement) 10 fois.

+0

Cela semble raisonnable. Étant inexpérimenté avec threading je me demandais -> Si mon objet APD est en cours d'exécution dans un thread de travail et qu'il déclenche un événement et l'eventhandler est en fait une méthode sur ma classe de formulaire. Dans quel thread l'eventhandler va-t-il s'exécuter? Et, si je veux définir ce drapeau comme vous l'avez dit, devrai-je utiliser les appels InvokeRequired pour mettre ce drapeau en sécurité? Ai-je raison de supposer que eventhandler va s'exécuter à partir du thread de travail? – Kris

+0

En outre, lorsque mon objet APD a son StartAPDAcquisition() appelé dans un thread de travail et StartAPDAcquisition() repose sur BeginXXX alors je vais réellement avoir 3 threads? (1 thread UI, le travailleur, le fil de l'opération asynchrone) Est-ce correct et est-ce souhaitable? Je pourrais vraiment utiliser un bon livre sur ces sujets :) – Kris

+0

Les gestionnaires d'événements sont toujours appelés dans le thread qui a déclenché l'événement (c'est-à-dire le thread de travail dans votre cas).Pour exécuter du code dans le thread d'interface utilisateur, vous pouvez utiliser Invoke/BeginInvoke ou l'approche différée que j'ai décrite ci-dessus. (L'approche différée est très similaire à BeginInvoke, sauf que plusieurs événements peuvent être fusionnés en une seule mise à jour) –

0

Je ne sais pas si je comprends parfaitement. Que faire si vous mettez à jour un objet contenant les données actuelles. Ainsi, le rappel n'interagit pas directement avec l'interface utilisateur. Vous pouvez ensuite mettre à jour l'interface utilisateur à un taux fixe, par ex. n fois par seconde à partir d'un autre thread. See this post on updating UI from a background thread. Je suppose que vous utilisez Windows Forms et non WPF.

1

Je voudrais essayer de déplacer la logique de surveillance IDevice pour séparer les threads pour chaque périphérique. L'interface utilisateur peut ensuite interroger les valeurs via un événement de minuterie, un clic de bouton ou un autre événement lié à l'interface utilisateur. De cette façon, votre interface restera sensible et vos threads feront tout le travail. Voici un exemple de base utilisant une boucle continue. Évidemment, c'est un exemple brutalement simple.

public partial class Form1 : Form 
{ 
    int count; 
    Thread t = null; 

    public Form1() 
    { 
     InitializeComponent(); 
    } 
    private void ProcessLogic() 
    {   
     //CPU intensive loop, if this were in the main thread 
     //UI hangs... 
     while (true) 
     { 
      count++; 
     } 
    } 

    private void Form1_Load(object sender, EventArgs e) 
    { 
     //Cannot directly call ProcessLogic, hangs UI thread. 
     //ProcessLogic(); 

     //instead, run it in another thread and poll needed values 
     //see button1_Click 
     t = new Thread(ProcessLogic); 
     t.Start(); 

    } 
    private void Form1_FormClosing(object sender, FormClosingEventArgs e) 
    { 
     t.Abort(); 
    } 

    private void button1_Click(object sender, EventArgs e) 
    { 
     button1.Text = count.ToString(); 
    } 
} 
+0

J'étais en fait penché vers une solution comme celle-ci. Au départ, j'avais quatre de mes objets APD mentionnés sur mon formulaire. J'appellerais alors le APD.StartAPDAquisition() sur chacun d'eux. Chacun des 4 objets APD a ses événements NewCountsAvailable reliés à une méthode stockant les comptes provenant de l'APD dans un objet document. Après les appels à APD.StartAPDAquisition() j'ai eu une boucle en cours d'exécution qui mettrait à jour l'interface utilisateur toutes les 200ms. Cependant, puisque tous ces appels sont sur le thread de l'interface utilisateur, j'ai remarqué que les événements déclencheurs empêcheraient la boucle de fonctionner ... – Kris

+0

Puisqu'il y a des choses qui ont besoin de clarifier concernant mon code actuel, j'ai ajouté une description plus élaborée ci-dessous. – Kris

1

Certaines mises à jour pour refléter les nouvelles données que vous avez fourni:

Bien que j'ai mes doutes que vos méthodes de EndXXX se passent sur le thread d'interface utilisateur, je pense toujours que vous devez reproduire de travail à un thread d'arrière-plan et ensuite mettre à jour l'interface utilisateur que les événements sont déclenchés ou au besoin.

Parce que vous avez ajouté un virage serré en boucle dans votre interface utilisateur, vous devez appelerApplication.DoEventspour permettre à vos autres événements à appeler.

Voici un exemple de mise à jour qui montre les résultats dans l'interface utilisateur comme ils se produisent:

public class NewCountArgs : EventArgs 
{ 
    public NewCountArgs(int count) 
    { 
     Count = count; 
    } 

    public int Count 
    { 
     get; protected set; 
    } 
} 

public class ADP 
{ 
    public event EventHandler<NewCountArgs> NewCountsAvailable; 

    private double _interval; 
    private double _steps; 
    private Thread _backgroundThread; 

    public void StartAcquisition(double interval, double steps) 
    { 
      _interval = interval; 
      _steps = steps; 

      // other setup work 

      _backgroundThread = new Thread(new ThreadStart(StartBackgroundWork)); 
      _backgroundThread.Start(); 
    } 

    private void StartBackgroundWork() 
    { 
     // setup async calls on this thread 
     m_rdrCountReader.BeginReadMultiSampleUInt32(-1, Callback, _steps); 
    } 

    private void Callback(IAsyncResult result) 
    { 
     int counts = 0; 
     // read counts from result.... 

     // raise event for caller 
     if (NewCountsAvailable != null) 
     { 
      NewCountsAvailable(this, new NewCountArgs(counts)); 
     } 
    } 
} 

public class Form1 : Form 
{ 
    private ADP _adp1; 
    private TextBox txtOutput; // shows updates as they occur 
    delegate void SetCountDelegate(int count); 

    public Form1() 
    { 
     InitializeComponent(); // assume txtOutput initialized here 
    } 

    public void btnStart_Click(object sender, EventArgs e) 
    { 
      _adp1 = new ADP(....); 
      _adp1.NewCountsAvailable += NewCountsAvailable; 
      _adp1.StartAcquisition(....); 

      while(!_adp1.IsDone) 
      { 
       Thread.Sleep(100); 

       // your NewCountsAvailable callbacks will queue up 
       // and will need to be processed 
       Application.DoEvents(); 
      } 

      // final work here 
    } 

    // this event handler will be called from a background thread 
    private void NewCountsAvailable(object sender, NewCountArgs newCounts) 
    { 
     // don't update the UI here, let a thread-aware method do it 
     SetNewCounts(newCounts.Count); 
    } 

    private void SetNewCounts(int counts) 
    { 
     // if the current thread isn't the UI thread 
     if (txtOutput.IsInvokeRequired) 
     { 
      // create a delegate for this method and push it to the UI thread 
      SetCountDelegate d = new SetCountDelegate(SetNewCounts); 
      this.Invoke(d, new object[] { counts }); 
     } 
     else 
     { 
      // update the UI 
      txtOutput.Text += String.Format("{0} - Count Value: {1}", DateTime.Now, counts); 
     } 
    } 
} 
+0

Je ne pense pas avoir bien compris votre suggestion. En l'état actuel des choses, ma classe APD fait déjà des choses dans un autre thread (en lisant le matériel) puisque l'API NI Daqmx m'offre les méthodes BeginRead et EndRead. Cette API m'offre également la possibilité d'arrêter les objets Tâche matérielle à partir desquels ces appels BeginRead et EndRead obtiennent leurs données. J'appelle cette méthode stop dans ma méthode APD.Stop(). Mon problème réside dans le fait que toutes ces opérations asynchrones font leur travail et notifient correctement l'IU de progrès. J'ai ajouté plus de code ci-dessous pour expliquer le problème plus loin ... – Kris

+0

J'ai mis à jour ma réponse basée sur les nouvelles informations que vous avez fournies. – bryanbcook

+0

Bien que j'ai mes doutes que vos méthodes EndXXX se produisent sur le fil de l'interface utilisateur -> Pour autant que je puisse dire, ils le font réellement. Peut-être que quelqu'un pourrait clarifier cela? – Kris

0

Le B * * * dy système de captcha a décidé qu'il était une bonne idée de perdre ma réponse, j'ai passé une demi-typage heure sans même un avertissement ou une chance de corriger ... donc nous y revoilà:

public class APD : IDevice 
{ 
    // Some members and properties go here, removed for clarity. 

    public event EventHandler ErrorOccurred; 
    public event EventHandler NewCountsAvailable; 

    public UInt32[] BufferedCounts 
    { 
     // Get for the _ui32Values returned by the EndReadMultiSampleUInt32() 
     // after they were appended to a list. BufferdCounts therefore supplies 
     // all values read during the experiment. 
    } 

    public bool IsDone 
    { 
     // This gets set when a preset number of counts is read by the hardware or when 
     // Stop() is called. 
    } 

    // Constructor 
    public APD(some parameters) 
    { 
     // Removed for clarity. 
    } 

    private void APDReadCallback(IAsyncResult __iaresResult) 
    { 
     try 
     { 
      if (this.m_daqtskRunningTask == __iaresResult.AsyncState) 
      { 
       // Get back the values read. 
       UInt32[] _ui32Values = this.m_rdrCountReader.EndReadMultiSampleUInt32(__iaresResult); 

       // Do some processing here! 

       if (NewCountsAvailable != null) 
       { 
        NewCountsAvailable(this, new EventArgs()); 
       } 

       // Read again only if we did not yet read all pixels. 
       if (this.m_dTotalCountsRead != this.m_iPixelsToRead) 
       { 
        this.m_rdrCountReader.BeginReadMultiSampleUInt32(-1, this.m_acllbckCallback, this.m_daqtskAPDCount); 
       } 
       else 
       { 
        // Removed for clarity. 
       } 
      } 
     } 
     catch (DaqException exception) 
     { 
      // Removed for clarity. 
     } 
    } 


    private void SetupAPDCountAndTiming(double __dBinTimeMilisec, int __iSteps) 
    { 
     // Do some things to prepare hardware. 
    } 

    public void StartAPDAcquisition(double __dBinTimeMilisec, int __iSteps) 
    { 
     this.m_bIsDone = false; 

     // Prepare all necessary tasks. 
     this.SetupAPDCountAndTiming(__dBinTimeMilisec, __iSteps); 

     // Removed for clarity. 

     // Begin reading asynchronously on the task. We always read all available counts. 
     this.m_rdrCountReader.BeginReadMultiSampleUInt32(-1, this.m_acllbckCallback, this.m_daqtskAPDCount); 
    } 

    public void Stop() 
    { 
     // Removed for clarity. 
    } 
} 

Remarque J'ai ajouté quelques choses que je laissé par erreur dans le message original.

Maintenant sur mon formulaire j'ai le code comme ceci;

public partial class Form1 : Form 
{ 
    private APD m_APD1; 
    private APD m_APD2; 
    private APD m_APD3; 
    private APD m_APD4; 
    private DataDocument m_Document; 

    public Form1() 
    { 
     InitializeComponent(); 
    } 

    private void Button1_Click() 
    {   
     this.m_APD1 = new APD(...); // times four for all APD's 

     this.m_APD1.NewCountsAvailable += new EventHandler(m_APD1_NewCountsAvailable);  // times 4 again... 

     this.m_APD1.StartAPDAcquisition(...); 
     this.m_APD2.StartAPDAcquisition(...); 
     this.m_APD3.StartAPDAcquisition(...); 
     this.m_APD4.StartAPDAcquisition(...); 

     while (!this.m_APD1.IsDone) // Actually I have to check all 4 
     { 
      Thread.Sleep(200); 
      UpdateUI(); 
     } 

     // Some more code after the measurement is done. 
    } 

    private void m_APD1_NewCountsAvailable(object sender, EventArgs e) 
    { 
     this.m_document.Append(this.m_APD1.BufferedCounts); 

    } 

    private void UpdateUI() 
    { 
     // use the data contained in this.m_Document to fill the UI. 
    } 
} 

ouf, j'espère DOD ne rien oublier yping une seconde fois (qui me apprendra pas copier avant de frapper Post).

Ce que je vois en cours d'exécution de ce code est celui;

1) L'objet APD fonctionne comme annoncé, il mesure. 2) Les événements NewCountsAvailable et leurs gestionnaires sont exécutés 3) APD.StartAPDAcquisition() est appelé sur le thread d'interface utilisateur. BeginXXX est donc appelé sur ce fil. Par conséquent, par conception, le rappel est également sur ce thread et, évidemment, les gestionnaires d'événements NewCountsAvailable s'exécutent sur le thread UI. La seule chose qui ne se trouve pas sur le thread d'interface utilisateur est d'attendre que le matériel retourne des valeurs à la paire d'appels BeginXXX EndXXX. 4) Comme les événements NewCountsAvailable tournent beaucoup, la boucle while que je voulais utiliser pour mettre à jour l'interface utilisateur ne fonctionne pas. Généralement, il s'exécute une fois au début, puis est interrompu par les gestionnaires d'événements qui doivent être traités. Je ne comprends pas bien ceci cependant, mais cela ne fonctionne pas ...

Je pensais pour résoudre ceci en se débarassant de la boucle while et en mettant un Forms.Timer sur le formulaire où UpdateUI() serait appelé de le gestionnaire d'événements Tick. Cependant, je ne sais pas si cela serait considéré comme «meilleure pratique». Je ne sais pas non plus si tous ces eventhandlers vont éventuellement amener le thread de l'interface utilisateur à une exploration, je devrais peut-être ajouter quelques autres de ces objets APD dans le futur. UpdateUI() peut aussi contenir du code plus lourd pour calculer une image basée sur les valeurs de m_Document. Donc le tick eventhandler peut aussi être un drain de ressource dans l'approche timer. Dans le cas où j'utiliserais cette solution, j'aurais aussi besoin d'un événement "Terminé" dans ma classe APD pour notifier quand chaque APD se termine. Est-ce que je ne devrais peut-être pas travailler avec des événements pour signaler que de nouveaux comptes sont disponibles, mais plutôt travailler avec une sorte de lecture "à la demande" de APD.BufferedCounts et mettre le tout dans un autre thread? Je n'ai vraiment pas la moindre idée ...

J'ai besoin essentiellement une solution propre, léger qui monte bien devrais-je ajouter encore plus de :) TTA

Questions connexes