为啥相同的代码在我的 BackGroundWorker 线程中比在我的 GUI 线程中慢得多?
Posted
技术标签:
【中文标题】为啥相同的代码在我的 BackGroundWorker 线程中比在我的 GUI 线程中慢得多?【英文标题】:Why is the same code so much slower in my BackGroundWorker thread than in my GUI thread?为什么相同的代码在我的 BackGroundWorker 线程中比在我的 GUI 线程中慢得多? 【发布时间】:2012-01-17 15:26:38 【问题描述】:我正在尝试创建一个在 RichTextBox 中搜索和突出显示文本的 c# WinForms 应用程序。我创建了两种搜索方法:一种在 GUI 线程中运行,另一种在 BackGroundWorker 中运行。两种方法的逻辑基本相同。但是,BGW 中的代码运行速度要慢得多。
请看下面的结果:
0.25MB 文本文件搜索常用关键字:GUI:2.9s - BGW:7.0s 1MB 文本文件搜索常用关键字:GUI:14.1s - BGW:71.4s 5MB 文本文件搜索常用关键字:GUI:172s - BGW:1545s
我觉得奇怪的是,这两种方法所花费的时间之间的关系在搜索大小方面并不是线性的。
该应用程序将用于搜索最大 10MB 的文件,因此速度快很重要。我想使用后台工作人员,以便用户可以看到进度并在执行搜索时继续阅读文件。
请看下面两种方法的代码:
// background search thread
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
// Get the BackgroundWorker that raised this event.
BackgroundWorker worker = sender as BackgroundWorker;
RichTextBox rtb = new RichTextBox();
RichTextBox results = new RichTextBox();
rtb.Rtf = e.Argument as string; //recive text to be searched
int hits = 0; // track number of hits
int pos = 0; // track position in rtb
int i = 0; // trach current line number for progress report
string lowerT = searchTerm.ToLowerInvariant();
string lowerl = "";
int n = 0;
int len = searchTerm.Length;
foreach (string l in rtb.Lines)
lowerl = l.ToLowerInvariant();
n = lowerl.IndexOf(lowerT);
if (n > -1)
while (n > -1) //if found sterm highlight instances
hits++; //incriment hits
//hilight term
rtb.SelectionStart = pos + n;
rtb.SelectionLength = len;
rtb.SelectionBackColor = Color.Yellow;
rtb.SelectionColor = Color.Black;
//find next
n = lowerl.IndexOf(lowerT, n + len);
searchRes.Add(pos); // add positon of hit to results list
//add rtb formatted text to results rtb
rtb.SelectionStart = pos;
rtb.SelectionLength = l.Length;
results.SelectedRtf = rtb.SelectedRtf;
results.AppendText(Environment.NewLine);
pos += l.Length + 1; //incriment position
//worker.ReportProgress(++i);
string[] res = rtb.Rtf,results.Rtf,hits.ToString();
e.Result = res;
// old non threaded search method
public void OldSearch(string sTerm)
int hits = 0; // track number of hits
int pos = 0; // track position in rtb
int oldPos = richTextBox1.SelectionStart; //save current positin in rtb
int oldLen = richTextBox1.SelectionLength;
string lowerT = sTerm.ToLowerInvariant();
sTime = 0;
System.Threading.Timer tmr = new System.Threading.Timer(new TimerCallback(TimerTask), null, 0, 100);
if (sTerm.Length > 0)
//clear old search
ReloadFile();
richTextBox4.Clear();
searchRes = new List<int>();
//open results pane
label1.Text = "Searching for \"" + sTerm + "\"...";
splitContainer1.Panel2Collapsed = false;
frmFind.Focus();
frmFind.ShowProgress(true);
foreach (string l in richTextBox1.Lines)
string lowerl = l.ToLowerInvariant();
int n = lowerl.IndexOf(lowerT);
if (n > -1)
while (n > -1) //if found sterm highlight instances
hits++; //incriment hits
//hilight term
richTextBox1.SelectionStart = pos + n;
richTextBox1.SelectionLength = sTerm.Length;
richTextBox1.SelectionBackColor = Color.Yellow;
richTextBox1.SelectionColor = Color.Black;
//find next
n = lowerl.IndexOf(lowerT, n + sTerm.Length);
searchRes.Add(pos);
richTextBox1.SelectionStart = pos;
richTextBox1.SelectionLength = l.Length;
richTextBox4.SelectedRtf = richTextBox1.SelectedRtf;
richTextBox4.AppendText(Environment.NewLine);
pos += l.Length + 1; //incriment position
tmr.Dispose();
float time = (float)sTime / 10;
label1.Text = "Search for \"" + sTerm + "\": Found " + hits + " instances in " + time + " seconds.";
richTextBox4.SelectionStart = 0;
richTextBox1.SelectionStart = oldPos;
richTextBox1.SelectionLength = oldLen;
richTextBox1.Focus();
frmFind.ShowProgress(false);
注意事项:
我知道 RTB 类有自己的 find 方法,但发现它比我自己的方法慢得多。 我已经阅读了许多关于 BGW 性能的线程,大多数似乎都将使用 Invoke 方法作为原因,但我没有使用任何线程。 我知道使用多线程会使其运行速度变慢,但没想到会有这么大的差异。 问题不在于ReportProgress
,我已将这一行注释掉。我这样做而不是百分比的原因是计算百分比的计算有很大的不同。这种方式实际上更快
另一个用户提供的link 描述了我如何在非 GUI 线程中使用我的 RTB。这似乎表明它不应该是一个问题,但会产生更多的开销,因为它会导致创建消息队列。我不确定这是否会影响我的 foreach 循环中代码的性能。任何有关此事的 cmet 将不胜感激。
【问题讨论】:
可能是后台线程的优先级设置得太低了? “基本相同的代码”也不是相同的代码。 @GCaiazzo 感谢您的评论。我尝试像这样设置优先级:System.Diagnostics.Process.GetCurrentProcess().PriorityClass = System.Diagnostics.ProcessPriorityClass.High;
但似乎没有什么不同。 (我知道这是一个坏主意,因为线程是池化的。我只是将其作为测试)。当我说基本相同时,我指的是 foreach 循环中的逻辑。这是一样的。我觉得^^
我正在查看的代码实际上是一件坏事(tm)。第一个问题是您正在后台线程上创建Control
(RichTextBox
)。根据经验,仅在主 UI 线程上创建控件。当您在后台线程上创建Control
时,您正在后台执行吨 的废话,而这不应 在后台线程上完成。相反,将一个字符串传递给您的后台线程并让您的后台线程返回突出显示索引,以便您的前台线程可以突出显示后台线程找到的文本块。
请记住,UI 的更新速度不会比每 20 毫秒快得多(大约每秒 60 次)——即使是这样,您为什么要这样做?因此,如果您每隔几毫秒就在后台工作人员升级进度,那么您将花费大量时间将数据编组到 UI 线程中——这不是免费的。您可以尝试在后台工作人员中尽可能快地更新数据,然后让 UI Timer 线程每秒获取几次数据——这可能会减少编组量。
【参考方案1】:
通常会减慢 Winforms 的一件事是与 UI 线程同步。如果 ReportProgress 这样做(我不知道,但我猜它必须这样做)并且您经常调用它(例如每秒 100-1000 次或更多),由于各种阻塞问题,它会使一切都停止这会发生。
尝试删除您拥有的 UI 和后台线程之间的任何交互,如果有帮助,请恢复交互,但让它发生的频率要低得多,比如每秒 1-100 次。
另外,我不确定,但如果您传递对控件对象的引用,它可能仍归 UI 线程所有,并且从另一个线程与它的每次交互也可能导致同步问题(以及与实际的表单控件会抛出异常)。
【讨论】:
感谢您的回答。我已经尝试了您的建议,但不幸的是性能并没有真正提高。我删除了循环中对 GUI 线程对象的所有引用(worker.ReportProgress()
和 searchRes.Add()
)。这就是我传递给线程的内容 string parsedText = richTextBox1.Rtf; backgroundWorker1.RunWorkerAsync(parsedText);
parsedText
是我的 Form1
对象的全局变量。【参考方案2】:
不确定...,但是每次调用 SelectedRtf 上的 setter 时,都会发生很多事情,包括获取字符串的 unicode 编码,将其写入缓冲区,然后发送大量 Windows 消息.
因此,首先,如果您可以重新设计算法以在不访问 RTF 搜索框的情况下尽可能多地执行,然后批量突出显示,您可能会提高性能。
至于为什么它更慢... RTF 框是在后台线程上创建的。可能是当他们发送消息并且没有消息循环来处理它们时,会有延迟。或者也许有一些编组回到正确的 SynchronizationContext 发生,这需要时间。不确定。
分析您自己的代码和 .NET Framework 代码的分析器应该会告诉您。
public string SelectedRtf
get
this.ForceHandleCreate();
return this.StreamOut(32770);
set
this.ForceHandleCreate();
if (value == null)
value = "";
this.StreamIn(value, 32770);
private void StreamIn(string str, int flags)
if (str.Length == 0)
if ((32768 & flags) != 0)
this.SendMessage(771, 0, 0);
this.ProtectedError = false;
else
this.SendMessage(12, 0, "");
else
int length = str.IndexOf(char.MinValue);
if (length != -1)
str = str.Substring(0, length);
byte[] buffer = (flags & 16) == 0 ? Encoding.Default.GetBytes(str) : Encoding.Unicode.GetBytes(str);
this.editStream = (Stream) new MemoryStream(buffer.Length);
this.editStream.Write(buffer, 0, buffer.Length);
this.editStream.Position = 0L;
this.StreamIn(this.editStream, flags);
private void StreamIn(Stream data, int flags)
if ((flags & 32768) == 0)
System.Windows.Forms.UnsafeNativeMethods.SendMessage(new HandleRef((object) this, this.Handle), 1079, 0, new System.Windows.Forms.NativeMethods.CHARRANGE());
try
this.editStream = data;
if ((flags & 2) != 0)
long position = this.editStream.Position;
byte[] numArray = new byte[RichTextBox.SZ_RTF_TAG.Length];
this.editStream.Read(numArray, (int) position, RichTextBox.SZ_RTF_TAG.Length);
string @string = Encoding.Default.GetString(numArray);
if (!RichTextBox.SZ_RTF_TAG.Equals(@string))
throw new ArgumentException(System.Windows.Forms.SR.GetString("InvalidFileFormat"));
this.editStream.Position = position;
System.Windows.Forms.NativeMethods.EDITSTREAM editstream = new System.Windows.Forms.NativeMethods.EDITSTREAM();
int num1 = (flags & 16) == 0 ? 5 : 9;
int num2 = (flags & 2) == 0 ? num1 | 16 : num1 | 64;
editstream.dwCookie = (IntPtr) num2;
editstream.pfnCallback = new System.Windows.Forms.NativeMethods.EditStreamCallback(this.EditStreamProc);
this.SendMessage(1077, 0, int.MaxValue);
if (IntPtr.Size == 8)
System.Windows.Forms.NativeMethods.EDITSTREAM64 editstreaM64 = this.ConvertToEDITSTREAM64(editstream);
System.Windows.Forms.UnsafeNativeMethods.SendMessage(new HandleRef((object) this, this.Handle), 1097, flags, editstreaM64);
editstream.dwError = this.GetErrorValue64(editstreaM64);
else
System.Windows.Forms.UnsafeNativeMethods.SendMessage(new HandleRef((object) this, this.Handle), 1097, flags, editstream);
this.UpdateMaxLength();
if (this.GetProtectedError())
return;
if (editstream.dwError != 0)
throw new InvalidOperationException(System.Windows.Forms.SR.GetString("LoadTextError"));
this.SendMessage(185, -1, 0);
this.SendMessage(186, 0, 0);
finally
this.editStream = (Stream) null;
【讨论】:
【参考方案3】:不适合评论,所以我会发布一个答案。
我已经很久没有使用 WinForms 了,但是 WinForms 不应该在从非 UI 代码访问 UI 元素时抛出错误吗?我记得不得不做一堆this.Invoke
的事情,但也许后台工作人员处理事情的方式不同。
无论如何,我的猜测是额外时间的主要部分将与 UI 线程同步以访问 RichTextBox。拿出好的旧秒表并测量您的代码,看看瓶颈在哪里。
我想知道将文本分成块并使用多个线程是否会更快 - 四核工作;) - 找到所有匹配项,然后最后转到 UI 线程,遍历所有匹配项并突出显示文本。
也应该可以只在屏幕可见区域突出显示文本,并且当用户滚动以突出显示更多文本时...
【讨论】:
以上是关于为啥相同的代码在我的 BackGroundWorker 线程中比在我的 GUI 线程中慢得多?的主要内容,如果未能解决你的问题,请参考以下文章