【问题标题】:Does using dispatcher.Invoke make my thread safe?使用 dispatcher.Invoke 是否使我的线程安全?
【发布时间】:2009-10-06 16:30:48
【问题描述】:

在我的 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;
        }
    }
}

【问题讨论】:

    标签: wpf thread-safety dispatcher


    【解决方案1】:

    您不必添加锁。 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)都必须有一个消息循环。
    • 有些通信代码(网络和进程间)使用消息循环,有些则不使用,具体取决于您使用的具体实现。

    【讨论】:

    • 您能否稍微扩展一下您的第二点 - 危险情况是什么?
    • 引用也不错。
    • 好的,我添加了一个简短的(仅大约是原始答案的 6 倍)说明 Dispatcher.(Begin)Invoke 的工作方式和时间。
    【解决方案2】:

    这是一个有趣的问题。 在调度程序中执行的项目排队并在与 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;
              }
         }
    }
    

    但这可能有点矫枉过正:)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-09-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-11-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多