Reputation: 3694
I am trying to call a method in main thread which will update multiple UI elements. One of these elements is a RichTextView
. I found 3 methods to update UI which all 3 of them after running for a while, crash with following error. As soon as I change RichTextView to simple textbox, type 2 will not crash anymore (I'm still not sure if this is the case).
An unhandled exception of type 'System.StackOverflowException' occurred in System.Windows.Forms.dll
My simplified code
// 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);
}
Full Code
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);
}
}
}
Question 1
Why and when I should use SynchronizationContext
or Invoke
. What is the difference (Correct me if I am wrong, As I am running on winform, SynchronizationContext.Current
always exists)?
Question 2 Why I am getting StackOverflow error here? Am I doing something wrong? Is there any difference between calling invoke in a separate method or same worker method (Log1 is crashing while directly calling Invoke does not)
Question 3
When User Closes the application before threads finish their job, I get and exception saying Form1 is disposed and is not accessible while calling Log method (any 3 type). Have should I handle exceptions in threads (including main thread)?
Upvotes: 1
Views: 416
Reputation: 942257
Application.DoEvents();
It is truly impressive how this method can make a program fail in totally inscrutable ways. The basic recipe you need to get it to blow your stack is a very easy one to come by. You need a worker thread that calls Log1() at a high rate, about a thousand times per second or more. And code that runs on the UI thread that does something expensive, taking more than a millisecond. Like adding a line to a TextBox.
Then:
You added Application.DoEvents() because you noticed that your UI froze, the textbox isn't showing the added lines of text and after 5 seconds you get the "Not Responding!" ghost window. Yes, it fixed that problem. But not in a very constructive way, as you found out. What you wanted it to do is dispatch the Paint event for the textbox. DoEvents() isn't selective enough, it does all events. Including the ones you didn't count on, the event triggered by Invoke(). Just make it selective:
edtLog.Update();
No more StackOverflowException. But still not a program that works. You'll get a crazily scrolling textbox that no human can read. And you can't stop it either, the program is still dead to user input so clicking the Close button doesn't work.
You haven't yet addressed the fundamental bug in your program. The fire-hose bug, the 3rd most common threading bug after races and deadlocks. Your worker thread is producing results at a rate far higher than the UI thread can consume them. And above all, a rate that a human can see them. You've created an unusable user interface.
Fix that problem and all the misery you now got from threading will disappear. The code is too fake to recommend a specific fix, but ought to be somewhere around only displaying snapshots that you take once a second. Or only updating the UI when the worker thread completes, the pattern encouraged by BackgroundWorker. Whatever it takes to rebalance the work so that the UI thread has less work to do than the worker thread.
Upvotes: 1