【问题标题】:In C# what is the recommended way of passing data between 2 threads?在 C# 中,在 2 个线程之间传递数据的推荐方式是什么?
【发布时间】:2016-09-23 02:31:23
【问题描述】:

我有我的主 GUI 线程,以及在它自己的 ApplicationContext 中运行的第二个线程(以保持它活着,即使没有工作要做)。我想从我的 GUI 线程调用我的第二个线程上的一个方法,但是如果我只是调用 thread.Method();它似乎在我的主 GUI 线程上运行并导致我的 GUI 变得无响应。在不同线程上调用方法的最佳方式是什么?

更新: 我真正想做的是在 2 个线程之间进行通信,而不是与 GUI 进行通信。 GUI 恰好是需要与我的第二个线程通信的线程之一。

更新 #2: 好吧,我一定是真的错过了什么。我创建了一个事件和一个委托,并让我的工作线程订阅了该事件。但是当我调用 Invoke(MyEvent);从我的 GUI 线程中,工作线程所做的工作最终在 GUI 线程上并挂起 GUI 线程,直到它完成处理。我正在尝试做的事情是否可能,而不是对静态对象进行轮询?

【问题讨论】:

    标签: c# multithreading


    【解决方案1】:

    哇,我不敢相信人们怎么会不费心阅读这个问题。

    不管怎样,这就是我的工作。

    1. 为您创建“消息”类。这会存储您要共享的所有信息。
    2. 为每个线程创建一个队列。使用 SyncLock(C# 锁)对其进行读/写。
    3. 当您想与线程交谈时,通过将消息添加到队列中,向它发送一个消息对象,其中包含它所需的所有信息的副本
    4. 然后工作线程可以从队列中读取,按顺序读取和处理每条消息。没有消息时,只需休眠即可。

    确保不要在两个线程之间共享对象。一旦您的 GUI 线程将消息粘贴到队列中,GUI 线程就不再拥有该消息。它不能包含对消息的引用,否则您将陷入困境。

    这不会为您提供最佳性能,但对于大多数应用程序来说已经足够了。更重要的是,这会让犯错变得更加困难。

    更新:不要使用 SyncLock 和队列。而是使用 ConcurrentQueue,它将自动为您处理任何锁定。您将获得更好的性能,并且出错的可能性更小。

    【讨论】:

    • 我很惊讶另一个响应被标记为答案。 Jonathon 描述的方法对我来说总是有效得多。出于显而易见的原因,请确保锁定队列的时间不要超过 Enqueue() 或 Dequeue() 所需的时间。顺便说一句,我还发现这种方法在具有三个线程(发送、接收和处理)和两个队列(接收 => 处理和处理 => 发送)的网络通信中非常有效。
    【解决方案2】:

    .Net 已经带有一个@987654321@ 类,专门用于处理执行后台任务和与 GUI 通信。使用它。

    【讨论】:

    • 我没有尝试从我的第二个线程与 GUI 通信,我正在尝试从 GUI 与我的线程通信。
    • “与 GUI 通信”是双向的,GUI 可以调用 BackgroundWorker 线程的公共方法。
    • 是的,但这实际上不会“与后台线程通信”
    【解决方案3】:

    老兄,阅读 Albahari 的 .Net 线程免费电子书。 我以任何方式连接到它,所以这不是插头。 我读过,我的同事也读过,我用过很多次。

    我建议创建一个生产者/消费者类,您可以在其中启动等待(非阻塞)的单个线程,将任务排入其队列,并发出信号开始工作。

    只需谷歌即可。

    【讨论】:

      【解决方案4】:

      实际上,您创建了一个穷人版本的线程池。您的第二个线程只是坐在那里无所事事,如果您没有大量工作,您就不能让它为您工作。您必须将委托传递到队列中,然后您的线程会起飞并执行。

      最好的办法是做你想做的事,只使用 .NET ThreadPool 并让它工作。

      【讨论】:

        【解决方案5】:

        我假设 GUI 中的某些事件需要启动一些在后台运行的长时间运行的任务 - 有两种主要方法可以做到这一点。如果你只是想在不同的线程上调用一个方法,那么你可以通过Calling Synchronous Methods Asynchronously 来实现。我通常会这样做:

        
        
        //delegate with same prototype as the method to call asynchrously
        delegate void ProcessItemDelegate(object item);
        
        //method to call asynchronously
        private void ProcessItem(object item) { ... }
        
        //method in the GUI thread
        private void DoWork(object itemToProcess)
        {
            //create delegate to call asynchronously...
            ProcessItemDelegate d = new ProcessItemDelegate(this.ProcessItem);
            IAsyncResult result = d.BeginInvoke(itemToProcess,
                                                new AsyncCallback(this.CallBackMethod),
                                                d); 
        }
        
        //method called when the async operation has completed
        private void CallbackMethod(IAsyncResult ar)
        {
            ProcessItemDelegate d = (ProcessItemDelegate)ar.AsyncState;
            //EndInvoke must be called on any delegate called asynchronously!
            d.EndInvoke(ar);
        }
        

        请注意,当使用此方法时,回调是在后台线程上执行的,因此对 GUI 的任何更新都必须使用 Invoke 完成。

        或者,您可以使用共享状态在线程之间进行通信,并使用EventWaitHandle 来表示对共享状态的更新 - 在此示例中,GUI 中的一个方法将工作项添加到队列中以在后台处理。当工作可用时,工作线程处理队列中的项目。

        
        //shared state
        private Queue workQueue;
        private EventWaitHandle eventHandle;
        
        //method running in gui thread
        private void DoWork(Item itemToProcess)
        {
           //use a private lock object instead of lock...
           lock(this.workQueue)
           {
               this.workQueue.Add(itemToProcess);
               this.eventHandle.Set();
           }
        }
        
        //method that runs on the background thread
        private void QueueMonitor()
        {
           while(keepRunning)
           {
               //if the event handle is not signalled the processing thread will sleep here until it is signalled or the timeout expires
               if(this.eventHandle.WaitOne(optionalTimeout))
               {
                   lock(this.workQueue)
                   {
                      while(this.workQueue.Count > 0)
                      {
                         Item itemToProcess = this.workQueue.Dequeue();
                         //do something with item...
                      }
                   }
                   //reset wait handle - note that AutoResetEvent resets automatically
                   this.eventHandle.Reset();
               }
           }
        }
        

        【讨论】:

        • 那里实际上存在竞争条件。如果您在 QueueMonitor 中的锁打开和事件重置之间执行 DoWork 调用,您将错过一个事件并且在下一个 DoWork 或超时之前不会解除阻塞。
        【解决方案6】:

        Control.BeginInvoke() 的便利性很难被忽略。你不必。向您的项目添加一个新类并粘贴此代码:

        using System;
        using System.Threading;
        using System.Windows.Forms;
        
        public partial class frmWorker : Form {
          public frmWorker() {
            // Start the worker thread
            Thread t = new Thread(new ParameterizedThreadStart(WorkerThread));
            t.IsBackground = true;
            t.Start(this);
          }
          public void Stop() {
            // Synchronous thread stop
            this.Invoke(new MethodInvoker(stopWorker), null);
          }
          private void stopWorker() {
            this.Close();
          }
          private static void WorkerThread(object frm) {
            // Start the message loop
            frmWorker f = frm as frmWorker;
            f.CreateHandle();
            Application.Run(f);
          }
          protected override void SetVisibleCore(bool value) {
            // Shouldn't become visible
            value = false;
            base.SetVisibleCore(value);
          }
        }
        

        这里有一些示例代码来测试它:

          public partial class Form1 : Form {
            private frmWorker mWorker;
            public Form1() {
              InitializeComponent();
              mWorker = new frmWorker();
            }
        
            private void button1_Click(object sender, EventArgs e) {
              Console.WriteLine(System.Threading.Thread.CurrentThread.ManagedThreadId);
              mWorker.BeginInvoke(new MethodInvoker(RunThisOnThread));
            }
            private void RunThisOnThread() {
              Console.WriteLine(System.Threading.Thread.CurrentThread.ManagedThreadId);
            }
        
            private void button2_Click(object sender, EventArgs e) {
              mWorker.Stop();
            }
          }
        

        【讨论】:

          【解决方案7】:

          使用同步对象来通知线程它需要处理新数据(或 GUI 的新状态)。一种相对简单的方法是使用事件对象。以下是它的工作原理:

          1. GUI 线程和第二个线程共享一个事件对象(所以他们都知道)
          2. 第二个线程通常在某种循环中运行,并且每次都等待事件发出信号
          3. GUI 线程在需要第二个线程做某事时发出事件信号
          4. 当第二个线程完成后,它会重置事件并再次等待(或退出)

          【讨论】:

          • 您需要使用某种队列来放置数据 - 否则,初始线程可能希望将数据发送到第二个线程,但它仍在处理第一个数据项。您还需要锁定队列,这样他们就不会尝试同时访问它。
          【解决方案8】:

          在您的第二个线程中放置一个循环,该循环大部分时间都在休眠,但是每个 [Interval] 它都会唤醒并检查一个共享变量,该变量告诉它是否运行您的方法,以及该共享布尔值是否设置为是的,那么它会运行一个方法来执行您尝试执行的任何任务...在该方法中,让该方法从另一个共享变量中收集所需的数据。

          在主 GUI 线程中,将数据放入方法参数共享变量中,然后将布尔“运行”共享变量设置为 true...

          在 worker 方法中,完成后记得将共享的 bool "run" 变量重置为 false,这样循环就不会一遍又一遍地运行同一个实例......

          【讨论】:

          • 这可行,但我需要方法调用是即时的。它必须轮询 1 毫秒或其他时间,这有点多。
          • 然后让等待线程连续运行。至于您对“有点多”的担心,请理解无论您如何执行此操作,等待线程都只能在操作系统发出时间片时才能响应,并且只有在代码运行在线程还活着
          • 查看 Brian 的回答,了解无需轮询共享变量的最佳方法。
          【解决方案9】:

          您可以使用事件或 Grauenwolf 所说的 - 消息提示。我将我的每个线程都包装为一个管理单例,从那里您可以轻松实现。你甚至可以做穷人的公共财产来翻转比特。

          你也可以实现一个状态机,而不是传递消息,每个线程可以互相监控

          【讨论】:

          • 我喜欢 Grauenwolf 的建议,并最终将其与接受的答案结合起来。
          猜你喜欢
          • 2018-04-21
          • 2012-12-18
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-04-28
          • 1970-01-01
          相关资源
          最近更新 更多