1

在我的 WPF 应用程序中,我有一个长时间运行的上传运行,它会在运行时引发进度事件,从而更新进度条。用户也有机会取消上传,否则可能会出错。这些都是异步事件,因此需要使用 Dispatcher.Invoke 执行它们才能更新 UI。

所以代码看起来像这样,ish:

void OnCancelButtonClicked(object sender, EventArgs e)
{
    upload.Cancel();
    _cancelled = true;
    view.Close();
    view.Dispose();
}

void OnProgressReceived(object sender, EventArgs<double> e)
{
    Dispatcher.Invoke(() => 
    {
        if (!cancelled)
            view.Progress = e.Value;
    }
}

假设在已处置视图上设置 view.Progress 会引发错误,那么这段代码线程安全吗?即如果用户在进度更新时单击取消,他/她将不得不等到进度更新,如果在执行 OnCancelButtonClicked 期间更新了进度,则 Dispatcher.Invoke 调用将导致 view.Progress 更新为在 _cancelled 设置后排队,所以我不会在那里遇到问题。

或者我是否需要一把锁才能安全,例如:

object myLock = new object();

void OnCancelButtonClicked(object sender, EventArgs e)
{
    lock(myLock)
    {
        upload.Cancel();
        _cancelled = true;
        view.Close();
        view.Dispose();
    }
}

void OnProgressReceived(object sender, EventArgs<double> e)
{
    Dispatcher.Invoke(() => 
    {
        lock(myLock)
        {
            if (!cancelled)
                view.Progress = e.Value;
        }
    }
}
4

2 回答 2

8

您不必添加锁。Dispatcher.Invoke 和 BeginInvoke 请求不会在其他代码中间运行(这就是它们的重点)。

只需考虑两件事:

  1. BeginInvoke 在这种情况下可能更合适,Invoke 会将请求排队,然后阻塞调用线程,直到 UI 线程空闲并执行完代码,BeginInvoke 只会将请求排队而不阻塞。
  2. 某些操作,尤其是打开窗口(包括消息框)或进行进程间通信的操作可能允许排队的调度程序操作运行。

编辑:首先,我没有引用,因为不幸的是有关该主题的 MSDN 页面的详细信息非常低 - 但我已经编写了测试程序来检查 BeginInvoke 的行为,我在这里写的所有内容都是这些测试的结果。

现在,为了扩展第二点,我们首先需要了解调度程序的作用。显然这是一个非常简化的解释。

任何 Windows UI 都是通过处理消息来工作的;例如,当用户将鼠标移到窗口上时,系统将向该窗口发送 WM_MOUSEMOVE 消息。

系统通过添加一个队列来发送消息,每个线程可能有一个队列,同一个线程创建的所有窗口共享同一个队列。

在每个 Windows 程序的核心都有一个称为“消息循环”或“消息泵”的循环,该循环从队列中读取下一条消息并调用相应窗口的代码来处理该消息。

在 WPF 中,这个循环和所有由 Dispatcher 处理的相关处理。

应用程序可以在消息循环中等待下一条消息,也可以正在做某事。这就是为什么当您进行长时间计算时,所有线程的窗口都变得无响应 - 线程正忙于工作并且不会返回消息循环来处理下一条消息。

Dispatcher.Invoke 和 BeginInvoke 通过将请求的操作排队并在下次线程返回到消息循环时执行它来工作。

这就是为什么 Dispatcher.(Begin)Invoke 不能在你的方法中间“注入”代码的原因,在你的方法返回之前你不会回到消息循环。

任何代码都可以运行消息循环。当您调用任何运行消息循环的程序时,将调用 Dispatcher 并可以运行 (Begin)Invoke 操作。

什么样的代码有消息循环?

  1. 任何具有 GUI 或接受用户输入的东西,例如对话框、消息框、拖放等 - 如果这些没有消息循环,则应用程序将无响应且无法处理用户输入。
  2. 在幕后使用 windows 消息的进程间通信(包括 COM 在内的大多数进程间通信方法都使用它们)。
  3. 其他任何需要很长时间并且不会冻结系统的东西(系统未冻结的速度很快就证明它正在处理消息)。

所以,总结一下:

  • Dispatcher 不能只是将代码放入您的线程中,它只能在应用程序处于“消息循环”时执行代码。
  • 您编写的任何代码都没有消息循环,除非您明确编写它们。
  • 大多数 UI 代码没有它自己的消息循环,例如,如果您调用 Window.Show 然后进行一些长时间的计算,则窗口只会在计算完成并且方法返回后出现(并且应用程序返回到消息循环并处理打开和绘制窗口所需的所有消息)。
  • 但是任何在返回之前与用户交互的代码(MessageBox.Show、Window.ShowDialog)都必须有一个消息循环。
  • 一些通信代码(网络和进程间)使用消息循环,有些则不使用,具体取决于您使用的具体实现。
于 2009-10-07T08:49:31.123 回答
0

这是个有趣的问题。 在调度程序中执行的项目被排队并在与UI 交互相同的线程上执行。这是关于这个主题的最佳文章:http: //msdn.microsoft.com/en-us/library/ms741870.aspx

如果我冒险猜测,我会说Dispatcher.Invoke(Action) 可能会排队一个原子工作项,所以它可能会没问题,但是我不确定它是否将你的 UI 事件处理程序包装在一个原子中行动项目例如:

//Are these bits atomic?  Not sure.
upload.Cancel();
_cancelled = true;

为了安全起见,我会亲自锁定,但您的问题需要更多研究。可能需要潜入反射器才能确定。

顺便说一句,我可能会稍微优化你的锁。

Dispatcher.Invoke(() => 
{
     if (!cancelled)
     {
          lock(myLock)
          {
               if(!cancelled)
                    view.Progress = e.Value;
          }
     }
}

但这可能是矫枉过正:)

于 2009-10-06T19:55:32.337 回答