3

我正在编写一个简单的地铁应用程序。但是,访问文件时 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"); 时阻塞。

4

4 回答 4

15

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

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

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

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

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

埃里克,谢谢你的解释。

别客气。

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

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

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.xml 中的任务不同的任务。返回的任务是表示作业的任务,它执行 GetATask 方法需要执行的所有操作

该任务的延续是什么?我们不知道!这由 GetATask 的调用者决定。

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

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

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

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

在这种情况下调用 Wait 显然是错误的,因为您正在等待自己在未来做某事,而那是行不通的。但更一般地说,调用 Wait首先完全否定了异步的全部意义。只是不要那样做;说“我想异步”和“但我想同步等待”都没有任何意义。那些是对立的。

于 2012-04-11T23:13:06.227 回答
7

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

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

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"));   
于 2012-04-11T22:11:24.333 回答
1
TestRetrieve(name).Wait();

你告诉它通过使用.Wait()调用来专门阻止。

删除.Wait()它,它不应该再阻塞了。

于 2012-04-11T22:08:55.950 回答
0

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

async/await 模式的工作方式,每次等待异步方法时,都是将该方法的异步上下文“附加”到当前方法的异步上下文。想象 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 模式的操作,在该模式中创建一个静态异步方法,该方法同步构造对象,然后等待异步方法。这通过一直等待和“附加”上下文来创建正确的异步链。

于 2012-04-20T08:53:34.577 回答