C#5异步支持将如何帮助解决UI线程同步问题?


16

我在某处听说C#5 async-await非常棒,您不必担心这样做:

if (InvokeRequired)
{
    BeginInvoke(...);
    return;
}
// do your stuff here

看起来,等待操作的回调将在调用者的原始线程中发生。埃里克·利珀特(Eric Lippert)和安德斯·海斯伯格(Anders Hejlsberg)多次指出,此功能源于使UI(尤其是触摸设备UI)具有更高响应能力的需求。

我认为此类功能的常见用法如下:

public class Form1 : Form
{
    // ...
    async void GetFurtherInfo()
    {
        var temperature = await GetCurrentTemperatureAsync();
        label1.Text = temperature;
    }
}

如果仅使用回调,则设置标签文本将引发异常,因为未在UI线程中执行该文本。

到目前为止,我找不到任何资源可以证实这种情况。有人知道吗?是否有任何文件从技术上解释这将如何工作?

请提供可靠来源的链接,而不仅仅是回答“是”。


至少在await功能方面,这似乎极不可能。继续传递只是很多语法上的糖。可能对WinForms进行了其他一些不相关的改进,应该有所帮助吗?但是,那将属于.NET框架本身,而不是C#。
亚伦诺特,2011年

@Aaronaught我同意,这就是为什么我要问这个问题。我编辑了问题以弄清楚我来自哪里。听起来很奇怪,他们会创建此功能,但仍然要求我们使用臭名昭著的InvokeRequired样式的代码。
亚历克斯(Alex)

Answers:


17

我认为您在这里有些困惑。您所要求的已经可以使用来实现System.Threading.Tasks,在C#5中,asyncawait只是为同一功能提供更好的语法糖。

让我们使用一个Winforms示例-在表单上放置一个按钮和一个文本框,然后使用以下代码:

private void button1_Click(object sender, EventArgs e)
{
    Task.Factory.StartNew<int>(() => DelayedAdd(5, 10))
        .ContinueWith(t => DelayedAdd(t.Result, 20))
        .ContinueWith(t => DelayedAdd(t.Result, 30))
        .ContinueWith(t => DelayedAdd(t.Result, 50))
        .ContinueWith(t => textBox1.Text = t.Result.ToString(),
            TaskScheduler.FromCurrentSynchronizationContext());
}

private int DelayedAdd(int a, int b)
{
    Thread.Sleep(500);
    return a + b;
}

运行它,你会看到(一)它不会阻塞UI线程以及(b)你没有得到通常的“跨线程操作无效”错误-除非你删除TaskScheduler从最后一个参数ContinueWith,在那样的话

这是沼泽标准的连续通过样式。魔术发生在TaskScheduler类中,特别是在检索的实例中FromCurrentSynchronizationContext。将其传递给任何延续,您将告诉延续必须在称为FromCurrentSynchronizationContext方法的任何线程(在本例中为UI线程)上运行。

从某种意义上来说,等待者稍微更加复杂,因为他们知道他们从哪个线程开始,并且需要在哪个线程上进行延续。所以上面的代码可以写成一点点更自然:

private async void button1_Click(object sender, EventArgs e)
{
    int a = await DelayedAddAsync(5, 10);
    int b = await DelayedAddAsync(a, 20);
    int c = await DelayedAddAsync(b, 30);
    int d = await DelayedAddAsync(c, 50);
    textBox1.Text = d.ToString();
}

private async Task<int> DelayedAddAsync(int a, int b)
{
    Thread.Sleep(500);
    return a + b;
}

这两个应该看起来非常相似,事实上,他们非常相似的。该DelayedAddAsync方法现在返回a Task<int>而不是一个int,因此a await只是将延续拍打到其中的每一个上。主要区别在于它沿着每一行的同步上下文传递,因此您不必像上一个示例中那样显式地进行同步。

从理论上讲,差异要大得多。在第二个示例中,方法中的每一行button1_Click实际上都是在UI线程中执行的,但是任务本身(DelayedAddAsync)在后台运行。在第一个示例中,除了我们明确附加到UI线程的同步上下文的分配之外所有内容都后台运行。textBox1.Text

真正有趣的是await-等待者能够跳入和跳出相同方法而没有任何阻塞调用。你打电话await,当前线程再次进入处理消息,并且当它这样做时,awaiter将正好拿起它离开的地方,在它英寸离开同一线程,但在你的条件Invoke/ BeginInvoke的问题,我”对比很抱歉,您应该早就停止这样做了。


@Aaronaught非常有趣。我知道连续传递样式,但不知道整个“同步上下文”。是否有任何文档将此同步上下文链接到C#5 async-await?我知道它是现有功能,但事实上,他们默认使用它听起来很重要,特别是因为它必须对性能产生重大影响,对吧?还有其他意见吗?谢谢您的回答。
Alex

1
@Alex:有关所有这些后续问题的答案,建议您阅读Async Performance:了解Async和Await的成本。“关心上下文”部分说明了它们如何与同步上下文相关联。
亚罗诺

(顺便说一下,同步上下文并不是新事物;它们从2.0开始就已经存在于框架中。TPL使得它们更易于使用。)
。TPL使其

2
我想知道为什么仍然有很多关于使用InvokeRequired样式的讨论,而我所见过的大多数线程甚至都没有提到同步上下文。本来可以节省我提出这个问题的时间...
Alex

2
@Alex:我想你选错地方了。我不知道该怎么说 .NET社区中有很大一部分需要很长时间才能赶上。地狱,我仍然看到一些编码员ArrayList在新代码中使用该类。我自己对RX几乎没有任何经验。人们学会了他们需要知道的内容并分享了他们已经知道的内容,即使他们已经知道的内容已经过时。这个答案可能会在几年后过时。
亚伦诺特,2011年

4

是的,对于UI线程,将在调用者的原始线程上发生await操作的回调。

埃里克·利珀特(Eric Lippert)一年前写了一个由8部分组成的系列文章:《精彩的编码历险记》

编辑:这是安德斯的// build /演示:channel9

顺便说一句,您是否注意到,如果将“ // build /”倒置,则会得到“ / plinq //” ;-)


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.