为什么文件异步API会阻塞

我正在写一个简单的地铁应用程序。 但是,访问文件时API会阻塞。 通过阻止,我的意思是程序永远等待。 创建/打开文件或文件夹最多需要几秒钟。 在这种情况下,它需要永远。

当我运行程序时,它永远不会从OnTest返回。 这是你得到的。 我明白了。等待创建文件和文件夹才能完成。 也许那不是很棒的设计。 但是,这不是重点。

我的问题是:

  • 你有同样的行为(永远阻止程序)
  • 是应该发生的还是WinRT中的错误? (我正在使用消费者预览)
  • 如果这是预期的行为,为什么需要永远?

这是XAML代码:

 

这是C#代码:

  private async void OnTest(object sender, RoutedEventArgs e) { var t = new Cache("test1"); t = new Cache("test2"); t = new Cache("test3"); } class Cache { public Cache(string name) { TestRetrieve(name).Wait(); } public static async Task TestRetrieve(string name) { StorageFolder rootFolder = ApplicationData.Current.LocalFolder; var _folder = await rootFolder.CreateFolderAsync(name, CreationCollisionOption.OpenIfExists); var file = await _folder.CreateFileAsync("test.xml", CreationCollisionOption.OpenIfExists); } } 

它阻止第二次调用新的Cache(“test2”);

我没有试图运行你的程序或重现你的问题,但我可以做出有根据的猜测。

假设您自己编写了以下待办事项列表:

  • 在邮箱里给妈妈写了一封信。
  • 设置闹钟,在我看完她的回复后立即叫醒我。
  • 去睡觉。
  • 检查邮箱是否有回复。
  • 阅读回复。

现在严格按照从上到下的顺序执行该列表中的所有操作。 怎么了?

问题不在于邮局或妈妈; 他们拿起你放在邮箱里的信,发给妈妈,妈妈正在写她的回复,邮局把它还给你。 问题是你永远不会进入第四步,因为你只能完成第五启动第四步并且警报唤醒你。 你会永远沉睡,因为你基本上在等待你未来的自我唤醒现在的自我

埃里克,谢谢你的解释。

别客气。

但是,我仍然感到困惑,为什么我的代码不起作用。

好吧,让我们分解吧。 你的程序真正做了什么? 我们简化一下:

 void M() { Task tx = GetATask(); tx.Wait(); } async Task GetATask() { Task ty = DoFileSystemThingAsync(); await ty; DoSomethingElse(); } 

首先: 什么是任务 ? 任务是一个对象,它代表(1)要完成的工作,以及(2)任务继续的委托:任务完成需要发生的事情。

所以你打电话给GetATask。 它有什么作用? 嗯,它做的第一件事是它制作一个任务并将其存储在ty中。 该任务表示作业“在磁盘上启动某些操作,并在完成后通知I / O完成线程”。

这项任务的延续是什么? 完成任务后会发生什么 ? 需要调用DoSomethingElse。 因此,编译器将await转换为一堆代码,告诉任务确保在任务完成时调用DoSomethingElse。

在设置了I / O任务的延续的那一刻, 方法GetATask将一个任务返回给调用者。 那是什么任务? 这与存储到ty中的任务不同 。 返回的任务是表示作业执行GetATask方法需要执行的所有操作的任务。

这项任务的延续是什么? 我们不知道! 这取决于GetATask的调用者

好的,让我们回顾一下。 我们有两个任务对象。 一个代表任务“在文件系统上做这件事”。 它将在文件系统完成其工作时完成。 它的延续是“致电DoSomething”。 我们有第二个任务对象,表示作业“在GetATask的主体中做所有事情”。 它将在调用DoSomethingElse返回后完成。

再说一遍:当文件I / O成功时,第一个任务将完成 。 当发生这种情况时,文件I / O完成线程将向主线程发送一条消息,说“嘿,你等待的文件I / O已经完成。我告诉你这个,因为现在是时候给你调用DoSomethingElse了”。

但是主线程没有检查它的消息队列 。 为什么不? 因为你告诉它同步等待GetATask中的所有东西,包括DoSomethingElse,都完成了 。 但是现在无法处理告诉您运行DoSomethingElse消息因为您正在等待DoSomethingElse完成

现在清楚了吗? 你告诉你的线程要等到你的线程完成运行DoSomethingElse, 然后再检查“请调用DoSomethingElse”是否在这个线程的工作队列中! 你等到看完妈妈的来信后才等到,但是你正在等待同步的事实意味着你没有检查你的邮箱,看看这封信是否已经到达

在这种情况下,呼叫等待显然是错误的,因为您正在等待自己在将来做某事 ,而这不会起作用。 但更一般地说,调用Wait 完全否定了首先出现异步的整个问题 。 只是不要那样做; 同时说“我想要异步”和“但我想同步等待”没有任何意义。 那些是对立的。

您在Cache类的构造函数中使用Wait() 。 这将阻止,直到当前正在异步执行的任何内容完成。

这不是设计这个的方法。 构造函数和异步没有意义。 也许像这样的工厂方法方法会更好:

 public class Cache { private string cacheName; private Cache(string cacheName) { this.cacheName = cacheName; } public static async Cache GetCacheAsync(string cacheName) { Cache cache = new Cache(cacheName); await cache.Initialize(); return cache; } private async void Initialize() { StorageFolder rootFolder = ApplicationData.Current.LocalFolder; var _folder = await rootFolder.CreateFolderAsync(this.cacheName, CreationCollisionOption.OpenIfExists); var file = await _folder.CreateFileAsync("test.xml", CreationCollisionOption.OpenIfExists); } } 

然后你像这样使用它:

 await Task.WhenAll(Cache.GetCacheAsync("cache1"), Cache.GetCacheAsync("cache2"), Cache.GetCacheAsync("cache3")); 
 TestRetrieve(name).Wait(); 

你告诉它使用.Wait()调用来阻止它。

删除.Wait() ,它不应再阻止。

现有的答案提供了非常彻底的解释,说明为什么它阻止和编码如何使其不被阻止的例子,但这些是比一些用户理解的“更多信息”。 这是一个更简单的“机械导向”解释。

async / await模式的工作方式,每次等待异步方法时,都会将该方法的异步上下文“附加”到当前方法的异步上下文中。 想象一下等待传递一个神奇的隐藏的参数“背景”。 这个context-paramater允许嵌套的await调用附加到现有的异步上下文。 (这只是一个类比……细节比这更复杂)

如果你在异步方法中,并且你同步调用一个方法,那么同步方法不会得到那个魔术隐藏的异步上下文参数,所以它不能附加任何东西。 然后使用’wait’在该方法内创建一个新的异步上下文是无效的操作,因为新的上下文没有附加到线程的现有顶级异步上下文(因为你没有它!)。

根据代码示例描述,TestRetrieve(name).Wait(); 没有做你认为它正在做的事情。 它实际上告诉当前线程重新进入顶部的async-activity-wait-loop。 在此示例中,这是UI线程,称为OnTest处理程序。 以下图片可能有所帮助:

UI线程上下文看起来像这样……

 UI-Thread -> OnTest 

由于你没有连续的await / async调用连接链,你永远不会将TestRetrieve异步上下文“附加”到上面的UI-Thread异步链。实际上,你所创建的新上下文只是悬而未决因此当你“等待”UIThread时,它就会回到顶端。

要使异步工作,您需要通过您需要执行的所有异步操作,从顶级同步线程(在这种情况下,它是执行此操作的UI线程)中保持连接的async / await链。 您不能使构造函数异步,因此您无法将异步上下文链接到构造函数中。 您需要同步构造对象,然后从外部等待TestRetrieve。 从构造函数中删除“等待”行并执行此操作…

 await (new Cache("test1")).TestRetrieve("test1"); 

执行此操作时,’TestRetrieve’异步上下文已正确附加,因此链如下所示:

 UI-Thread -> OnTest -> TestRetrieve 

现在,UI-thread async-loop-handler可以在异步完成期间正确恢复TestRetrieve,并且您的代码将按预期工作。

如果你想创建一个’异步构造函数’,你需要做一些像Drew的GetCacheAsync模式,你可以在这里创建一个静态的Async方法,它同步构造对象然后等待异步方法。 这通过等待和“附加”上下文一直向下创建正确的异步链。