Я разрабатываю приложение на С#, используя 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 объекта-детектор?
У меня действительно нет подсказки!
Я бы использовал простую отложенную систему обновлений.
1) Рабочие потоки сигнализируют "данные готовы", поднимая событие
2) Пользовательский поток прослушивает событие. Когда он получен, он просто устанавливает флаг "Обновление потребностей данных" и возвращает, поэтому минимальная обработка происходит в самом событии.
3) В потоке пользовательского интерфейса используется таймер (или сидит на события Application.Idle), чтобы проверить флажок "Обновление потребностей данных" и, при необходимости, обновить интерфейс. Во многих случаях UI нужно обновлять только один или два раза в секунду, поэтому это не требует большого количества времени процессора.
Это позволяет UI продолжать работать как обычно все время (оставаясь интерактивным для пользователя), но в течение короткого периода времени некоторые данные готовы, он отображается в пользовательском интерфейсе.
Кроме того, и что самое важное для хорошего пользовательского интерфейса, этот подход можно использовать, чтобы позволить нескольким событиям, готовым к "подготовке данных", запускаться и переноситься в одно обновление пользовательского интерфейса. Это означает, что если 10 фрагментов данных будут завершены с тесной последовательностью, пользовательский интерфейс обновится один раз, а не будет мерцать в течение нескольких секунд, когда пользовательский интерфейс будет повторно (без необходимости) 10 раз.
Некоторые обновления, отражающие новые данные, которые вы предоставили:
Хотя у меня есть сомнения, что ваши методы 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);
}
}
}
Я бы попытался переместить логику мониторинга 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();
}
}
Система 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:)
Я не знаю, полностью ли я понимаю. Что делать, если вы обновите объект, содержащий текущие данные. Таким образом, обратный вызов напрямую не взаимодействует с пользовательским интерфейсом. Затем вы можете обновлять пользовательский интерфейс с фиксированной скоростью, например. n раз в секунду из другого потока. См. это сообщение об обновлении пользовательского интерфейса из фонового потока. Я предполагаю, что вы используете Windows Forms, а не WPF.