消费者/生产者锁定 GUI 线程

Posted

技术标签:

【中文标题】消费者/生产者锁定 GUI 线程【英文标题】:Consumer/producer locking GUI thread 【发布时间】:2018-05-22 08:20:22 【问题描述】:

我制作了一个示例消费者/生产者线程应用程序,因此我可以学习如何正确使用它。

我希望它允许一个线程向 GUI 线程发送命令,用内容更新 GUI。

我可以正常工作,但有一个小问题。 GUI 线程是我的消费者线程,所以我总是让它检查新命令(使用 while 循环)。问题是由于这个 while 循环,GUI 永远不会显示,因为它总是卡在 while 循环中。请注意,字符串 Queue 最终将被替换为更复杂的对象(包含数据和命令类型的对象)。

我不确定如何让 GUI 线程在不中断 GUI 功能的情况下使用命令。我做错了吗?

这是我的 Form1.cs 代码(该表单仅包含 1 个 RichTextBox 用于显示名为 OutputBox 的输出)。

using System;
using System.Collections.Generic;
using System.Threading;
using System.Windows.Forms;

namespace MultiThreading

   class ThreadCommandQueue
   
      public static ThreadCommandQueue instance = new ThreadCommandQueue();

      private Queue<string> m_queue;
      private Object m_lock;

      public static ThreadCommandQueue GetInstance()
      
         return instance;
      

      private ThreadCommandQueue()
      
         m_queue = new Queue<string>();
         m_lock = new Object();
      

      public void Add(
         string data_to_add)
      
         lock (m_lock)
         
            m_queue.Enqueue(data_to_add);
         
      

      public string Get()
      
         lock (m_lock)
         
            if (m_queue.Count > 0)
            
               return m_queue.Dequeue();
            

            return null;
         
      
   

   public partial class Form1 : Form
   
      public Form1()
      
         InitializeComponent();
      

      private void PopulateThreadCommandQueue()
      
         int i = 0;
         while(true)
         
            ThreadCommandQueue.GetInstance().Add("Item #: " + i.ToString());
            i++;
         
      

      private void Form1_Load(object sender, EventArgs e)
      
         // Create the Command Queue....
         ThreadCommandQueue.GetInstance();

         // Create a Testing Producer Thread
         Thread ProducerThread = new Thread(PopulateThreadCommandQueue);
         ProducerThread.Start();

         // The GUI thread is the Consumer, so keep checking the CommandQueue for data...
         while(true)
         
            string latest = ThreadCommandQueue.GetInstance().Get();
            if(latest != null)
            
               OutputBox.Text += latest + "\n";
            
         
      
   

【问题讨论】:

不清楚你在问什么。如果练习的目的是显式实现消费者,则该对象必须在 UI 线程之外的线程中执行。就那么简单。对于任何事情,您都不能阻止 UI 线程。但是,UI 线程本身就是一种消费者线程;它将执行您传递给Control.Invoke()Control.BeginInvoke() 方法的任何委托。这种隐式使用已经存在的 UI 线程使用者实现将适用于大多数实际场景,并且可能适用于您的情况。但是您的问题根本不够清楚,无法知道 尝试使用 BlockingCollection,在您的代码中读取时会浪费 CPU。当然,在某些情况下 spinwait 被占用,但不是在这个。 【参考方案1】:

使用并发队列。因此从队列中添加和获取不需要锁定。

此外,您不会实时从 UI 线程(while 循环)连续接收命令。如果您有这种情况,请使用单独的线程来接收结果。

然后,您可以从接收者线程中使用 Invoke 命令更新 UI,如下所示。

//This method called from receiver thread 
public void UpdateForm(Data d) 

   if(this.InvokeRequired) 
   
      this.Invoke(new MethodInvoker(() => this.UpdateFormUI(r)));
   
   else 
   
      this.UpdateFormUI(data)
   


public void UpdateFormUI(Data d)

   //Does actual ui update

【讨论】:

不,它不是线程安全的。文档说“如果您需要同时从多个线程访问集合,请使用 ConcurrentQueue 或 ConcurrentStack。” 太棒了,谢谢!你让我看起来除了使用 while 循环之外还有另一个选择。有没有办法让我只在有可用命令时尝试读取命令?【参考方案2】:

好的,这里的问题是您需要一直轮询消息循环以使 GUI 正常工作,并且您还需要一直轮询 IPC 命令队列以使命令正常工作,并且您需要在以下时间进行轮询同一时间在同一线程上。

有多种方法可以解决此问题,但最简单的方法是处理消息循环,当没有要处理的内容时,执行 IPC 命令队列处理。对于 WinForms 应用程序,这将类似于:

public Form1()

   InitializeComponent();
   Application.Idle += (sender, eargs) => ProcessCommands();


private void ProcessCommands()

    while(true)
    
      string latest = ThreadCommandQueue.GetInstance().Get();
      if(string.IsNullOrEmpty(latest)) return;
      OutputBox.Text += latest + "\n";
    

【讨论】:

以上是关于消费者/生产者锁定 GUI 线程的主要内容,如果未能解决你的问题,请参考以下文章

如何使用互斥锁在 C 中执行生产者-消费者程序

单一生产者单一消费者的无锁循环缓冲区

day24

Java学习第二十四天

c_cpp 生产者消费者问题使用锁定和转向变量实现

c_cpp 生产者消费者问题使用锁定和转向变量实现