调用函数的最佳方式是什么?
What is the best way to invoke a function?
我的程序有 2 个线程 运行,线程 1 做一些事情来控制线程 2 上表单 运行 中的标签。所以我必须使用委托并调用表单 1 中的函数class 访问标签。我的代码在下面,它工作得很好。不过,我想知道是否有更短、更好的方法来做到这一点?
delegate void Change_Status_Call_Back(string status_changed);
public void change_status(string status_changed)
{
if (this.label_status.InvokeRequired)
{
Change_Status_Call_Back obj = new Change_Status_Call_Back(change_status);
this.Invoke(obj, new object[] { status_changed });
}
else
{
this.label_status.Text = status_changed;
}
}
我会这样做:
public void change_status(string status_changed)
{
this.label_status.InvokeSafely(c => c.Text = status_changed);
}
您需要这个扩展方法:
public static void InvokeSafely(this Control control, Action<Control> action)
{
if (control.InvokeRequired)
{
control.Invoke((Action)(() => action?.Invoke(control)));
}
else
{
action?.Invoke(control);
}
}
本题为"primarily opinion based"。尽管如此,你还是触动了我的小毛病,所以……
您应该完全跳过 InvokeRequired
检查:
public void change_status(string status_changed)
{
this.Invoke((MethodInvoker)(() => this.label_status.Text = status_changed));
}
框架必须有效地检查 InvokeRequired
无论如何,因为它需要支持在 UI 线程上调用而不会死锁。所以检查你的代码是多余的。在像这样的 UI 代码中,始终将方法主体包装在委托调用中的开销是无关紧要的,特别是因为如果您正在编写这段代码,那么当 InvokeRequired
无论如何都是真的(即 "fast path" 永远不会被采用)。
更好的是使用更现代的机制来处理跨线程访问,例如async
/await
或Progress<T>
class。然后你根本不需要写一个显式调用 Invoke()
。
前段时间,我在这里更深入地咆哮:MSDN’s canonical technique for using Control.Invoke is lame
环顾四周,我想到了这个:
// UPDATE DISPLAY items (using Invoke in case running on BW thread).
IAsyncResult h = BeginInvoke((MethodInvoker)delegate
{
FooButton.Text = temp1;
BarUpdown.Value = temp2;
}
);
EndInvoke(h); // Wait for invoke to complete.
h.AsyncWaitHandle.Close(); // Explicitly close the wait handle.
// (Keeps handle count from growing until GC.)
详情:
- 我完全删除了
if (InvokeRequired)
。 (从 Peter Duniho 的回答中发现。) Invoke() 在 UI 线程上工作得很好。在 运行 仅在 UI 线程上的代码中,UI 操作不需要特殊处理。在 运行 仅在非 UI 线程上的代码中,将所有 UI 操作包装在 Invoke() 中。在可以在 UI 线程上 运行 或非 UI 线程的代码中,同样将所有 UI 操作包装在 Invoke() 中。在 UI 线程上 运行 时,始终使用 Invoke() 会增加一些开销,但是:开销不大(我希望如此);无论如何,在 UI 线程上的操作 运行 较少; 通过始终使用 Invoke,您不必对 UI 操作进行两次编码。我卖了。
- 我用
BeginInvoke() .. EndInvoke() .. AsyncWaitHandle.Close()
替换了 Invoke()
。 (找到 elsewhere。)Invoke()
可能只是 BeginInvoke() .. EndInvoke()
,所以这只是内联扩展(稍微多一点的目标代码;稍微快一点的执行)。添加 AsyncWaitHandle.Close()
可以解决其他问题:当 运行 在非 UI 线程上运行时,Invoke()
会留下数百个句柄,这些句柄会在垃圾回收之前一直存在。 (在任务管理器中看到句柄数量增长令人恐惧。)使用 BeginInvoke() .. EndInvoke()
留下挥之不去的句柄。 (惊喜:仅使用 BeginInvoke()
不会留下句柄;看起来 EndInvoke()
是罪魁祸首。)使用 AsyncWaitHandle.Close()
明确杀死死句柄消除了挥之不去的句柄的 [化妆品] 问题.当 UI 线程上的 运行ning 时,BeginInvoke() .. EndInvoke()
(如 Invoke()
)不留下任何句柄,因此 AsyncWaitHandle.Close()
是不必要的,但我认为它也不昂贵.
- IsDisposed 测试在竞争条件下似乎是安全的,但我认为没有必要。我担心 BackgroundWorker 可以 Invoke() 操作;当它处于挂起状态时,单击可以触发 UI 线程上的回调,该线程可以 Close() 表单,然后消息循环执行此操作。 (不确定这是否会发生。)
问题:(当有问题时我会在这里更新。)我将我所有的 UI 更新从 运行ning on a UI timer kludge 更改为使用 Invoke() (如上面),现在关闭表单大约有 20% 的时间在竞争条件下失败。如果用户点击停止了我的后台工作人员,那么点击关闭就可以正常工作了。但是,如果用户直接单击关闭,则会触发 UI 线程上的回调,Close() 表单;触发另一个标记后台工作人员停止;后台工作人员继续,并在 EndInvoke() 处崩溃并显示 "Cannot access a disposed object. Object name: 'MainWin'. at System.Windows.Forms.Control.MarshaledInvoke(Control caller, Delegate method, Object[] args, Boolean synchronous) ..."。在 EndInvoke() .. AsyncWaitHandle.Close()
周围添加 if (!this.IsDisposed) {}
并不能解决问题。
选项:返回使用表单计时器:让 BW 将其更改写入十几个全局 "mailbox" 变量。让计时器做 FooButton.Text = nextFooButtonText;
,等等。大多数这样的赋值几乎什么都不做,因为设置表单字段只会在值实际发生变化时更新显示。 (为清楚起见并减少复制对象,将邮箱变量初始化为 null,并让计时器执行 if (nextFooButtonText != null) { FooButton.Text = nextFooButtonText; nextFooButtonText = null; }
,等等)计时器每隔几毫秒在 UI 消息循环中放置一个新事件,这比 Invoke()s 更愚蠢。在计时器回调上更新显示会延迟每次更新 [最多] 计时器间隔。 (恶心。)
工作选项:仅使用 BeginInvoke()
。为什么让 BW 等待每个 Invoke 完成? 1) temp1 和 temp2 似乎作为引用传递 - 如果它们在 BeginInvoke()
之后发生更改,则新值获胜。 (但这并不是那么糟糕。) 2) temp1 和 temp2 可以超出范围。 (但是,在最后一个引用消失之前,它们不会被释放吗?)3)等待确保 BW 一次只有一个未决的调用操作 - 如果 UI 线程阻塞了一段时间,BW 可以'不要把它埋在事件中。 (但是我的 UI 线程无法阻塞,至少在我的 BW 为 运行ning 时不会阻塞。)
选项:将 try .. catch
放在 EndInvoke()
周围。 (恶心。)
我看到了其他几个建议的技巧:
•让Close 自行取消,启动计时器,然后return 以便任何挥之不去的Invoke()s 在UI 线程上完成;不久之后,计时器回调执行真正的关闭(发现 here; from here)。
•杀死后台工作线程。
•更改 Program.cs 以不同方式关闭。
我的程序有 2 个线程 运行,线程 1 做一些事情来控制线程 2 上表单 运行 中的标签。所以我必须使用委托并调用表单 1 中的函数class 访问标签。我的代码在下面,它工作得很好。不过,我想知道是否有更短、更好的方法来做到这一点?
delegate void Change_Status_Call_Back(string status_changed);
public void change_status(string status_changed)
{
if (this.label_status.InvokeRequired)
{
Change_Status_Call_Back obj = new Change_Status_Call_Back(change_status);
this.Invoke(obj, new object[] { status_changed });
}
else
{
this.label_status.Text = status_changed;
}
}
我会这样做:
public void change_status(string status_changed)
{
this.label_status.InvokeSafely(c => c.Text = status_changed);
}
您需要这个扩展方法:
public static void InvokeSafely(this Control control, Action<Control> action)
{
if (control.InvokeRequired)
{
control.Invoke((Action)(() => action?.Invoke(control)));
}
else
{
action?.Invoke(control);
}
}
本题为"primarily opinion based"。尽管如此,你还是触动了我的小毛病,所以……
您应该完全跳过 InvokeRequired
检查:
public void change_status(string status_changed)
{
this.Invoke((MethodInvoker)(() => this.label_status.Text = status_changed));
}
框架必须有效地检查 InvokeRequired
无论如何,因为它需要支持在 UI 线程上调用而不会死锁。所以检查你的代码是多余的。在像这样的 UI 代码中,始终将方法主体包装在委托调用中的开销是无关紧要的,特别是因为如果您正在编写这段代码,那么当 InvokeRequired
无论如何都是真的(即 "fast path" 永远不会被采用)。
更好的是使用更现代的机制来处理跨线程访问,例如async
/await
或Progress<T>
class。然后你根本不需要写一个显式调用 Invoke()
。
前段时间,我在这里更深入地咆哮:MSDN’s canonical technique for using Control.Invoke is lame
环顾四周,我想到了这个:
// UPDATE DISPLAY items (using Invoke in case running on BW thread).
IAsyncResult h = BeginInvoke((MethodInvoker)delegate
{
FooButton.Text = temp1;
BarUpdown.Value = temp2;
}
);
EndInvoke(h); // Wait for invoke to complete.
h.AsyncWaitHandle.Close(); // Explicitly close the wait handle.
// (Keeps handle count from growing until GC.)
详情:
- 我完全删除了
if (InvokeRequired)
。 (从 Peter Duniho 的回答中发现。) Invoke() 在 UI 线程上工作得很好。在 运行 仅在 UI 线程上的代码中,UI 操作不需要特殊处理。在 运行 仅在非 UI 线程上的代码中,将所有 UI 操作包装在 Invoke() 中。在可以在 UI 线程上 运行 或非 UI 线程的代码中,同样将所有 UI 操作包装在 Invoke() 中。在 UI 线程上 运行 时,始终使用 Invoke() 会增加一些开销,但是:开销不大(我希望如此);无论如何,在 UI 线程上的操作 运行 较少; 通过始终使用 Invoke,您不必对 UI 操作进行两次编码。我卖了。 - 我用
BeginInvoke() .. EndInvoke() .. AsyncWaitHandle.Close()
替换了Invoke()
。 (找到 elsewhere。)Invoke()
可能只是BeginInvoke() .. EndInvoke()
,所以这只是内联扩展(稍微多一点的目标代码;稍微快一点的执行)。添加AsyncWaitHandle.Close()
可以解决其他问题:当 运行 在非 UI 线程上运行时,Invoke()
会留下数百个句柄,这些句柄会在垃圾回收之前一直存在。 (在任务管理器中看到句柄数量增长令人恐惧。)使用BeginInvoke() .. EndInvoke()
留下挥之不去的句柄。 (惊喜:仅使用BeginInvoke()
不会留下句柄;看起来EndInvoke()
是罪魁祸首。)使用AsyncWaitHandle.Close()
明确杀死死句柄消除了挥之不去的句柄的 [化妆品] 问题.当 UI 线程上的 运行ning 时,BeginInvoke() .. EndInvoke()
(如Invoke()
)不留下任何句柄,因此AsyncWaitHandle.Close()
是不必要的,但我认为它也不昂贵. - IsDisposed 测试在竞争条件下似乎是安全的,但我认为没有必要。我担心 BackgroundWorker 可以 Invoke() 操作;当它处于挂起状态时,单击可以触发 UI 线程上的回调,该线程可以 Close() 表单,然后消息循环执行此操作。 (不确定这是否会发生。)
问题:(当有问题时我会在这里更新。)我将我所有的 UI 更新从 运行ning on a UI timer kludge 更改为使用 Invoke() (如上面),现在关闭表单大约有 20% 的时间在竞争条件下失败。如果用户点击停止了我的后台工作人员,那么点击关闭就可以正常工作了。但是,如果用户直接单击关闭,则会触发 UI 线程上的回调,Close() 表单;触发另一个标记后台工作人员停止;后台工作人员继续,并在 EndInvoke() 处崩溃并显示 "Cannot access a disposed object. Object name: 'MainWin'. at System.Windows.Forms.Control.MarshaledInvoke(Control caller, Delegate method, Object[] args, Boolean synchronous) ..."。在 EndInvoke() .. AsyncWaitHandle.Close()
周围添加 if (!this.IsDisposed) {}
并不能解决问题。
选项:返回使用表单计时器:让 BW 将其更改写入十几个全局 "mailbox" 变量。让计时器做 FooButton.Text = nextFooButtonText;
,等等。大多数这样的赋值几乎什么都不做,因为设置表单字段只会在值实际发生变化时更新显示。 (为清楚起见并减少复制对象,将邮箱变量初始化为 null,并让计时器执行 if (nextFooButtonText != null) { FooButton.Text = nextFooButtonText; nextFooButtonText = null; }
,等等)计时器每隔几毫秒在 UI 消息循环中放置一个新事件,这比 Invoke()s 更愚蠢。在计时器回调上更新显示会延迟每次更新 [最多] 计时器间隔。 (恶心。)
工作选项:仅使用 BeginInvoke()
。为什么让 BW 等待每个 Invoke 完成? 1) temp1 和 temp2 似乎作为引用传递 - 如果它们在 BeginInvoke()
之后发生更改,则新值获胜。 (但这并不是那么糟糕。) 2) temp1 和 temp2 可以超出范围。 (但是,在最后一个引用消失之前,它们不会被释放吗?)3)等待确保 BW 一次只有一个未决的调用操作 - 如果 UI 线程阻塞了一段时间,BW 可以'不要把它埋在事件中。 (但是我的 UI 线程无法阻塞,至少在我的 BW 为 运行ning 时不会阻塞。)
选项:将 try .. catch
放在 EndInvoke()
周围。 (恶心。)
我看到了其他几个建议的技巧:
•让Close 自行取消,启动计时器,然后return 以便任何挥之不去的Invoke()s 在UI 线程上完成;不久之后,计时器回调执行真正的关闭(发现 here; from here)。
•杀死后台工作线程。
•更改 Program.cs 以不同方式关闭。