【问题标题】:Raising PropertyChanged in asynchronous Task and UI Thread在异步任务和 UI 线程中提高 PropertyChanged
【发布时间】:2014-06-18 19:38:46
【问题描述】:

在许多博客、教程和 MSDN 中,我可以读到从非 UI 线程访问 UI 元素是不可能的 - 好吧,我会得到一个未经授权的异常。为了测试它,我写了一个非常简单的例子:

// simple text to which TextBlock.Text is bound
private string sample = "Starting text";
public string Sample
{
    get { return sample; }
    set { sample = value; RaiseProperty("Sample"); }
}

private async void firstButton_Click(object sender, RoutedEventArgs e)
{
    await Job(); // asynchronous heavy job
    commands.Add("Element"); // back on UI thread so this should be ok?
}

private async Task Job()
{
    // I'm not on UI Thread ?
    await Task.Delay(2000); // some other job
    Sample = "Changed";  // not ok as not UI thread?
    commands.Add("Element from async"); // also not ok?
}

我有一个异步运行的Task。在那个Task 中,我想更改我的属性(这将引发PropertyChanged)并将元素添加到ObservableCollection。因为它运行async,我不应该这样做,但我没有例外,代码工作正常。因此我的怀疑和误解:

  • 为什么我没有得到异常?
  • 可以在async任务中提出PropertyChanged吗?
  • 是否可以在async Task中修改ObservableCollection,还是返回Task<ICollection>,得到结果后修改ObservableCollection- 清除并填写?
  • 我什么时候在 UI 线程的Task 中,什么时候不在?
  • 在上面firstButton_Click 的代码中,在awaitTask 之后管理UI 元素是否可以?我总是回到 UI 线程吗?

为了进行更多测试,我将我的属性更改和集合修改放在其他线程中:

System.Threading.Timer newThreadTimer = new System.Threading.Timer((x) =>
   {
       Sample = "Changed";  // not UI thread - exception
       commands.Add("Element from async"); // not UI thread - exception
   }, null, 1000, Timeout.Infinite);

在上面的代码中,我的想法是好的——就在第一行或第二行之后,我得到了一个异常。但是第一个代码呢?我的Task在 UI 线程上运行只是运气吗?

我怀疑这是非常基本的事情和我的误解,但我需要一些澄清,因此这个问题。

【问题讨论】:

    标签: c# multithreading asynchronous async-await


    【解决方案1】:

    当等待Task 时,当前线程的SynchronizationContext 被捕获(特别是在Task 的情况下由TaskAwaiter 捕获)。然后将延续编组回 SynchronizationContext 以执行该方法的其余部分(await 关键字之后的部分)。

    让我们看看你的代码示例:

    private async Task Job()
    {
        // I'm not on UI Thread ?
        await Task.Delay(2000); // some other job
        Sample = "Changed";  // not ok as not UI thread?
        commands.Add("Element from async"); // also not ok?
    }
    

    当您await Task.Delay(2000) 时,编译器会隐式捕获SynchronizationContext,即当前您的WindowsFormsSynchronizationContext。当 await 返回时,继续在相同的上下文中执行,因为您没有明确告诉它不要这样做,这是您的 UI 线程。

    如果您将代码更改为await Task.Delay(200).ConfigureAwait(false),则延续不会编组回您当前的SynchronizationContext,并且会运行ThreadPool 线程,从而导致您的UI 元素更新引发异常。

    在您的计时器示例中,Elapsed 事件是通过 ThreadPool 线程引发的,因此您会收到一个异常,即您尝试更新由不同线程控制的元素。

    现在,让我们一一解答您的问题:

    为什么我没有得到异常?

    如前所述,await Task.Delay(2000) 在 UI 线程上执行了 Continuation,从而可以更新您的控件。

    可以在异步任务中提升属性吗?

    我不确定“引发属性”是什么意思,但如果你的意思是引发 INotifyPropertyChanged 事件,那么是的,可以不在 UI 线程上下文中执行它们。

    是否可以在异步任务中修改 ObservableCollecition,或者我应该 返回 Task 并在获得结果后修改 Observable - Clear 它并填充它?

    如果您有 async 方法并且想要更新 UI 绑定元素,请确保在 UI 线程上编组延续。如果从 UI 线程调用该方法并且您 await 其结果,则继续将隐式地在您的 UI 线程上运行。如果您想通过 Task.Run 将工作卸载到后台线程并确保在 UI 上运行您的延续,您可以使用 TaskScheduler.FromCurrentSynchronizationContext() 捕获您的 SynchronizationContext 并将其显式传递给延续

    我什么时候在 UI 线程的 Task 中,什么时候不在?

    Task 是对未来工作的承诺。当您在 UI 线程上下文中的 TaskAwaitableawait 时,您仍然在 UI 线程上运行。如果出现以下情况,您不在 UI 线程中:

    1. 您的异步方法当前正在从不同于 UI 线程的线程(ThreadPool 线程或新的 Thread)执行
    2. 您使用 Task.Run 将工作卸载到后台 ThreadPool 线程。

    在上面的代码中 firstButton_Click 是否可以管理 UI 元素 在等待任务之后?我总是回到 UI 线程吗?

    只要您没有使用ConfigureAwait(false) 明确告诉您的代码不要返回到其当前上下文,您就会返回到 UI 线程

    【讨论】:

    • 如果不是这个声明,我会 +1:“任务并行库使用 TaskScheduler 隐式(或显式...)捕获当前的 SynchronizationContext... " 这对于await 延续不太正确:stackoverflow.com/q/23071609/1768303
    • 长答案(谢谢),但我有一些疑问: 1. 看看我的作业是从 Click 以异步 await Job(); 运行的 - 所以捕获的 UI 上下文是前后而不是内部? 2. 我对在同一个线程上从等待返回感到困惑——例如I cannot await inside Mutex,因此在返回“捕获的上下文很可能意味着任何线程池线程”之后。 3.如果可以从异步任务(在不同的线程上)提高属性更改,那么为什么不是定时器?
    • @Romasz 你混淆了asynchronous/synchronousconcurrent 的不同概念。您编写的所有代码都是单线程的。这意味着你没有问题。 async/await 的初学者错误是认为,对...我们是异步的,这意味着线程对吗?线程意味着concurrent,过去这是实现asynchronous的唯一方法。
    • 也许我们应该在聊天中继续这个? chat.stackoverflow.com/rooms/51870/async-talk
    • @Noseratio 改了,希望这次能正确描述流程:)
    【解决方案2】:

    PropertyChanged 事件由 WPF 自动分派到 UI 线程,因此您可以修改从任何线程引发它的属性。在 .NET 4.5 之前,ObservableCollection.CollectionChanged 事件不是这种情况,因此,在 UI 之外的线程中添加或删除元素会导致异常。

    从 .NET 4.5 开始,CollectionChanged 也会自动分派到 UI 线程,因此您无需担心。话虽如此,您仍然需要从 UI 线程访问 UI 元素(例如按钮)。

    【讨论】:

    • 更正。 WPF 总是通过绑定分派给 UI 线程。问题是它从不支持在 UI 线程之外更改集合。这通常表现在 CollectionViewSource 类不支持等等等等。
    猜你喜欢
    • 1970-01-01
    • 2017-07-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-08-06
    相关资源
    最近更新 更多