问题描述
对 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 的内容。