2014-05-02 119 views
7

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

// 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? 
} 

我正运行的异步任务,在任务我想:更改我的财产(这将提高的PropertyChanged),并添加元素的ObservableCollection。由于它运行异步,我不应该能够这样做,但我没有例外,代码工作正常。因此我的疑惑和误解:

  • 为什么我不能例外?
  • 可以在异步任务中提高PropertyChanged吗?
  • 可以在异步任务中修改ObservableCollecition,或者我应该返回Task并获得结果后修改Observable - 清除它并填充它?
  • 当我在任务上的UI线程和什么时候没有?
  • 上面的代码在firstButton_Click是否可以在等待任务后管理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); 

在上面的代码中我的想法是好的 - 只是第一或第二线,我得到一个异常后。但是第一个代码是什么?我的任务在UI线程上运行只是运气好吗?

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

回答

9

当等待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在异步任务,或者我应该 返回任务和获得的结果修改后的可观测 - 清除 并补吗?

如果您有一个async方法,并且想要更新UI绑定元素,请确保编组UI线程上的延续。如果该方法是从UI线程调用的,并且您的结果是,则该延续将隐式地在您的UI线程上运行。如果你想通过Task.Run卸载工作到后台线程,并确保您的延续是运行在用户界面中,您可以在SynchronizationContext使用TaskScheduler.FromCurrentSynchronizationContext()和明确地传递给它的延续

对UI的任务是什么时候拍摄线程和什么时候没有?

A Task是未来将要完成的工作承诺。当您从UI线程上下文的TaskAwaitable上执行await时,那么您仍然在UI线程上运行。你是不是在UI线程,如果:

  1. 您的异步方法正在从不同的线程,则UI线程(无论是ThreadPool线程或新Thread
  2. 你工作卸载到后台线程池线程执行使用Task.Run
以上面firstButton_Click代码

是它确定管理UI元素 等待任务后?我总是回到UI线程?

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

+0

我会+1这个如果不是这个声明:*“任务并行库使用'TaskScheduler'隐式(或明确地...)捕获当前的'SynchronizationContext' ...“*这是不正确的'await'延续:http://stackoverflow.com/q/23071609/1768303 – Noseratio

+0

长答案(谢谢你),但我一些疑虑:1.看看我的Job是以异步方式运行的[等待Job();'Click) - 这样捕获的UI上下文是不是内部之前和之后?2.我很困惑从同一个线程返回等待 - for例如[我无法在Mutex内部等待](http://stackoverflow.com/q/23153155/2681948),因此在返回'捕获的上下文可能意味着任何线程池线程'。3.如果它可以将属性从async Task(在不同的线程上)改变,然后为什么不使用定时器? – Romasz

+1

@Romasz你通过'asynchronous' /'synchronous'和'concurrent'的不同概念混淆了相关的问题,你写的所有代码都是单线程,这意味着你没有问题,使用'async' /'await'的初学者错误是想,对...我们重新异步,这意味着线程是正确的?线程意味着“并发”,在过去这是实现“异步”的唯一方法。 – Aron

2

PropertyChanged事件是由WPF自动分派到UI线程所以你可以修改任何线程引发它的属性。在.NET 4.5之前,ObservableCollection.CollectionChanged事件并非如此,因此,添加或删除除UI之外的线程中的元素将导致异常。

从.NET 4.5开始CollectionChanged也会自动发送到UI线程,因此您不必担心这一点。这就是说,你仍然需要从UI线程访问UI元素(例如一个按钮)。

+1

更正。 WPF总是通过绑定分派给UI线程。问题是它从来不支持在UI线程之外更改集合。这典型地表现在'CollectionViewSource'类不支持等等等等。 – Aron