使用 Dispatcher.Invoke 会使我的线程安全吗?

发布于 2024-08-06 15:18:30 字数 1176 浏览 5 评论 0原文

在我的 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 之后,所以我不会在那里遇到问题。

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

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;
        }
    }
}

In my WPF app I have a long running upload running, which raises progress events as it goes which updates a progress bar. The user also has a chance of cancelling the upload, or it might go wrong. These are all async events, so they need to be executed using Dispatcher.Invoke in order to update the UI.

So the code looks like this, 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;
    }
}

Supposing that setting view.Progress on a disposed view is going to throw an error, is this code thread safe? i.e. if a user clicks cancel while the progress is updating, he/she will have to wait until the progress has been updated, and if the progress is updated during execution of OnCancelButtonClicked, the Dispatcher.Invoke call will cause the view.Progress update to be queued til after _cancelled is set, so I won't get a problem there.

Or do I need a lock to be safe, a la:

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;
        }
    }
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

人海汹涌 2024-08-13 15:18:30

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

只需考虑两件事:

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

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

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

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

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

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

在 WPF 中,此循环以及所有相关处理均由 Dispatcher 处理。

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

Dispatcher.Invoke 和 BeginInvoke 的工作原理是对请求的操作进行排队,并在线程下次返回消息循环时执行它。

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

但是

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

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

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

因此,总结一下:

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

You don't have to add a lock. Dispatcher.Invoke and BeginInvoke requests will not run in the middle of other code (that's the whole point of them).

Just two things to consider:

  1. BeginInvoke may be more appropriate in this case, Invoke will queue the request and then block the calling thread until the UI thread becomes idle and finishes executing the code, BeginInvoke will only queue the request without blocking.
  2. Some operations, especially operations that open windows (including message boxes) or do inter-process communication may allow the queued dispatcher operations to run.

EDIT: first, I don't have citations because the MSDN pages on the subject are unfortunately very low on details - but I have written test programs to check the behavior of BeginInvoke and everything I write here is the result of those tests.

Now, to expand on the second point we first need to understand what the dispatcher does. Obviously this is a very simplified explanation.

Any Windows UI works by processing messages; For example when the user moves the mouse over a window the system will send that window a WM_MOUSEMOVE message.

The system send the message by adding it a queue, each thread may have a queue, all windows created by the same thread share the same queue.

In the heart of every Windows program there's a loop called "message loop" or "message pump", this loop reads the next message from the queue and calls the appropriate window's code to process that message.

In WPF this loop and all the related processing handled by the Dispatcher.

An application can either be in the message loop waiting for the next message or it could be doing something. That is why when you have a long calculation all the thread's windows become unresponsive - the thread is busy working and doesn't return to the message loop to process the next message.

Dispatcher.Invoke and BeginInvoke works by queuing the requested operation and executing it the next time the thread returns to the message loop.

That is why Dispatcher.(Begin)Invoke can't "inject" code in the middle of your method, you won't get back to the message loop until your method returns.

BUT

Any code can run a message loop. When you call anything that runs a message loop the Dispatcher will be called and can run the (Begin)Invoke operations.

What kinds of code has a message loop?

  1. Anything that has a GUI or that accepts user input, for example dialog boxes, message boxes, drag&drop etc. - if those didn't have a message loop then the app would have been unresponsive and unable to handle user input.
  2. Inter-process communication that uses windows messages behind the scenes (most inter-process communication methods, including COM, use them).
  3. Anything else that takes a long time and doesn't freeze the system (the fast that the system isn't frozen is proof it's processing messages).

So, to summarize:

  • the Dispatcher can't just drop code into your thread, it can only execute code when the application is in the "message loop".
  • Any code you write doesn't have message loops unless you explicitly wrote them.
  • Most UI code doesn't have it's own message loop, for example if you call Window.Show and then do some long calculation the window will only appear after the calculation is finished and the method returns (and the app returns to the message loop and processes all the messages required to open and draw a window).
  • But any code that interacts with the user before it returns (MessageBox.Show, Window.ShowDialog) has to have a message loop.
  • Some communication code (network and inter-process) uses message loops, some doesn't, depending on the specific implementation you are using.
魂ガ小子 2024-08-13 15:18:30

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

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

This is an interesting question. Items executed in the dispatcher are queued and execute on the same thread as UI interaction. Here's the best article on this subject:
http://msdn.microsoft.com/en-us/library/ms741870.aspx

If I were to venture a guess, I would say that Dispatcher.Invoke(Action) probably queues an atomic work item, so it's likely going to be ok, however I'm not sure if it wraps up your UI event handler in an atomic action item for instance:

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

For safety's sake I would personally lock, but your question warrants more research. Might need a dive in reflector to figure out for sure.

As an aside, I'd probably optimize your lock a bit.

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

But that's probably overkill :)

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文