问题描述
|
我有Form负责将来自所有线程的所有必要信息记录到其richTextBox中。当我想从不同的线程访问其控件(将文本追加到richTextBox)时,我使用Invoke,它可以完美地工作。
但是我需要在第一个消息追加到richTextBox时显示此日志表单,并且我不知道线程中的哪个首先执行此操作。
另外,当我关闭日志表单时,我希望在下一条消息到来时再次显示它(在这种情况下,我仍然不知道哪个线程会首先调用它)。
我试图在新线程中并通过Application.Run(ApplicationContext)创建此表单,但是这些解决方案均无效。
你有什么提示吗?
解决方法
类似于此其他答复,我将使消息入队,并让表单使它们出队,以便保留顺序。必须提供一些并发策略,以使线程在表单出队时将其送入队列,但是您要确保保留事件的顺序,并且可以在看到的第一个事件处启动表单。
, 与关闭/重生窗口相比,隐藏和取消隐藏通常更容易。
每当您创建主窗口时就创建该窗口,永远不要关闭它。添加一个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);
}
}
这样一来,您可以将表单更新逻辑粘贴到类似这样的内容中...
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
将输出捕获到Console.Write
。可以在多个线程上对此TextWriter调用Write,因为实现是自同步的。
您可以像这样测试它...
Console.SetOut(threadSafeLogWriter);
Parallel.For(0,100,i=>{
threadSafeLogWriter.Write(\"Hello({0}) \",i);
Thread.Yield();
Console.WriteLine(\"World({0})!\",i);
});
如果您对小消息进行大量日志记录,则可能会遇到许多BeginInvoke调用,这些调用很慢。作为一种优化,您可以将所有日志消息放入一个ConcurrentQueue
或其他同步结构中,并且如果队列在入队之前为空,则仅将BeginInvoke排队。这样,通常在UI更新之间只执行一次BeginInvoke。当用户界面开始执行此操作时,它将清除该标志,然后立即附加所有排队的文本。但是,由于所有进行日志记录的线程只会按照它们进入记录器的顺序转储它们的消息,因此对于可读性而言,拥有许多小的日志语句可能会非常糟糕;最好一次记录尽可能大的字符串,以确保它不会被另一个线程消息打断;如果这样做的话,您将不会有很多BeginInvoke \ perf。将不再是一个问题。