C# 线程安全日志表单
我有一个表单负责将所有线程的所有必要信息记录到它的richTextBox中。当我想从不同的线程访问它的控件(将文本附加到richTextBox)时,我使用Invoke,它可以完美地工作 但我需要在第一条消息将被追加到richTextBox时显示这个日志表单,我不知道我的线程中的哪个将首先执行此操作。 另外,当我关闭日志表单时,我希望在下一条消息出现时再次显示它(在这种情况下,我仍然不知道哪个线程将首先调用它) 我试图在新线程中通过Application.Run(ApplicationContext)创建此表单,但这些解决方案都不起作用C# 线程安全日志表单,c#,multithreading,forms,logging,C#,Multithreading,Forms,Logging,我有一个表单负责将所有线程的所有必要信息记录到它的richTextBox中。当我想从不同的线程访问它的控件(将文本附加到richTextBox)时,我使用Invoke,它可以完美地工作 但我需要在第一条消息将被追加到richTextBox时显示这个日志表单,我不知道我的线程中的哪个将首先执行此操作。 另外,当我关闭日志表单时,我希望在下一条消息出现时再次显示它(在这种情况下,我仍然不知道哪个线程将首先调用它) 我试图在新线程中通过Application.Run(ApplicationContex
你有什么提示吗?与其关闭/重新打开窗口,通常更容易隐藏和取消隐藏 无论何时创建主窗口,都要创建窗口,不要关闭它。添加一个onbeforeclose(或任何再次调用的…)事件处理程序,该处理程序取消用户启动的关闭,而是隐藏窗口 现在,您已经在使用线程安全调度程序来修改窗口内容:只需在事件处理程序中添加一行即可取消隐藏窗口(如果窗口已可见,则不会执行任何操作),现在就可以开始了 顺便说一句,在这些场景中有一个有用的东西是自定义TextWriter子类。您可以按如下方式制作:
public abstract class AbstractTextWriter : TextWriter {
protected abstract void WriteString(string value);
public override Encoding Encoding { get { return Encoding.Unicode; } }
public override void Write(char[] buffer, int index, int count) {
WriteString(new string(buffer, index, count));
}
public override void Write(char value) {
WriteString(value.ToString(FormatProvider));
}
public override void Write(string value) { WriteString(value); }
//subclasses might override Flush, Dispose and Encoding
}
public class DelegateTextWriter : AbstractTextWriter {
readonly Action<string> OnWrite;
readonly Action OnClose;
static void NullOp() { }
public DelegateTextWriter(Action<string> onWrite, Action onClose = null) {
OnWrite = onWrite;
OnClose = onClose ?? NullOp;
}
protected override void WriteString(string value) { OnWrite(value); }
protected override void Dispose(bool disposing) {
OnClose(); base.Dispose(disposing);
}
}
。。。并在任何地方使用它,甚至可能执行Console.settout
捕获Console.Write的输出。在多个线程上调用此TextWriter上的Write是可以的,因为实现是自同步的
你可以这样测试它
var threadSafeLogWriter = new DelegateTextWriter(str => {
Action updateCmd = ()=>{
myControl...//append text to whatever control here
myControl.Show();
};
if(myControl.InvokeRequired) myControl.BeginInvoke(updateCmd);
else updateCmd();
});
Console.SetOut(threadSafeLogWriter);
Parallel.For(0,100, i=>{
threadSafeLogWriter.Write("Hello({0}) ", i);
Thread.Yield();
Console.WriteLine("World({0})!",i);
});
如果你对小消息做了大量的日志记录,你可能会收到很多BeginInvoke呼叫,而且这些呼叫速度很慢。作为一种优化,您可以将所有日志消息排队到ConcurrentQueue
或其他一些同步结构中,并且只有当队列在排队之前为空时才开始唤醒。这样,在UI更新之间,您通常只进行一次启动;当用户界面开始做它的事情时,它会清除标志,然后立即追加所有排队的文本。然而,由于所有做日志记录的线程都只是按照它们进入日志记录程序的任何顺序转储它们的消息,因此有很多小日志语句对可读性非常不利;最好一次记录尽可能大的字符串,以确保它不会被其他线程消息中断;如果你这样做了,你就不会有太多的BeginInvoke和性能。问题会少一些。与其关闭/重新打开窗口,通常更容易隐藏和取消隐藏
无论何时创建主窗口,都要创建窗口,不要关闭它。添加一个onbeforeclose(或任何再次调用的…)事件处理程序,该处理程序取消用户启动的关闭,而是隐藏窗口
现在,您已经在使用线程安全调度程序来修改窗口内容:只需在事件处理程序中添加一行即可取消隐藏窗口(如果窗口已可见,则不会执行任何操作),现在就可以开始了
顺便说一句,在这些场景中有一个有用的东西是自定义TextWriter子类。您可以按如下方式制作:
public abstract class AbstractTextWriter : TextWriter {
protected abstract void WriteString(string value);
public override Encoding Encoding { get { return Encoding.Unicode; } }
public override void Write(char[] buffer, int index, int count) {
WriteString(new string(buffer, index, count));
}
public override void Write(char value) {
WriteString(value.ToString(FormatProvider));
}
public override void Write(string value) { WriteString(value); }
//subclasses might override Flush, Dispose and Encoding
}
public class DelegateTextWriter : AbstractTextWriter {
readonly Action<string> OnWrite;
readonly Action OnClose;
static void NullOp() { }
public DelegateTextWriter(Action<string> onWrite, Action onClose = null) {
OnWrite = onWrite;
OnClose = onClose ?? NullOp;
}
protected override void WriteString(string value) { OnWrite(value); }
protected override void Dispose(bool disposing) {
OnClose(); base.Dispose(disposing);
}
}
。。。并在任何地方使用它,甚至可能执行Console.settout
捕获Console.Write的输出。在多个线程上调用此TextWriter上的Write是可以的,因为实现是自同步的
你可以这样测试它
var threadSafeLogWriter = new DelegateTextWriter(str => {
Action updateCmd = ()=>{
myControl...//append text to whatever control here
myControl.Show();
};
if(myControl.InvokeRequired) myControl.BeginInvoke(updateCmd);
else updateCmd();
});
Console.SetOut(threadSafeLogWriter);
Parallel.For(0,100, i=>{
threadSafeLogWriter.Write("Hello({0}) ", i);
Thread.Yield();
Console.WriteLine("World({0})!",i);
});
如果你对小消息做了大量的日志记录,你可能会收到很多BeginInvoke呼叫,而且这些呼叫速度很慢。作为一种优化,您可以将所有日志消息排队到ConcurrentQueue
或其他一些同步结构中,并且只有当队列在排队之前为空时才开始唤醒。这样,在UI更新之间,您通常只进行一次启动;当用户界面开始做它的事情时,它会清除标志,然后立即追加所有排队的文本。然而,由于所有做日志记录的线程都只是按照它们进入日志记录程序的任何顺序转储它们的消息,因此有很多小日志语句对可读性非常不利;最好一次记录尽可能大的字符串,以确保它不会被其他线程消息中断;如果你这样做了,你就不会有太多的BeginInvoke和性能。将不会有太大问题。类似于我将消息排队,并让表单对其进行查询,因此将保留顺序。必须提供一些并发策略,让线程在窗体退出队列时为队列提供数据,但您必须确保事件的顺序保持不变,并且可以在看到的第一个事件时启动窗体。类似于我将使消息排队并让窗体退出队列,因此顺序将保持不变。必须提供一些并发策略,让线程在窗体退出队列时为队列提供数据,但您必须确保事件顺序保持不变,并且可以在看到第一个事件时启动窗体。谢谢您的回答。若第一条消息由创建日志表单实例的同一线程追加,那个么一切都正常。但是,如果我想在其他线程中调用Show(),该线程将在几分钟内终止,日志表单只会在这几分钟内出现,并在线程结束时消失。除了让其他线程保持活动状态(我希望避免),还有什么方法可以让框架保持可见;当invokererequired
返回false时,使用BeginInvoke
调度Show
和Hide
在GUI线程上运行。不要从其他线程对GUI对象做任何操作。我已经编辑了我的答案,以包括InvokeRequired/BeginInvoke注意,我还没有测试这个版本。此外,我还添加了一些关于根据日志模式可能需要的重大优化的简介。@Daryn:Console
是线程安全的;这大概包括