【问题标题】:Awaiting custom functions等待自定义函数
【发布时间】:2014-02-21 12:24:58
【问题描述】:

我正在尝试了解 C# 中的新异步功能,到目前为止,我注意到的最奇怪的事情是,异步功能的每个示例都有一个等待 另一个异步的函数框架中定义的函数,但都没有自定义代码。

例如,我想要从文本文件中的每一行创建一个对象,但是是异步的,这样 UI 线程就不会冻结:

async Task Read()
{
    string[] subjectStrings = File.ReadAllLines(filePath);
    for (int i = 0; i < subjectStrings.Length; i++)
    {
        Task<Subject> function = new Task<Subject>(code => new Subject((string)code), subjectStrings[i]);
        try
        {
            Subject subject = await function;
            subjects.Add(subject);
        }
        catch (Exception ex)
        {
            debugWriter.Write("Error in subject " + subjectStrings[i]);
            continue;
        }
    }
}

如你所见,我定义了一个任务,它根据文本文件中的一行创建一个新的Subject 对象,然后等待这个任务。如果我这样做,调试器会到达await 行,然后停止。据我所知,没有更多代码运行。

如果我使用旧的异步功能,我只需使用 Task.ContinueWith() 并添加一个回调 lambda,将主题添加到列表中,然后就可以了。

所以我的问题是:

  1. 为什么这段代码不起作用?您应该如何创建一个不使用任何异步方法本身的自定义异步方法?
  2. 您应该如何使用异步方法?你不能使用await,除非你在一个异步函数中,而且你不应该在没有等待的情况下调用异步方法,那么你如何首先从同步方法中调用该方法?

【问题讨论】:

  • what you're talking about here 的示例以及为什么不应该这样做...
  • 请不要在标题中包含语言标签,除非没有它就没有意义。标记用于此目的。
  • 但这仅与C#有关?
  • 说,你的Subject构造函数是什么样的?

标签: c# .net multithreading asynchronous


【解决方案1】:

你没有开始任务 - 所以它永远不会完成。

使用Task.Run 而不是new Task,它将为您创建并开始任务。

请注意,您仍在同步读取文件,这并不理想...如果您的 Subject 构造函数真的需要很长时间才能完成,我会质疑它是否应该做一个构造函数。

【讨论】:

  • @Luaan 最后一点是构造函数不应该执行昂贵的操作,而不是他们应该异步执行它们。
  • @StevenLiekens 没有误解。这就是我要强调的一点——如果你需要并行运行构造函数,那你就做错了。很多事情很可能发生。
  • 并不是每个构造函数都需要很长时间,因为我想在不阻塞 UI 线程的情况下运行数千个这样的构造函数。我可以使用单独的 .Read() 函数,但肯定不如构造函数清楚吗?
  • @Miguel:听起来您应该考虑在单个任务中执行整个“读取文件,调用构造函数”块。我假设subjects.Add(subject) 会影响用户界面吗? (如果是这样,请在最后对所有主题执行此操作。)
  • 是的,我可以(并且可能应该)这样做。是的,subjects 是绑定到 WPF ListBox 的 ObservableCollection
【解决方案2】:

为什么这段代码不起作用?您应该如何制作一个不使用任何异步方法本身的自定义异步方法?

使用await Task.Runawait Task.Factory.StartNew 创建并运行任务。调用new Task 将创建一个尚未开始的任务。在大多数情况下,这是不必要的,但您可以在以这种方式创建的任务上调用 Start

你应该如何使用异步方法?除非在异步函数中,否则不能使用 await,并且不应该在没有 await 的情况下调用异步方法,那么如何首先从同步方法中调用该方法?

适当的“根”异步调用取决于应用程序的类型:

  • 在控制台应用程序中:Wait 返回 Task

  • 在 GUI 应用程序中:使用 async void 事件处理程序。

  • 在 ASP.NET MVC 中:控制器可以返回 Task

【讨论】:

  • 您能否详细说明您将如何在控制台应用程序中执行此操作?等待返回的任务是什么意思?
  • Main 控制台应用程序中的方法不能是异步的。因此,如果您需要调用async Task f() 方法,则将其称为f().Wait() 而不是await f()。这会阻塞调用线程,直到调用完成。
【解决方案3】:

您应该如何创建一个不使用任何异步方法本身的自定义异步方法?

你没有。如果该方法没有异步工作要做,它应该是同步的;它不应该是async

在核心,所有async 方法都归结为两种方法之一。它们要么通过Task.Run 之类的方式将工作排队到线程池(不推荐用于库代码),要么通过TaskCompletionSource&lt;T&gt;Task.Factory.FromAsync 之类的快捷方式执行真正的异步工作。

你应该如何使用异步方法?除非您在异步函数中,否则您不能使用 await,并且您不应该在没有 await 的情况下调用异步方法,那么您如何首先从同步方法中调用该方法?

你没有。理想情况下,您应该一直是async。控制台应用程序是此规则的一个例外;他们必须拥有一个同步的Main。但是对于 WinForms、WPF、Silverlight、Windows Store、ASP.NET MVC、WebAPI、SignalR、iOS、Android 和 Windows Phone 应用程序以及单元测试,您应该一直使用异步。

您可以通过awaitTask.WhenAllTask.WhenAny 等组合符使用async 方法。这是使用async 方法的最常见方式,但不是唯一的;例如,您可以调用 async 方法并将其作为 IObservable&lt;T&gt; 使用。

【讨论】:

  • “如果该方法没有异步工作要做,它应该是同步的”我确实有异步工作。我想将这个文件读入一个数组,同时允许用户在 UI 中做任何他们想做的事情。那应该如何同步?
  • 如果每个方法都应该是异步的,为什么 WPF 或 Winforms 不能自动实现异步功能?
  • @Miguel:如果您正在执行文件 I/O,那么这是异步的,您应该使您的方法异步。我要说的是不是每个方法都应该是异步的;只有当它有异步工作要做时,它才应该是异步的。
  • 但是我是否在做 I/O 并不重要。即使我正在做任意数学方程式,同时运行 UI,这不应该是异步的吗?
  • @Miguel:数学是同步工作的一个例子。如果您想从您的 UI 中卸载它,那么您可以使用 Task.Run 将其视为异步。但是您不想创建一个公开异步 API 来进行数学运算的库,因为这项工作自然不是异步的。 I explain why on my blog.
【解决方案4】:

您混淆了等待工作。等待使用async/await,工作没有。这仍然意味着您可以await 一个 CPU 密集型任务,但您必须手动运行它,例如:

var result = await Task.Run(YourLongOperation);

帮助我从直觉上理解这一点的一个区别是,等待是合作的 - 我自愿放弃我的 CPU 时间份额,因为我实际上并不需要它。另一方面,工作必须并行运行。

在正常情况下,仅使用固有异步async/awaits,不必多于您开始使用的单个线程。将受 CPU 限制的操作与受 I/O 限制的操作结合通常是个坏主意(受 CPU 限制的操作会阻塞,除非您明确地并行运行任务)。

【讨论】:

    【解决方案5】:

    我不会对 Yet Another Technical Explanation™ 感到厌烦,而是让我根据您的代码示例向您展示一个实际示例。

    从在调用线程上完成所有工作的同步版本开始。

    class SubjectFactory
    {
        public IEnumerable<Subject> Read(string filePath)
        {
            string[] subjectStrings = File.ReadAllLines(filePath);
    
            return Parse(subjectStrings);
        }
    
        private IEnumerable<Subject> Parse(IEnumerable<string> subjects)
        {
            string code = "XYZ";
    
            foreach ( var subject in subjects )
            {
                yield return new Subject(code, subject);
            }
        }
    }
    

    假设Subject中的构造函数是轻量级的,那么最大的瓶颈就是File.ReadAllLines。为什么?因为磁盘 I/O 本身就很慢。

    那么你如何将它包装在一个任务中呢?

    如果框架有 File.ReadAllLinesAsync() 方法,您可以等待它并完成它。

    public async Task<IEnumerable<Subject>> ReadAsync(string filePath)
    { // Doesn't exist!
        string[] subjectStrings = await File.ReadAllLinesAsync(filePath);
    
        return this.Parse(subjectStrings);
    }
    

    不幸的是,生活是艰难的,并行编程也是如此。看起来你必须重新发明轮子。

    private async Task<string[]> ReadAllLinesAsync(string filePath)
    {
        ArrayList allLines = new ArrayList();
    
        using ( var streamReader = new StreamReader(File.OpenRead(filePath)) )
        {
            string line = await streamReader.ReadLineAsync();
    
            allLines.Add(line);
        }
    
        return (string[]) allLines.ToArray(typeof(string));
    }
    

    现在您可以像以前一样做同样的事情,但使用您的自定义方法ReadAllLinesAsync()

    public async Task<IEnumerable<Subject>> ReadAsync(string filePath)
    {
        // call with 'this' instead of 'File'
        string[] subjectStrings = await this.ReadAllLinesAsync(filePath);
    
        return Parse(subjectStrings);
    }
    

    一切就绪后,在您的 WPF 应用程序中,您要做的就是:

    var filePath             = @"X:\subjects\";
    var subjectFactory       = new SubjectFactory();
    var subjectsCollection   = await subjectFactory.ReadAsync(filePath);
    var observableCollection = new ObservableCollection<Subject>(subjectsCollection);
    

    【讨论】:

      猜你喜欢
      • 2021-08-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-05-27
      • 1970-01-01
      • 1970-01-01
      • 2013-06-10
      • 1970-01-01
      相关资源
      最近更新 更多