C# 字符串变空

问题描述

对 C# 和一般编码相对较新(此处为第一篇文章)。我有一个 WinForms 本地应用程序,其中一些信息在 ReadOnly(true) RichTextBox显示用户。几乎我所有的课程都需要向 RichTextBox 发送信息。为了简化此过程,我在静态类中创建了一个方法,该方法使用锁定的委托将信息发送到 RichTextBox。这是一个示例:

static class MyClass
{
    public delegate void MessageReceivedEventHandler(string message);
    public static event MessageReceivedEventHandler messageReceivedEventHandler;

    public static void MessageBox(string message)
    {
        lock (messageReceivedEventHandler)
        {
            //Thread.Sleep(20);
            messageReceivedEventHandler?.Invoke(message);
        }
    }
}
partial class MyForm : Form
{
    public MyForm()
    {
        MyClass.messageReceivedEventHandler += OnMessageReceived;
    }

    private void OnMessageReceived(string message)
    {
        richTextBox1.Text = richTextBox1.Text.Insert(0,$" {message}\n");
    }
    private void Button1_click()
    {
        MyClass.MessageBox("This should be working!");
        //Add more work here...
    }
}

上面的代码只会打印“这应该可以工作!”在 RichtTextBox 内。

问题是 RichTextBox1 中的文本有时会变空。当快速连续调用 MessageBox 方法时,似乎会出现此问题。我的假设是,由于我有不同的任务同时运行(在我的代码的其他部分),可能是两个任务试图使用相同的静态资源,因此使用了 Lock。但我仍然有问题。

添加 Thread.Sleep(20) 似乎解决了这个问题,但这远非优雅/健壮。当 Sleep 中的时间

编辑1: 为了澄清我所说的“字符串变空”是什么意思,它意味着 RichTextBox1 中的文本在某些时候是 == "",这不应该发生,因为代码总是插入文本,而不是替换它。 OnMessageReceived 方法是对 RichTextBox 文本执行操作的唯一位置。

编辑2: 我看到许多与正在运行的其他任务相关的问题。首先,是的,它是一个多线程应用程序。这些任务和我的主要表单之间的唯一关系是我上面写的“打印”函数。为了提供更多上下文,此应用程序用于控制步进电机相对于电信号的位置。这样做时,我需要在主表格中打印重要信息。这就是为什么在我的 RichTextBox(我打印信息的地方)中丢失信息是一个问题的原因。我在 RichTextBox 中丢失文本的可能原因应该是该线程的重点。

请记住,这是个人的业余项目,而不是大型应用程序。

谢谢, 洛朗

解决方法

您的代码中存在多个问题。

首先,您不应该锁定公共对象,因为这允许其他线程锁定同一个对象,从而冒着互锁线程的风险。其次,您的症状表明多个线程正在尝试访问资源。与其依赖复杂的线程锁定代码,不如在 UI 上下文中安排 UI 操作,这将允许从后台任务调用添加消息。

最好的方法是使用 Control.BeginInvoke()

你不能在任何地方复制你的表单实例,所以我们将公开一个静态方法。您可以将类设为单例,但如果您需要多个不起作用的实例。我会举一个更通用的例子。当调用静态方法时,您将无法再访问表单实例,因此我们将使用带有事件和委托的 IOC 模式。

让我们创建一个私有静态事件,所有实例都将在构造函数中注册回调。当静态方法引发静态事件时,将调用所有实例回调。回调将安排对其文本框的修改。

partial class MyForm : Form
{
    private class MessageWriteRequestedEventArgs : EventArgs
    {
        public string Message { get; }
        public MessageWriteRequestedEventArgs(string message)
        {
            Message = message;
        }
    }

    private static event EventHandler<MessageWriteRequestedEventArgs> MessageWriteRequested;

    public MyForm()
    {
        MessageWriteRequested += OnMessageWriteRequested;
    }

    public static void WriteMessage(string message)
    {
        MessageWriteRequested?.Invoke(this,new MessageWriteRequestedEventArgs(message));
    }

    private void OnMessageWriteRequested(object sender,MessageWriteRequestedEventArgs e)
    {
        richTextBox1.BeginInvoke(() => WriteMessageSafe(e.message));            
    }

    private void WriteMessageSafe(string message)
    {
        richTextBox1.Text = richTextBox1.Text.Insert(0,$" {message}\n");
    }

    private void Button1_click()
    {
        // you're on ui context,you're safe to access local ui resources
        WriteMessageSafe("This should be working!");
        // if you have multiple MyForm instances,you need to use the event
        WriteMessage("Broadcasting my tralala");
    }
}

如果您需要从其他任何地方写入文本框:

// do stuff
MyForm.WriteMessage("Ho Ho Ho !");
,

.NET 已经包含一个类,用于以线程安全的方式报告异步操作的进度(或任何其他信息),Progress< T>。它不需要锁定,更好的是,它将发送方和接收方解耦。许多长时间运行的 BCL 操作都接受 IProgress<T> 参数来报告进度。

您还没有解释表单中发生了什么,或者报告数据的任务是什么。假设生产者是另一种形式相同的方法,您可以在启动异步操作的相同方法中创建一个 Progress<T> 实例,例如:

async void Button1_Click()
{
    var progress=new Progress<string>(ReportMessage);

    ReportMessage("Starting");
    await Task.Run(()=>SomeLongOp(progress));
    ReportMessage("Finished");
}

void SomeLongOp(IProgress<string> progress)
{

    for(int i=0;i<1000000;i++)
    {
        ...
        progress.Report($"Message {i}");
        ...
    }

}

void ReportMessage(string message)
{
    richTextBox1.Text = richTextBox1.Text.Insert(0,$" {message}\n");
}

通过使用 IProgress< T>SomeLongOp 方法不会绑定到特定的表单或全局实例。它很容易成为另一个类的方法

发布大量消息

假设您有 很多 个工人,他们做很多事情,例如监控很多设备,并希望他们所有人都将消息发布到同一个 Log 文本框或RTF 盒。 Progress< T>“简单地”在其原始同步上下文中执行报告委托或事件处理程序。它没有异步 Report 方法,也不能对消息进行排队。在真正高流量的环境中,同步切换可能会延迟所有工作人员。

对此的内置答案是使用发布/订阅类之一,例如 ActionBlock< T>Channel

ActionBlock< T> 使用默认在 ThreadPool 上运行的工作任务按顺序处理其输入队列中的消息。这可以通过在其执行选项中指定不同的 TaskScheduler 来更改。默认情况下,它的输入队列是无界的。

可以使用 ActionBlock 接收来自多个 worker 的消息并将它们显示在文本框中。该块可以在构造函数中创建,并作为 ITargetBlock<T> 接口传递给所有工作人员:


ActionBlock<string> _logBlock;

public MyForm()
{

    var options=new ExecutionDataFlowBlockOptions {
        TaskScheduler=TaskScheduler.FromCurrentSynchronizationContext();
    };
    _block=new ActionBlock<string>(ReportMessage,options);

}

现在好戏开始了。如果工作人员是由表单本身创建的,工作人员可以直接发布到块:

public async void Start100Workers_Click(...)
{
    var workers=Enumerable.Range(0,100)
                          .Select(id=>DoWork(id,_block));
    await Task.WhenAll(workers);
}

async Task DoWork(int id,ITargetBlock<string> logBlock)
{
    .....
    await logBlock.SendAsync(message);
    ...
}

或者块可以通过公共属性公开,以便应用程序中的其他类/表单可以发布到它。

public ITargetBlock<string> LogBlock=>_block;
,

我将展示一种简单的方法来完成我认为您所追求的事情。

我从一个 .NET Core 3.1 Win 表单应用程序开始。我在表单中添加了一个富文本控件。我在表单中添加了一个按钮。

我添加了一个 TaskCompletionSource 作为实例属性 - 这将用于控制充当您描述的工作人员的任务。

CancellationTokenSource sharedCancel = new CancellationTokenSource();

我创建了一个接口来表示接受你描述的消息的东西:

public interface IMyMessageSink
{
    Task ReceiveMessage(string message);
}

我让我的表单支持这个接口。

public partial class Form1 : Form,IMyMessageSink

ReceiveMessage 方法如下所示:

    public Task ReceiveMessage(string message)
    {
        if(this.sharedCancel == null || this.sharedCancel.IsCancellationRequested)
            return Task.FromResult(0);

        this.Invoke(new Action<Form1>((s) => this.richTextBox1.Text = this.richTextBox1.Text.Insert(0,$"{message}\n")),this);

        return Task.FromResult(0);
    }

您将看到 Invoke 处理同步回 UI 线程。

这可能应该使用 BeginInvoke,然后将 APM 转换为异步任务,您可以阅读有关 here 的内容。但是对于 SO 答案,上面的简单代码就足够了。

另请注意,没有错误处理。您需要将其添加到生成器和按钮处理程序中。

接下来我创建了一个类来表示创建消息的东西。此类采用创建的接口和取消令牌。它看起来像这样:

public class MyMessageGenerator
{
    CancellationToken cancel;
    IMyMessageSink sink;

    public MyMessageGenerator(CancellationToken cancel,IMyMessageSink sink)
    {
        this.cancel = cancel;
        this.sink = sink;
    }

    public async Task GenerateUntilCanceled()
    {
        try
        {
            while (!this.cancel.IsCancellationRequested)
            {
                await sink.ReceiveMessage(this.GetHashCode().ToString());
                await Task.Delay(5000,this.cancel);
            }
        }
        catch (OperationCanceledException)
        { }
    }
}

在按钮处理程序中,我们创建消息生成器。

    async void button1_Click(object sender,EventArgs e)
    {
        if (null == this.sharedCancel)
            return;

        await Task.Run(() => new MyMessageGenerator(this.sharedCancel.Token,this).GenerateUntilCanceled());
    }

最后我为表单关闭事件添加了一个覆盖:

    protected override void OnClosing(CancelEventArgs e)
    {
        if (null != this.sharedCancel)
        {
            this.sharedCancel.Cancel();
            this.sharedCancel.Dispose();
            this.sharedCancel = null;
        }

        base.OnClosing(e);
    }

如果应用程序变得更大、更复杂,您可能会受益于添加使用 DI 容器公开的服务。您可以阅读有关将 DI 添加到 winforms 应用 here 的内容。