【问题标题】:UI update with multiple concurrent operations具有多个并发操作的 UI 更新
【发布时间】:2009-08-29 15:24:01
【问题描述】:

我正在使用 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 的回调 en 还会触发一个指示数据可用的事件。

我有多达 4 个这样的检测器对象作为我的 UI 表单的成员。我依次调用所有它们的 Start() 方法来开始我的测量。这有效,并且所有四个都会触发 NewCountsAvailable 事件。

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

由于我是 C# 线程编程的新手,我现在陷入困境;

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

我真的不知道!

【问题讨论】:

  • 您使用的是什么版本的 .NET?
  • nidaq 在 UI 线程上的回调如何? (意思是:你确定吗?)
  • 您需要在 GUI 中显示多少数据?我们在说视频吗?我同意 gimpf 的评论,请确定。
  • @James:基本上.NET 3.5,不需要限制.NET版本。 @ Gimf & Henk: APD.StartAPDAquisition() 在 ButtonClick 事件处理程序中的 UI 线程上调用。因此,也在 UI 线程上调用 BeginReadMultiSampleUInt32(...)。这也意味着回调将在每个定义的这个线程上。我检查了这一点,使用在 Threading 类中获取线程句柄的调用。数据量大约是 4 到 5 个数组,包含 512*512 个元素……当然没有视频流。

标签: c# concurrency user-interface multithreading


【解决方案1】:

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

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

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

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

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

此外,对于良好的 UI,最重要的是,这种方法可用于允许触发多个“数据就绪”事件并将其滚动到单个 UI 更新中。这意味着如果 10 条数据连续完成,则 UI 会更新一次,而不是在 UI 重绘(不必要地)10 次时您的窗口闪烁几秒钟。

【讨论】:

  • 听起来很合理。由于缺乏线程经验,我想知道 -> 如果我的 APD 对象在工作线程中运行并且它会触发一个事件并且事件处理程序实际上是我的表单类上的一个方法。事件处理程序将在哪个线程中执行?而且,如果我想像你说的那样设置这个标志,我是否必须使用 InvokeRequired 调用来安全地设置这个标志?我是否正确假设事件处理程序将从工作线程运行?
  • 另外,当我的 APD 对象在工作线程中调用它的 StartAPDAcquisition() 并且 StartAPDAcquisition() 依赖于 BeginXXX 时,我实际上会有 3 个线程吗? (1 个 UI 线程,Worker,异步操作的线程)这是正确的,这是可取的吗?我真的可以在这些主题上使用一本好书:)
  • 事件处理程序总是在引发事件的线程(即您的情况下的工作线程)中调用。要在 UI 线程中执行代码,您可以使用 Invoke/BeginInvoke,或者我上面概述的延迟方法。 (延迟方法在效果上与 BeginInvoke 非常相似,不同之处在于可以将多个事件合并到单个更新中)
【解决方案2】:

我会尝试将 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();
    }
}

【讨论】:

  • 我实际上倾向于这样的解决方案。最初,我的表单上有四个我提到的 APD 对象。然后我会在他们每个人上调用 APD.StartAPDAquisition()。 4 个 APD 对象中的每一个都将其 NewCountsAvailable 事件连接到将来自 APD 的计数存储在文档对象中的方法。在调用 APD.StartAPDAquisition() 之后,我运行了一个 while 循环,它将每 200 毫秒更新一次 UI。然而,由于所有这些调用都在 UI 线程上,我注意到触发的事件会阻止循环运行......
  • 由于我当前的代码有一些需要澄清的地方,我在下面添加了更详细的描述。
【解决方案3】:

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

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

由于您在 UI 中添加了紧密的 while 循环,您需要调用 Application.DoEvents 以允许调用其他事件。

这是一个更新后的示例,在 UI 中显示结果:

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

【讨论】:

  • 我不认为我完全理解你的建议。现在的情况是,我的 APD 类已经在另一个线程(读取硬件)中完成了工作,因为 NI Daqmx API 为我提供了 BeginRead 和 EndRead 方法。此 API 还为我提供了停止这些 BeginRead 和 EndRead 调用从中获取数据的硬件任务对象的可能性。我在 APD.Stop() 方法中调用此停止方法。我的问题在于让所有这些异步操作完成他们的业务并正确通知 UI 进度。我在下面添加了更多代码以进一步解释问题...
  • 我已根据您提供的新信息更新了我的答案。
  • 虽然我怀疑您的 EndXXX 方法是否发生在 UI 线程上 -> 据我所知,它们确实如此。也许有人可以澄清这一点?
  • 稍后我会检查您的建议。今天早上,我尝试了摆脱 while 循环的解决方案,而是选择了一个在计时时调用 UpdateUI() 的计时器。 UpdateUI() 包含对 DoEvents() 的调用。我看到的是,只要传入的数据率不太高,这似乎就可以工作。如果是,我会在 DoEvents() 上遇到 StackOverflow 异常。尚不确定这一切意味着什么,但我一定会对其进行全部测试并将最终解决方案发布回此处...
【解决方案4】:

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

【讨论】:

    【解决方案5】:

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

    唷,我希望我不会忘记第二次输入的任何内容(这将教会我在点击帖子之前不要复制它)。

    我看到运行这段代码是这样的;

    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 中的值计算图像。因此,滴答事件处理程序也可能是计时器方法中的资源消耗。如果我使用这个解决方案,我还需要在我的 APD 类中有一个“完成”事件来通知每个 APD 何时完成。

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

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

    【讨论】:

      猜你喜欢
      • 2017-12-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多