问题:
用一个系统.线程.定时器 https://learn.microsoft.com/en-us/dotnet/api/system.threading.timer类,当定时器回调 https://learn.microsoft.com/en-us/dotnet/api/system.threading.timercallback被调用,事件被引发,以通知订阅者DeliveryProgressChangedEvent
and DeliveryCompletedEvent
定制的Game
过程的进展和终止的类别。
在示例类中,订阅者(此处为 Form 类)更新 UI、设置 ProgressBar 控件的值并显示 MessageBox(在此处显示的类示例的实际实现中使用)。
看来在调用第一个事件后:
DeliveryProgressChangedEvent?.Invoke(this, EventArgs.Empty);
++_counter;
的线_counter
应该增加的值永远不会达到,因此检查的代码_counter
将计时器设置为新值的操作永远不会执行。
会发生什么:
-
The System.Threading.Timer
由 ThreadPool 线程(多个)提供服务。它的回调是在 UI 线程以外的线程上调用的。从回调调用的事件也在 ThreadPool 线程中引发。
然后,处理程序委托中的代码 onDelivery ProgressChanged 将在同一线程上运行。
private void onDeliveryProgressChanged(object sender, EventArgs e)
{
if (InvokeRequired)
pbDelivery.BeginInvoke((MethodInvoker)delegate { pbDelivery.Increment(1); });
MessageBox.Show("Delivery Inprogress");
}
当显示消息框时(它是一个模态窗口),它会像往常一样阻止线程运行。永远不会到达调用事件的行后面的代码,因此_counter
永远不会增加:
DeliveryProgressChangedEvent?.Invoke(this, EventArgs.Empty);
++_counter;
-
The System.Threading.Timer
可以由多个线程提供服务。我只是引用文档来说明这一点,它非常简单:
定时器执行的回调方法应该是可重入的,因为
它在 ThreadPool 线程上调用。回调可以执行
如果计时器间隔为,则同时在两个线程池线程上
小于执行回调所需的时间,或者如果所有线程
池线程正在使用中,并且回调已多次排队。
实际上,发生的情况是,虽然执行 CallBack 的线程被 MessageBox 阻止,但这不会阻止 Timer 从另一个线程执行 CallBack:调用事件时会显示一个新的 MessageBox,并且它会显示一个新的 MessageBox。继续运行,直到有资源为止。
-
MessageBox 没有所有者。当显示 MessageBox 时未指定所有者,其类使用获取活动窗口() https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getactivewindow找到 MessageBox 窗口的所有者。该函数尝试返回附加到调用线程的消息队列的活动窗口的句柄。但是运行 MessageBox 的线程没有活动窗口,因此,所有者是桌面(实际上,IntPtr.Zero
here).
这可以是manually通过激活(单击)调用 MessageBox 的表单来验证:MessageBox 窗口将在表单下方消失,因为它不是owned by it.
怎么解决:
- 当然,使用另一个定时器。这系统.Windows.Forms.定时器 https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.timer(WinForms) 或调度定时器 https://learn.microsoft.com/en-us/dotnet/api/system.windows.threading.dispatchertimer(WPF)是natural替代品。它们的事件在 UI 线程中引发。
► 这里提供的代码只是一个 WinForms 实现
重现问题,因此这些可能不适用于所有情况。
-
Use a 系统.定时器.定时器 https://learn.microsoft.com/en-us/dotnet/api/system.timers.timer: the 同步对象 https://learn.microsoft.com/en-us/dotnet/api/system.timers.timer.synchronizingobject属性提供了将事件编组回创建当前类实例的线程的方法(与具体实现上下文相关的考虑相同)。
-
生成一个异步操作 https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.asyncoperation使用AsyncOperationManager.CreateOperation() https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.asyncoperationmanager.createoperation方法,然后使用发送或后回调 https://learn.microsoft.com/en-us/dotnet/api/system.threading.sendorpostcallback委托让AsyncOperation
打电话给SynchronizationContext.Post() https://learn.microsoft.com/en-us/dotnet/api/system.threading.synchronizationcontext.post方法(经典的 BackGroundWorker 风格)。
-
开始调用() https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.control.begininvoke消息框,附着它到 UI 线程SynchronizationContext
. E.g.,:
this.BeginInvoke(new Action(() => MessageBox.Show(this, "Delivery Completed")));
现在 MessageBox 归 Form 所有,并且它将像往常一样运行。 ThreadPool 线程可以自由继续:模态窗口与 UI 线程同步。
-
避免使用 MessageBox 进行此类通知,因为它确实很烦人:) 还有许多其他方法可以通知用户状态更改。 MessageBox 可能比较少周到.
为了使它们按预期工作,而不改变当前的实现,Game
and Form1
类可以这样重构:
class Game
{
private System.Threading.Timer deliveryTimer = null;
private int counter;
public event EventHandler DeliveryProgressChangedEvent;
public event EventHandler DeliveryCompletedEvent;
public Game(int eventsCount) { counter = eventsCount; }
public void StartDelivery() {
deliveryTimer = new System.Threading.Timer(MakeDelivery);
deliveryTimer.Change(1000, 1000);
}
public void StopDelivery() {
deliveryTimer?.Dispose();
deliveryTimer = null;
}
private void MakeDelivery(object state) {
if (deliveryTimer is null) return;
DeliveryProgressChangedEvent?.Invoke(this, EventArgs.Empty);
counter -= 1;
if (counter == 0) {
deliveryTimer?.Dispose();
deliveryTimer = null;
DeliveryCompletedEvent?.Invoke(this, EventArgs.Empty);
}
}
}
public partial class Form1 : Form
{
Game game = null;
public Form1() {
InitializeComponent();
pbDelivery.Maximum = 5;
game = new Game(pbDelivery.Maximum);
game.DeliveryProgressChangedEvent += onDeliveryProgressChanged;
game.DeliveryCompletedEvent += onDeliveryCompleted;
}
private void onDeliveryProgressChanged(object sender, EventArgs e)
{
this.BeginInvoke(new MethodInvoker(() => {
pbDelivery.Increment(1);
// This MessageBox is used to test the progression of the events and
// to verify that the Dialog is now modal to the owner Form.
// Of course it's not used in an actual implentation.
MessageBox.Show(this, "Delivery In progress");
}));
}
private void onDeliveryCompleted(object sender, EventArgs e)
{
this.BeginInvoke(new Action(() => MessageBox.Show(this, "Delivery Completed")));
}
private void button1_Click(object sender, EventArgs e)
{
game.StartDelivery();
}
}