【问题标题】:Why does the file async API block为什么文件异步 API 阻塞
【发布时间】:2012-04-11 21:57:26
【问题描述】:

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

当我运行程序时,它永远不会从 OnTest 中返回。是不是你得到的。 我了解 .Wait 将等待文件和文件夹的创建完成。也许这不是很好的设计。然而,这不是重点。

我的问题是:

  • 你是否得到相同的行为(永远阻止程序)
  • 这是应该发生的事情还是 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);
            }
        }

它在第二次调用 new Cache("test2"); 时阻塞;

【问题讨论】:

    标签: c# .net asynchronous microsoft-metro


    【解决方案1】:

    我没有尝试运行您的程序或重现您的问题,但我可以对正在发生的事情做出有根据的猜测。

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

    • 在邮箱里给妈妈写一封信。
    • 我一读到她的回复,就设置闹钟叫醒我。
    • 去睡觉吧。
    • 查看邮箱是否有回复。
    • 阅读回复。

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

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

    埃里克,谢谢你的解释。

    不客气。

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

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

    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 首先完全否定了异步的全部意义。只是不要那样做;说“我想异步”和“但我想同步等待”都没有任何意义。这些是相反的。

    【讨论】:

    • 埃里克,谢谢你的解释。但是,我仍然对为什么我的代码不起作用感到困惑。似乎文件和文件夹的内部代码正在做一些我不知道的奇怪事情。如果有机会,请尝试我的代码。我很想知道它的行为方式是否符合您的预期。
    • 埃里克,我知道死锁会是个问题。但是,就我而言,所有三个任务都在独立文件夹上工作。换句话说,完成一项任务不会影响其他任务。旁边,他们一个接一个地跑。那么,你的观点与这个例子有什么关系呢?
    • @user380719:我的类比与您的情况完全相关。以此类推,“你的妈妈”是文件系统,“邮件”是线程的消息队列。你的妈妈——文件系统——给你发了一封信说工作已经完成,你告诉自己要去睡觉,直到你检查了邮件并得到回复。这没有任何意义。我已经用更详细的解释更新了我的答案。
    • 我认为这不会与线程池同步上下文发生死锁。即规则是从不调用 .Wait 在 UI 线程上
    • 不仅仅是 UI 线程。我recommend 一个更强的规则:永远不要在异步代码库中调用Wait(或Result)。同样的死锁情况也会发生在其他情况下,例如 ASP.NET(即使在那种情况下有两个不同的线程,它们仍然会死锁),或者 .NET 4.5 中的独占线程池调度程序......
    【解决方案2】:

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

    【讨论】:

    • 德鲁,也许我的设计不是很好。但这不是我的问题。我的问题是为什么它会永远阻塞?
    • 我的第一段解释了这一点。如果你切换到我的实现并等待 Task.WhenAll 你不再阻塞。
    • 德鲁,感谢您的澄清。我仍然很困惑。我同意它会阻塞,直到创建文件和文件夹。所以我预计构造函数可能会阻塞一两秒。但是,它会永远阻塞,就好像文件系统被某种方式锁定了一样。是不是应该的样子?
    • 如果您仍在使用原始代码,原因可能是您定义了异步方法 TestRetrieve 以便返回一个任务。尝试将您的 TestRetreive 方法更改为具有 void 返回类型,看看是否有帮助。然后,您还需要更改为在构造函数中使用 await,这就是为什么您应该将设计更改为我建议的更干净地适合异步模型的原因。
    • 死锁是因为TestReceive试图在UI线程上恢复,但是UI线程在TestReceive上被阻塞了。我建议您阅读我的async intro post,它解释了async 方法使用的“上下文”。在那篇文章中,我解释了一些“推荐的最佳实践”,例如尽可能调用ConfigureAwait,尽可能返回Task而不是void,等等。另请参阅this MSDN forum post和Stephen Toub的Zen of Async,他实际演示了这个确切的死锁情况。
    【解决方案3】:
    TestRetrieve(name).Wait();
    

    您是在告诉它使用 .Wait() 调用来专门阻止。

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

    【讨论】:

    • 我想等待,而不是阻止。稍等一下就好了。但是,程序会永远...
    • 因为Wait 不允许其他任务运行。即使这些其他任务是您的事物需要完成的任务。要让其他任务在等待时运行,请使用await
    • Raymond,你是说程序将永远阻塞的预期行为吗?如果是这样的话,我敢打赌很多 .NET 开发人员会感到惊讶。
    • 我认为 Raymond 的意思是 Wait 不允许其他任务在该线程上运行。是的,这已经绊倒了几个人,但是如果你允许async 在你的代码库中自然增长,你最终将根本不会调用Wait(或Result)。
    【解决方案4】:

    现有的答案提供了非常详尽的解释,解释了它为什么会阻塞,并提供了如何使其不阻塞的代码示例,但这些答案比某些用户理解的“更多信息”。这是一个更简单的“面向机制”的解释..

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

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

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

    UI 线程上下文如下所示...

    UI-Thread ->
        OnTest
    

    由于您一直没有连接的等待/异步调用链,因此您从未将 TestRetrieve 异步上下文“附加”到上述 UI-Thread 异步链。实际上,您创建的新上下文只是悬空无处可去。因此,当您“等待” UIThread 时,它会直接回到顶部。

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

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

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

    UI-Thread ->
        OnTest ->
            TestRetrieve
    

    现在 UI 线程异步循环处理程序可以在异步完成期间正确恢复 TestRetrieve,您的代码将按预期工作。

    如果您想创建一个“异步构造函数”,您需要执行类似 Drew 的 GetCacheAsync 模式的操作,在该模式中创建一个静态异步方法,该方法同步构造对象,然后等待异步方法。这通过一直等待和“附加”上下文来创建正确的异步链。

    【讨论】:

      猜你喜欢
      • 2011-05-02
      • 2015-03-09
      • 1970-01-01
      • 2019-10-16
      • 1970-01-01
      • 2012-01-15
      • 1970-01-01
      • 2020-07-16
      • 2018-04-06
      相关资源
      最近更新 更多