Обновление пользовательского интерфейса с несколькими одновременными операциями

2

Я разрабатываю приложение на С#, используя National Instruments Daqmx для выполнения измерений на определенном оборудовании.

Моя настройка состоит из нескольких детекторов, из которых я должен получать данные в течение заданного периода времени, и все это время обновляю свой интерфейс с этими данными.

 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. 
    }
}

Объект, представляющий детектор, в основном вызывает операцию BeginXXX с обратным вызовом, который содержит EndXXX en, также вызывает событие, указывающее доступные данные.

У меня есть до 4 из этих объектов-детекторов в качестве элементов моей формы пользовательского интерфейса. Я вызываю метод Start() для всех из них в последовательности, чтобы начать измерение. Это работает, и событие NewCountsAvailable срабатывает для всех четырех.

Из-за характера моей реализации метод BeginXXX вызывается в потоке пользовательского интерфейса, а Callback и Event также находятся в этом потоке пользовательского интерфейса. Поэтому я не могу использовать какой-то цикл while внутри моего потока пользовательского интерфейса, чтобы постоянно обновлять свой интерфейс с новыми данными, потому что события постоянно срабатывают (я пробовал это). Я также не хочу использовать какой-то метод UpdateUI() в каждом из четырех обработчиков событий NewCountsAvailable, так как это слишком сильно загрузит мою систему.

Поскольку я новичок в программировании на языке С#, я теперь застрял,

1) Каков "правильный" способ справиться с такой ситуацией? 2) Звучит ли моя реализация объекта извещателя? Должен ли я вызвать методы Start() на этих четырех объектах детектирования из еще одного потока? 3) Могу ли я использовать таймер для обновления моего интерфейса каждые несколько сотен миллисекунд, независимо от того, что делают 4 объекта-детектор?

У меня действительно нет подсказки!

  • 0
    Какую версию .NET вы используете?
  • 1
    Как происходит обратный вызов из nidaq в потоке пользовательского интерфейса? (имеется в виду: ты уверен?)
Показать ещё 2 комментария
Теги:
multithreading
user-interface
concurrency

5 ответов

4
Лучший ответ

Я бы использовал простую отложенную систему обновлений.

1) Рабочие потоки сигнализируют "данные готовы", поднимая событие

2) Пользовательский поток прослушивает событие. Когда он получен, он просто устанавливает флаг "Обновление потребностей данных" и возвращает, поэтому минимальная обработка происходит в самом событии.

3) В потоке пользовательского интерфейса используется таймер (или сидит на события Application.Idle), чтобы проверить флажок "Обновление потребностей данных" и, при необходимости, обновить интерфейс. Во многих случаях UI нужно обновлять только один или два раза в секунду, поэтому это не требует большого количества времени процессора.

Это позволяет UI продолжать работать как обычно все время (оставаясь интерактивным для пользователя), но в течение короткого периода времени некоторые данные готовы, он отображается в пользовательском интерфейсе.

Кроме того, и что самое важное для хорошего пользовательского интерфейса, этот подход можно использовать, чтобы позволить нескольким событиям, готовым к "подготовке данных", запускаться и переноситься в одно обновление пользовательского интерфейса. Это означает, что если 10 фрагментов данных будут завершены с тесной последовательностью, пользовательский интерфейс обновится один раз, а не будет мерцать в течение нескольких секунд, когда пользовательский интерфейс будет повторно (без необходимости) 10 раз.

  • 0
    Звучит разумно. Будучи неопытным с многопоточностью, мне было интересно -> Если мой объект APD запущен в рабочем потоке, и он запускает событие, а обработчик событий на самом деле является методом в моем классе формы. В каком потоке будет выполняться обработчик событий? И, если я хочу установить этот флаг, как вы сказали, придется ли мне использовать вызовы InvokeRequired для безопасной установки этого флага? Правильно ли я полагаю, что обработчик событий будет запускаться из рабочего потока?
  • 0
    Кроме того, когда мой объект APD имеет свой StartAPDAcquisition (), вызываемый в рабочем потоке, и StartAPDAcquisition () полагается на BeginXXX, тогда у меня фактически будет 3 потока? (1 поток пользовательского интерфейса, рабочий, поток асинхронной операции) Это правильно и желательно ли это? Я мог бы действительно использовать хорошую книгу на эти темы :)
Показать ещё 1 комментарий
1

Некоторые обновления, отражающие новые данные, которые вы предоставили:

Хотя у меня есть сомнения, что ваши методы EndXXX происходят в потоке пользовательского интерфейса, я все равно думаю, что вы должны отложить работу до фонового потока, а затем обновить интерфейс либо при запуске событий, либо при необходимости.

Поскольку вы добавили в свой пользовательский интерфейс жесткий цикл while, вам нужно вызвать Application.DoEvents, чтобы разрешить вызывать другие события.

Здесь обновленный образец, который показывает результаты в пользовательском интерфейсе по мере их возникновения:

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
    Я не думаю, что полностью понимаю ваше предложение. Как сейчас, мой класс APD уже выполняет вещи в другом потоке (чтение аппаратного обеспечения), поскольку API NI Daqmx предлагает мне методы BeginRead и EndRead. Этот API также предлагает мне возможность останавливать аппаратные объекты Task, из которых эти вызовы BeginRead и EndRead получают свои данные. Я вызываю методы stop в моем методе APD.Stop (). Моя проблема заключается в том, чтобы все эти асинхронные операции выполняли свою работу и правильно уведомляли пользовательский интерфейс о прогрессе. Я добавил больше кода ниже, чтобы объяснить проблему дальше ...
  • 0
    Я обновил свой ответ на основе предоставленной вами новой информации.
Показать ещё 2 комментария
1

Я бы попытался переместить логику мониторинга IDevice для разделения потоков для каждого устройства. Пользовательский интерфейс затем может опросить значения с помощью события таймера, нажатия кнопки или другого события, связанного с пользовательским интерфейсом. Таким образом, ваш пользовательский интерфейс будет оставаться отзывчивым, а ваши потоки выполнят весь тяжелый подъем. Вот основной пример этого, используя непрерывный цикл. Очевидно, это жестоко простой пример.

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
    Я на самом деле склонялся к решению, как это. Первоначально у меня было четыре из моих упомянутых объектов APD в моей форме. Затем я бы вызвал APD.StartAPDAquisition () для каждого из них. Каждый из 4 объектов APD имеет свои события NewCountsAvailable, связанные с методом, хранящим счетчики, поступающие от APD, в объекте документа. После вызовов APD.StartAPDAquisition () у меня был запущен цикл while, который обновлял пользовательский интерфейс каждые 200 мс. Тем не менее, поскольку все эти вызовы выполняются в потоке пользовательского интерфейса, я заметил, что запуск событий будет препятствовать выполнению цикла ...
  • 0
    Поскольку есть некоторые вещи, которые требуют уточнения относительно моего текущего кода, я добавил более подробное описание ниже.
0

Система B * * * dy captcha решила, что было бы неплохо потерять мой ответ. Я потратил полчаса, набрав без предупреждения или шанса исправить... так что мы снова и снова:

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. 
    }
}

Примечание. Я добавил некоторые вещи, которые я по ошибке оставил в исходном сообщении.

Теперь в моей форме у меня есть такой код:

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.
    } 
}

phew, я надеюсь, что я не забуду ничего, что бы это повторилось во второй раз (это научит меня не копировать его, прежде чем ударить пост).

Что я вижу, запуская этот код, это:

1) Объект APD работает как рекламируемый, он измеряет. 2) Запуск событий NewCountsAvailable и их обработчики выполняются 3) APD.StartAPDAcquisition() вызывается в потоке пользовательского интерфейса. Таким образом, BeginXXX вызывается в этом потоке. Поэтому по дизайну обратный вызов также находится в этом потоке, и, очевидно, также выполняются обработчики событий NewCountsAvailable в потоке пользовательского интерфейса. Единственное, что не в потоке пользовательского интерфейса, - это ожидание того, что аппаратное обеспечение вернет значения в пару вызовов BeginXXX EndXXX. 4) Поскольку события NewCountsAvailable очень многого срабатывают, цикл while, который я планировал использовать для обновления пользовательского интерфейса, не запускается. Обычно он запускается один раз в начале, а затем каким-то образом перерабатывается обработчиками событий, которые необходимо обработать. Я не совсем понимаю это, но это не работает...

Я решил решить эту проблему, избавившись от цикла while и поставив Forms.Timer на форму, где UpdateUI() будет вызываться из обработчика событий Tick. Однако я не знаю, будет ли это считаться "лучшей практикой". Я также не знаю, будут ли все эти обработчики событий в конечном итоге привести поток пользовательского интерфейса к обходу, мне может понадобиться добавить еще несколько таких объектов APD в будущем. Кроме того, UpdateUI() может содержать более тяжелый код для вычисления изображения на основе значений в m_Document. Таким образом, обработчик события tick также может быть утечкой ресурсов в подходе таймера. В случае, если я использую это решение, мне также необходимо иметь событие "Готово" в моем классе APD, чтобы уведомлять о завершении каждого APD.

Должен ли я, возможно, не работать с событиями для уведомления о том, что новые подсчеты доступны, но вместо этого работать с каким-то "по требованию" чтением APD.BufferedCounts и поместить все это в еще один поток? У меня действительно нет подсказки...

Мне в основном нужно чистое, легкое решение, которое хорошо масштабируется, если я добавлю еще больше APD:)

0

Я не знаю, полностью ли я понимаю. Что делать, если вы обновите объект, содержащий текущие данные. Таким образом, обратный вызов напрямую не взаимодействует с пользовательским интерфейсом. Затем вы можете обновлять пользовательский интерфейс с фиксированной скоростью, например. n раз в секунду из другого потока. См. это сообщение об обновлении пользовательского интерфейса из фонового потока. Я предполагаю, что вы используете Windows Forms, а не WPF.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню