你在那里,异步写入的数值?

8
最近几天我一直在学习关于异步/等待(async/await)的内容。昨天我在 Channel 9 上发现了这个视频,让我对一些问题产生了疑问。请看下面的幻灯片。
除了 Lucian Wischik 提到的问题之外,我还想知道变量赋值的情况。假设我们将 async void 改为 async Task,并在 SendData 调用之前添加 await。这样我们就可以获取流,分配变量 m_GetResponse,等待两秒钟并打印它。但是这个变量会发生什么?它可能被与读取它的线程不同的线程写入。我们需要在这里使用某种内存屏障、使变量成为 volatile,或者其他什么方法吗?当我们打印它时,它仍然可能为空吗?

我认为两个答案都很好,但我不能接受两个答案。对此很抱歉。=) - Caramiriel
async-await 对并发性没有任何影响。这是一个完全不同的问题。 - Paulo Morgado
相关内容:内存屏障生成器 - Theodor Zoulias
也有一些相关的内容:多个Parallel.ForEach调用,MemoryBarrier? - undefined
2个回答

4
在上面的例子中,可以安全地读取 m_GetResponse,因为在 UI 线程中调用该方法时赋值将会发生在同一个 UI 线程中。
这是因为 SynchronizationContext 将被捕获并在异步方法恢复后继续。因此,写入字段和读取字段的是同一个 UI 线程,这里不会有问题。请参阅我的相关答案
如果从非 UI 上下文调用,则不能保证继续运行在同一线程中。通常情况下,它将在线程池线程中运行。由于字段读取不是 volatile 的,如果没有插入必要的屏障,可能会获取到先前的值。但是您不需要担心,因为 TPL已经为您做了这些工作
从上面的链接中可以看出:

是的,TPL在任务排队时和任务执行的开始/结束时包含适当的屏障,以便正确地使值可见。

因此,在使用 TPL 时,您无需担心内存屏障,因为任务已经完成。但是如果您手动创建线程(您不应该这样做)并直接处理线程,则必须插入必要的内存屏障。
顺便说一下,ReadToEnd 是一个阻塞调用。我不会在 UI 线程中调用它。我会使用 ReadToEndAsync 来释放您的 UI 线程。并且我不会在这里使用字段;我会从异步方法中返回该值,因为每个方法调用都只依赖于参数,因此从方法中返回该值是有意义的。
因此,您的方法将变为以下内容:
private async Task<string> SendDataAsync(string url)
{
    var request = WebRequest.Create(url);
    using(var response = await request.GetResponseAsync());
    using(var reader = new StreamReader(request.GetResponseStream());
        return await reader.ReadToEndAsync();
}

2
async/await确实提供了完整的内存屏障,因此即使在非UI上下文中,如果您使用await,也不会出现看不到更新值的危险。这仅在真正的竞态条件场景中存在问题,其中多个线程可能会更新该值。 - Stephen Cleary
@StephenCleary 感谢您提供的信息。我快速搜索了一下,发现Stephen Toub在此处发表的评论。我已经更新了我的答案,请查看。 - Sriram Sakthivel
1
TPL博客现在已经存档在此处,但原始评论已被删除。不过,WayBack机器已经备份了它。 - tm1
1
@tm1 谢谢提醒链接已经失效。将更新新的链接。为什么微软不能提供旧链接的重定向 :( - Sriram Sakthivel

3
但变量会发生什么?它可能被不同的线程写入和读取。
如果`m_GetResponse`是一个私有字段,并且该类被不同的线程多次调用,则在其他人尝试读取它时,它的值可能是“脏”的。为了使其线程安全,您可以在其周围进行锁定。似乎作者的意图是仅从UI线程调用它,因此他将`SendData`作为私有方法。在这种情况下,`m_GetResponse`作为私有字段是安全的,因为负责变量赋值的异步方法的继续将发生在UI消息循环中。
它在打印时仍然可能为空吗?
如果代码中的其他地方将该变量设置为null,那么它可能为空,因为它是一个类级别的变量。如果你正在谈论“我们是否尝试在await完成状态机执行之前打印m_GetResponse”,那么不会。再一次,我不确定作者的意图是否围绕并发执行而制定,而是为了向您展示async-await功能。
还是其他原因?
为了使其线程安全,您可以简单地删除全局变量,并返回一个本地变量。`SendData`不应该是`async void`,因为它不像`Button1_Click`一样用于事件处理程序委托分配。
您可以这样做得更好(我将使用`HttpClient`来简化):
public async Task<string> SendDataAsync(string url)
{
    var httpClient = new HttpClient();
    var response = await httpClient.GetAsync();
    return response.Content.ReadAsStringAsync();
}

请注意,async-await并不是为了解决并行性问题,而更多的是为了处理并发性以及简化自然异步IO操作的使用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接