【问题标题】:How do I write to a textbox from 3 threads? [duplicate]如何从 3 个线程写入文本框? [复制]
【发布时间】:2021-01-31 10:05:42
【问题描述】:

我有 3 个线程执行不同的工作,我需要收集有关它们的日志并将其写入文本框。所以我有:

public delegate void Notify(string message);

这些线程的 3 个类中的每一个都有:

public event Data.Notify Notify;

(数据只是一个静态类,我在其中保存我的应用程序的一般信息)。 在我使用的所有这 3 个类中的方法 Work() 中:

Notify?.Invoke("message");

在消息中,我知道作业是否已启动,是否成功。在我得到的表格类中:

Data.workingThread.Notify += Thread_Notify;
Data.readingThread.Notify += Thread_Notify;
Data.writingThread.Notify += Thread_Notify;
Data.writingThread.Start();
Data.workingThread.Start();
Data.readingThread.Start();

Thread_Notify 在哪里:

private void Thread_Notify(string message)
{
    tbLogs.Text += message;
}

当我运行代码时,它抛出 System.InvalidOperationException,因为我尝试访问 'tbLogs' 而不是从创建它的线程。 我为此方法和 lock(tbLogs) 尝试了 async/await,但这些对我不起作用

【问题讨论】:

  • 您可以将BeginInvoke() 发送至tbLogs.AppendText(message)。或者将事件替换为 IProgress<T> 委托和 Report() 到 UI 线程。在这种情况下,使用任务而不是线程更简单。您多久向该控件添加一次文本?

标签: c# multithreading winforms


【解决方案1】:

据我所知,只有在主窗口线程中才能直接管理文本框。 由于您的事件(Thread_Notify)可以从其他线程调用,因此您不能直接使用它,因为如果它在与主窗口不同的线程中运行,则会抛出异常。

为了让它工作,你必须这样写通知:

private void Thread_Notify(string message)
{
   Dispatcher.Invoke(new Action(() =>
   {
      tbLogs.Text += message;
   }), DispatcherPriority.Background);
}

【讨论】:

【解决方案2】:

看看Control.InvokeRequiredpropertyInvokemethod

使用它们在 UI 线程上调度文本框更改。

例子:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        Random rnd = new Random();
        Action GetAction(string threadName) => () =>
        {
            for (int i = 0; i < 5; i++)
            {
                var tid = Thread.CurrentThread.ManagedThreadId;
                Thread_Notify($"{threadName} ({tid}): {i} ");
                int sleepMillis = 0;
                lock (rnd) sleepMillis = rnd.Next(0, 400);
                Thread.Sleep(sleepMillis);
            }
        };

        Task.WhenAll(
            Task.Run(GetAction("worker1")),
            Task.Run(GetAction("worker2")),
            Task.Run(GetAction("worker3"))
        ).ContinueWith(_ => Thread_Notify("Done."));
    }
    private void Thread_Notify(string message)
    {
        Action setText = delegate { tbLogs.Text += message + "\r\n"; };
        if (tbLogs.InvokeRequired)
        {
            tbLogs.Invoke(setText);
        }
        else
        {
            setText();
        }
    }
}

另一种选择是使用System.Reactive.Linq 库为您进行调度:

  1. 创建主题。
  2. 在工作线程中调用_subject.OnNext(message)
  3. 更改订阅中的文本框。

例子:

using System.Reactive.Linq;
using System.Reactive.Subjects;
// ...
public partial class Form1 : Form
{
    private readonly Subject<string> _messages = new Subject<string>();
    public Form1()
    {
        InitializeComponent();
        Random rnd = new Random();
        Action GetAction(string threadName) => () =>
        {
            for (int i = 0; i < 5; i++)
            {
                var tid = Thread.CurrentThread.ManagedThreadId;
                _messages.OnNext($"{threadName} ({tid}): {i} ");
                int sleepMillis = 0;
                lock (rnd) sleepMillis = rnd.Next(0, 400);
                Thread.Sleep(sleepMillis);
            }
        };

        Task.WhenAll(
            Task.Run(GetAction("worker1")),
            Task.Run(GetAction("worker2")),
            Task.Run(GetAction("worker3"))
        ).ContinueWith(_ => _messages.OnNext("Done."));

        _messages
            .ObserveOn(this)
            .Subscribe(msg => tbLogs.AppendText(msg + "\r\n"));
    }
}

【讨论】:

  • 没有必要检查InvokeRequired:这显然是必需的,因为该事件是由工作线程引发的。在这里调用Invoke() 可能会陷入僵局。几乎可以肯定 Control 或其容器是否同时被释放或以其他方式阻塞(除非在某个时候调用了Thread.Join(),那么它肯定是101%)。 BeginInvoke() 在这里更安全。并发是唯一的问题
猜你喜欢
  • 2010-10-05
  • 2012-04-19
  • 2018-03-10
  • 2019-11-01
  • 2014-05-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多