具有多个并发操作的UI更新

时间:2009-08-29 15:24:01

标签: c# concurrency user-interface multithreading

我正在使用National Instruments Daqmx在C#中开发一个应用程序,用于在某些硬件上执行测量。

我的设置包含多个检测器,我必须在一段时间内从中获取数据,同时使用此数据更新我的UI。

 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还会触发一个指示数据可用的事件。

我最多有4个这样的探测器对象作为我的UI表单的成员。我按顺序对所有这些方法调用Start()方法来开始我的测量。这有效,NewCountsAvailable事件将触发所有四个事件。

由于我的实现的性质,在UI线程上调用BeginXXX方法,并且回调和事件也在此UI线程上。因此我不能在我的UI线程中使用某种while循环来不断地用新数据更新我的UI,因为事件不断激发(我试过这个)。我也不想在四个NewCountsAvailable事件处理程序中的每一个中使用某种UpdateUI()方法,因为这会加载我的系统太多。

由于我不熟悉C#中的线程编程,我现在卡住了;

1)处理这种情况的“正确”方法是什么? 2)我的探测器对象的实现是否合理?我应该从另一个线程调用这四个探测器对象上的Start()方法吗? 3)我可以每隔几百毫秒使用一个计时器更新我的UI,而不管4个探测器对象在做什么?

我真的不知道!

5 个答案:

答案 0 :(得分:4)

我使用简单的延迟更新系统。

1)工作线程通过引发事件来发出“数据就绪”信号

2)UI线程侦听事件。当它被接收时,它只是设置一个“数据需要更新”标志并返回,因此事件本身发生的处理最少。

3)UI线程使用计时器(或坐在Application.Idle事件上)来检查“数据需要更新”标志,并在必要时更新UI。在许多情况下,UI只需要每秒更新一次或两次,因此不需要耗费大量的CPU时间。

这允许UI一直保持正常运行(对于用户保持交互),但在一些数据准备好的短时间内,它将显示在UI中。

此外,最重要的是,对于良好的用户界面,此方法可用于允许触发多个“数据就绪”事件并将其转换为单个UI更新。这意味着,如果紧接着连续完成10个数据,则UI会更新一次而不是窗口闪烁几秒钟,因为UI会重新绘制(不必要地)10次。

答案 1 :(得分:1)

我会尝试将IDevice监控逻辑移动到每个设备的单独线程。然后,UI可以通过计时器事件,按钮单击或一些其他UI相关事件来轮询值。这样你的UI将保持响应,你的线程正在做所有繁重的工作。以下是使用连续循环的基本示例。显然,这是一个非常简单的例子。

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();
    }
}

答案 2 :(得分:1)

一些更新反映了您提供的新数据:

虽然我怀疑您的EndXXX方法是在UI线程上发生的,但我仍然认为您应该将工作产生到后台线程,然后在触发事件或根据需要更新UI。 / em>的

因为您在UI中添加了一个紧凑的while循环,所以您需要调用 Application.DoEvents 以允许调用其他事件。< / em>的

这是一个更新的示例,显示用户界面中的结果:

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);
         }
     }
}

答案 3 :(得分:0)

我不知道我是否完全理解。如果更新包含当前数据的对象,该怎么办?因此回调不直接与UI交互。然后,您可以以固定的费率更新用户界面,例如来自另一个线程的每秒n次。 See this post on updating UI from a background thread。我假设你使用的是Windows Forms而不是WPF。

答案 4 :(得分:0)

B * * * dy验证码系统决定丢失我的答案是个好主意我花了半个小时打字而没有警告或有机会纠正......所以我们再来一次:

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.
    } 
}
p pw,我希望我第二次不要忘记任何事情(这会教我在击中Post之前不要复制它)。

我看到运行此代码的是那个;

1)APD对象的工作方式与广告一致,它测量。 2)NewCountsAvailable事件触发并执行其处理程序 3)在UI线程上调用APD.StartAPDAcquisition()。因此在这个线程上也调用了BeginXXX。因此,按照设计,回调也在这个线程上,显然NewCountsAvailable事件处理程序也在UI线程上运行。唯一不在UI线程上的是等待硬件将值返回到BeginXXX EndXXX调用对。 4)因为NewCountsAvailable事件触发了很多,我打算用来更新UI的while循环不会运行。通常它在开始时运行一次,然后以某种方式由需要处理的事件处理程序中断。我不完全理解这一点,但它不起作用......

我正在考虑解决这个问题,方法是删除while循环,并在表单上放置一个Forms.Timer,从Tick事件处理程序调用UpdateUI()。但是,我不知道这是否会被视为“最佳做法”。我也不知道所有这些事件处理程序是否最终都会使UI线程进行爬行,我可能需要在将来添加更多这些APD对象。此外,UpdateUI()可能包含一些较重的代码,用于根据m_Document中的值计算图像。因此tick tickhandler也可能是计时器方法中的资源消耗。如果我使用这个解决方案,我还需要在我的APD类中有一个“完成”事件,以便在每个APD完成时通知。

我是否应该处理事件以通知新的计数可用,而是使用某种“按需”读取APD.BufferedCounts并将整个事情放在另一个线程中?我真的没有线索......

我基本上需要一个干净,轻量级的解决方案,如果我添加更多APD,它可以很好地扩展:)