繁体   English   中英

为什么文件异步API会阻塞

[英]Why does the file async API block

我正在写一个简单的城域应用程序。 但是,访问文件时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);
            }
        }

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM