【问题标题】:SynchronizationContext or InvokeSynchronizationContext 或 Invoke
【发布时间】:2015-10-04 07:43:09
【问题描述】:

我试图在主线程中调用一个方法来更新多个 UI 元素。这些元素之一是RichTextView。我找到了 3 种更新 UI 的方法,其中所有 3 种在运行一段时间后都会崩溃并出现以下错误。一旦我将 RichTextView 更改为简单文本框,类型 2 就不会再崩溃(我仍然不确定是否是这种情况)。

“System.StackOverflowException”类型的未处理异常 发生在 System.Windows.Forms.dll 中

我的简化代码

    // Type 1
    private readonly SynchronizationContext synchronizationContext;

    public Form1() {
        InitializeComponent();
        // Type 1
        synchronizationContext = SynchronizationContext.Current;
    }

    //Type 3
    public void Log1(object message) {
        Invoke(new Log1(Log), message);
    }

    public void Log(object message) {
        if (this.IsDisposed || edtLog.IsDisposed)
            return;

        edtLog.AppendText(message.ToString() + "\n");
        edtLog.ScrollToCaret();
        Application.DoEvents();
    }

    private void btnStart_Click(object sender, EventArgs e) {

        for (int i = 0; i < 10000; i++) {
            ThreadPool.QueueUserWorkItem(Work, i);
        }
        Log("Done Adding");
    }

    private void Work(object ItemID) {

        int s = new Random().Next(10, 15);// Will generate same random number in different thread occasionally

        string message = ItemID + "\t" + Thread.CurrentThread.ManagedThreadId + ",\tRND " + s;

        //Type1
        synchronizationContext.Post(Log, message);

        // Type 2
        //Invoke(new Log1(Log), message);

        // Type 3
        //Log1(message);

        Thread.Sleep(s);
    }

完整代码

using System;
using System.Threading;
using System.Windows.Forms;

namespace Test {

    public delegate void Log1(string a);

    public partial class Form1 : Form {

        private System.ComponentModel.IContainer components = null;
        private Button btnStart;
        private TextBox edtLog;

        protected override void Dispose(bool disposing) {
            if (disposing && (components != null)) {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        private void InitializeComponent() {
            this.btnStart = new Button();
            this.edtLog = new TextBox();
            this.SuspendLayout();

            this.btnStart.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
            this.btnStart.Location = new System.Drawing.Point(788, 12);
            this.btnStart.Size = new System.Drawing.Size(75, 23);
            this.btnStart.Text = "Start";
            this.btnStart.Click += new System.EventHandler(this.btnStart_Click);

            this.edtLog.Anchor = (AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right);
            this.edtLog.Location = new System.Drawing.Point(12, 41);
            this.edtLog.Multiline = true;
            this.edtLog.ScrollBars = ScrollBars.Vertical;
            this.edtLog.Size = new System.Drawing.Size(851, 441);

            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
            this.ClientSize = new System.Drawing.Size(875, 494);
            this.Controls.Add(this.edtLog);
            this.Controls.Add(this.btnStart);
            this.ResumeLayout(false);
            this.PerformLayout();
        }

        [STAThread]
        static void Main() {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }


        // Type 1
        private readonly SynchronizationContext synchronizationContext;

        public Form1() {
            InitializeComponent();
            // Type 1
            synchronizationContext = SynchronizationContext.Current;
        }

        //Type 3
        public void Log1(object message) {
            Invoke(new Log1(Log), message);
        }

        public void Log(object message) {
            if (this.IsDisposed || edtLog.IsDisposed)
                return;

            edtLog.AppendText(message.ToString() + "\n");
            edtLog.ScrollToCaret();
            Application.DoEvents();
        }

        private void btnStart_Click(object sender, EventArgs e) {

            for (int i = 0; i < 10000; i++) {
                ThreadPool.QueueUserWorkItem(Work, i);
            }
            Log("Done Adding");
        }

        private void Work(object ItemID) {

            int s = new Random().Next(10, 15);// Will generate same random number in different thread occasionally

            string message = ItemID + "\t" + Thread.CurrentThread.ManagedThreadId + ",\tRND " + s;

            //Type1
            synchronizationContext.Post(Log, message);

            // Type 2
            //Invoke(new Log1(Log), message);

            // Type 3
            //Log1(message);

            Thread.Sleep(s);
        }

    }
}

问题 1

为什么以及何时应该使用SynchronizationContextInvoke。有什么区别(如果我错了,请纠正我,因为我在 winform 上运行,SynchronizationContext.Current 始终存在)?

问题 2 为什么我在这里收到 StackOverflow 错误?难道我做错了什么?在单独的方法或同一个worker方法中调用invoke有什么区别(Log1崩溃而直接调用Invoke没有)

问题 3

当用户在线程完成其工作之前关闭应用程序时,我得到一个异常说 Form1 已被释放并且在调用 Log 方法(任何 3 种类型)时不可访问。我应该在线程(包括主线程)中处理异常吗?

【问题讨论】:

    标签: .net multithreading visual-studio-2010 c#-4.0


    【解决方案1】:
       Application.DoEvents();
    

    这种方法可以使程序以完全难以理解的方式失败,这确实令人印象深刻。你需要让它炸毁你的筹码的基本配方很容易得到。您需要一个工作线程以高速率调用 Log1(),大约每秒一千次或更多。而在 UI 线程上运行的代码会执行一些昂贵的操作,耗时超过一毫秒。就像在 TextBox 中添加一行一样。

    然后:

    • Lo​​g1() 调用 Log(),后者调用 DoEvents()。
    • 这允许另一个 Log1() 调用调用调用 DoEvents 的 Log()。
    • 这允许另一个 Log1() 调用调用调用 DoEvents 的 Log()。
    • 这允许另一个 Log1() 调用调用调用 DoEvents 的 Log()。
    • 这允许另一个 Log1() 调用调用调用 DoEvents 的 Log()。
    • ....
    • 咔嚓!

    您添加 Application.DoEvents() 是因为您注意到您的 UI 冻结,文本框未显示添加的文本行,并且 5 秒后您收到“未响应!”鬼窗。是的,它解决了这个问题。但正如你所发现的那样,不是以一种非常有建设性的方式。你想要它做的是为文本框分派 Paint 事件。 DoEvents() 的选择性不够,它会处理 所有 事件。包括你没想到的,Invoke() 触发的事件。让它有选择性:

      edtLog.Update();
    

    不再有 StackOverflowException。但仍然不是一个有效的程序。你会得到一个人类无法阅读的疯狂滚动的文本框。而且您也无法停止它,程序仍然无法输入用户输入,因此单击“关闭”按钮不起作用。

    您尚未解决程序中的基本错误。 fire-hose bug,在比赛和死锁之后第三个最常见的线程错误。您的工作线程产生结果的速度高于 UI 线程可以消耗它们的速度。最重要的是,人类可以看到它们的速率。您创建了一个无法使用的用户界面。

    解决这个问题,你现在从线程中得到的所有痛苦都会消失。该代码太假了,无法推荐特定的修复程序,但应该在某个地方仅显示您每秒拍摄一次的快照。或者只在工作线程完成时更新 UI,BackgroundWorker 鼓励的模式。不惜一切代价重新平衡工作,使 UI 线程比工作线程要做的工作更少。

    【讨论】:

    • 我不知道你所说的 fire-hoise bug 是什么意思(这是一个常用术语吗?)。我明白你的意思。我怎样才能同步线程,以便在开始新更新之前主线程完成更新 UI 之前它们被阻塞,我认为 Invoke 应该这样做?实际上我不在乎文本框是否正在滚动,但是用户应该能够关闭应用程序、移动其窗口或调整其大小(显然我不知道该怎么做)。
    • 是否应该在调用前添加信号量,然后释放?
    • 像所有线程错误一样,消防软管错误是一个基本错误,需要您更改程序的设计。您必须阻止 UI 线程燃烧 100% 内核。当它必须做这么多工作时,没有什么好事发生,它优先履行职责。并且清空调用队列比响应输入或绘制用户界面具有更高的优先级。这就是它所做的一切。如果要同步,则必须知道 UI 线程何时空闲并准备好做更多工作。这在技术上是可行的,Application.Idle 事件告诉你。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-14
    • 2017-07-26
    • 1970-01-01
    • 2019-12-07
    相关资源
    最近更新 更多