我们已经建立了一个巨大的winforms项目,已经进行了多年。
有时,我们的用户会收到类似this one的异常。
这个问题的解决方案似乎是:
不要从后台线程访问UI组件
但由于我们的项目是一个非常大的项目,有很多不同的主题,我们不能成功找到所有这些。
有没有办法检查(使用某些工具或调试选项)从后台线程调用哪些组件?
澄清:
我使用一个Form
创建了一个示例winforms项目,其中包含两个Button
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
button1.Text = "Clicked!";
}
private void button2_Click(object sender, EventArgs e)
{
Task.Run(() =>
{
button2.BackColor = Color.Red; //this does not throw an exception
//button2.Text = "Clicked"; //this throws an exception when uncommented
});
}
}
单击按钮时,button2
的背景颜色设置为红色。这发生在后台线程中(被认为是不良行为)。但是,它没有(立即)抛出异常。我想要一种方法来检测这种不良行为'。最好是通过扫描我的代码,但如果它只能通过调试来实现(一旦从后台线程访问UI组件就暂停),它也没问题。
答案 0 :(得分:4)
我有2个建议可以一起使用,第一个是名为DebugSingleThread的Visual Studio插件。
您可以冻结所有线程并一次处理一个(显然是非主UI线程)并查看每个线程对控件的访问权限。单调乏味我知道但第二种方法并不是那么糟糕。
第二种方法是获取步骤以重现问题。如果您知道重现它的步骤,将会更容易看到导致它的原因。为此,我在Github上创建了这个User Action Log项目。
它会记录用户所做的每一个动作,你可以在SO:User Activity Logging, Telemetry (and Variables in Global Exception Handlers)上阅读。
我建议您也记录线程ID,然后当您能够重现问题时,请转到日志的末尾并确定准确的步骤。它不像看起来那么痛苦,也非常适合应用遥测。
您可以自定义此项目,例如捕获DataSource_Completed事件或添加一个虚拟DataSource属性,该属性设置真实的Grids DataSource属性并引发INotifyPropertyChanged
事件 - 如果它是非主线程ID,那么Debugger.Break();
。
我的直觉是你在后台线程中更改控件的(例如网格)数据源(对于那种非冻结的感觉),这会导致同步问题。这是经历过这种情况的另一个DevExpress客户所发生的事情。它在与您引用的线程不同的线程中讨论了here。
答案 1 :(得分:3)
跨线程操作应该一直在winforms中爆炸。几乎每种方法都会疯狂地检查它们。作为起点,请查看https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/Control.cs。
在你的应用中的某个地方,有人可能已经放了这行代码:
Control.CheckForIllegalCrossThreadCalls = False;
注释并运行应用程序,然后按照例外情况进行操作。
(通常你可以通过在调用中包装更新来解决问题,例如,如果你看到textbox1.text=SomeString;
将其更改为`textbox.invoke(()=> {textbox1.text = SomeString;});。
您可能还需要添加对InvokeRequired的检查,使用BeginInvoke来避免死锁,并从invoke返回值,这些都是单独的主题。
这是假设即使是一个温和的重构也是不可能的,即使是中型企业应用也几乎总是如此。
注意:通过静态分析(即不运行应用程序)无法保证成功发现此案例。除非你能解决停止问题... https://cs.stackexchange.com/questions/63403/is-the-halting-problem-decidable-for-pure-programs-on-an-ideal-computer等......
答案 2 :(得分:2)
我这样做是为了搜索特定情况,但当然需要根据您的需要进行调整,但这样做的目的是为您提供至少一种可能性。
我调用了这个方法SearchForThreads
但是因为它只是一个例子,你可以随意调用它。
这里的主要思想是将这个Method调用添加到基类并在构造函数上调用它,使其更灵活。
然后使用反射在从此基础派生的所有类上调用此方法,并在任何类中发现此情况时抛出异常或其他内容。
有一个预先要求,即Framework 4.5的使用。
此版本的框架添加了CompilerServices
属性,该属性为我们提供了有关Method的调用者的详细信息。
此文档为here
有了它,我们可以打开源文件并深入研究。
我所做的只是使用基本文本搜索来搜索您在问题中指定的情况。
但它可以让您深入了解如何在您的解决方案中执行此操作,因为我对您的解决方案知之甚少,我只能使用您在帖子上放置的代码。
public static void SearchForThreads(
[System.Runtime.CompilerServices.CallerMemberName] string memberName = "",
[System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "",
[System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
var startKey = "this.Controls.Add(";
var endKey = ")";
List<string> components = new List<string>();
var designerPath = sourceFilePath.Replace(".cs", ".Designer.cs");
if (File.Exists(designerPath))
{
var designerText = File.ReadAllText(designerPath);
var initSearchPos = designerText.IndexOf(startKey) + startKey.Length;
do
{
var endSearchPos = designerText.IndexOf(endKey, initSearchPos);
var componentName = designerText.Substring(initSearchPos, (endSearchPos - initSearchPos));
componentName = componentName.Replace("this.", "");
if (!components.Contains(componentName))
components.Add(componentName);
} while ((initSearchPos = designerText.IndexOf(startKey, initSearchPos) + startKey.Length) > startKey.Length);
}
if (components.Any())
{
var classText = File.ReadAllText(sourceFilePath);
var ThreadPos = classText.IndexOf("Task.Run");
if (ThreadPos > -1)
{
do
{
var endThreadPos = classText.IndexOf("}", ThreadPos);
if (endThreadPos > -1)
{
foreach (var component in components)
{
var search = classText.IndexOf(component, ThreadPos);
if (search > -1 && search < endThreadPos)
{
Console.WriteLine($"Found a call to UI thread component at pos: {search}");
}
}
}
}
while ((ThreadPos = classText.IndexOf("Task.Run", ++ThreadPos)) < classText.Length && ThreadPos > 0);
}
}
}
我希望它可以帮助你。
如果您拆分文本以便输出,您可以获取行号,但我不想解决问题,因为我不知道什么对您有用。
string[] lines = classText.Replace("\r","").Split('\n');
答案 3 :(得分:1)
试试:
public static void Main(string[] args)
{
// Add the event handler for handling UI thread exceptions to the event.
Application.ThreadException += new ThreadExceptionEventHandler(exception handler);
// Set the unhandled exception mode to force all Windows Forms errors to go through the handler.
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
// Add the event handler for handling non-UI thread exceptions to the event.
AppDomain.CurrentDomain.UnhandledException += // add the handler here
// Runs the application.
Application.Run(new ......);
}
然后您可以记录消息和调用堆栈,这应该为您提供足够的信息来解决问题。
答案 4 :(得分:1)
我建议您更新GUI以自动处理这种情况,以方便您使用。您改为使用一组继承的控件。
这里的一般原则是覆盖属性Set方法,使其成为线程安全。因此,在每个重写的属性中,不是直接更新基本控件,而是检查是否需要调用(意味着我们在GUI的单独线程上)。然后,Invoke调用更新GUI线程上的属性,而不是辅助线程。
因此,如果使用了继承的控件,那么尝试从辅助线程更新GUI元素的表单代码可以保留原样。
这是文本框和按钮。您可以根据需要添加更多,并根据需要添加其他属性。而不是将代码放在单独的表单上。
您不需要进入设计器,您只能在设计器文件上进行查找/替换。例如,在所有designer.cs文件中,您将使用ThreadSafeControls.TextBoxBackgroundThread和System.Windows.Forms.Button将Thread.Windows.Forms.TextBox替换为ThreadSafeControls.ButtonBackgroundThread。
可以使用相同的原理创建其他控件,具体取决于哪种控件类型和控制类型。属性正在从后台线程更新。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace ThreadSafeControls
{
class TextBoxBackgroundThread : System.Windows.Forms.TextBox
{
public override string Text
{
get
{
return base.Text;
}
set
{
if (this.InvokeRequired)
this.Invoke((MethodInvoker)delegate { base.Text = value; });
else
base.Text = value;
}
}
public override System.Drawing.Color ForeColor
{
get
{
return base.ForeColor;
}
set
{
if (this.InvokeRequired)
this.Invoke((MethodInvoker)delegate { base.ForeColor = value; });
else
base.ForeColor = value;
}
}
public override System.Drawing.Color BackColor
{
get
{
return base.BackColor;
}
set
{
if (this.InvokeRequired)
this.Invoke((MethodInvoker)delegate { base.BackColor = value; });
else
base.BackColor = value;
}
}
}
class ButtonBackgroundThread : System.Windows.Forms.Button
{
public override string Text
{
get
{
return base.Text;
}
set
{
if (this.InvokeRequired)
this.Invoke((MethodInvoker)delegate { base.Text = value; });
else
base.Text = value;
}
}
public override System.Drawing.Color ForeColor
{
get
{
return base.ForeColor;
}
set
{
if (this.InvokeRequired)
this.Invoke((MethodInvoker)delegate { base.ForeColor = value; });
else
base.ForeColor = value;
}
}
public override System.Drawing.Color BackColor
{
get
{
return base.BackColor;
}
set
{
if (this.InvokeRequired)
this.Invoke((MethodInvoker)delegate { base.BackColor = value; });
else
base.BackColor = value;
}
}
}
}