为什么文件异步API会阻塞

时间:2012-04-11 21:57:26

标签: c# .net asynchronous microsoft-metro

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

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

我的问题是:

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

以下是XAML代码:

<Button Click="OnTest">Test</Button>

这是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);
            }
        }

它阻止第二次调用新缓存(“test2”);

4 个答案:

答案 0 :(得分:15)

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

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

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

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

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

  

Eric,谢谢你的解释。

欢迎你。

  

但是,我仍然对我的代码无效的原因感到困惑。

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

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显然是错误的,因为您正在等待自己在将来做某事,而这不会起作用。但更一般地说,调用Wait 完全否定了首先出现异步的全部要点。就是不要那样做;同时说“我想要异步”和“但我想同步等待”没有任何意义。那些是对立的。

答案 1 :(得分:7)

您在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"));   

答案 2 :(得分:1)

TestRetrieve(name).Wait();

您告诉它使用.Wait()电话专门阻止。

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

答案 3 :(得分:0)

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

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方法,它同步构造对象,然后等待异步方法。这通过等待和“附加”上下文一直向下创建正确的异步链。