我在这个异步示例中做错了什么吗?

Am I doing anything wrong in this async example?

我是异步编程的新手,我一直在研究一个小例子来演示如何使用任务进行编程。我想知道你对样本的看法。

我是不是堵在什么地方了?我做错了什么吗? 能否以某种方式做得更好?

示例代码如下:

static void Main(string[] args)
{
    const string directory= "files";

    IEnumerable<string> files = FindFiles(directory).ToList();

    DateTime startAsink = DateTime.Now;

    ProcessFilesAsync(files).ContinueWith(r =>
    {
        r.Result.ToList().ForEach(Console.WriteLine);
        Console.WriteLine(DateTime.Now.Ticks - startAsink.Ticks);
    });

    Console.ReadKey();
}

private static IEnumerable<string> FindFiles(string directoy)
{
    return Directory.GetFiles(directory).ToList();
}

private static Task<Tuple<string, int>> ProcessOneFile(string name)
{
    return Task.Run(() =>
    {
        IEnumerable<string> lines = File.ReadLines(name);
        int sum = 0;

        foreach (var line in lines )
        {
            sum += line.Split(' ').Length;
        }

        return new Tuple<string, int>(name, sum);
    });
}

    private static async Task<IEnumerable<Tuple<string, int>>>  ProcessFilesAsync(IEnumerable<string> files)
    {
        var listOfResults = files.Select(ProcessOneFile);

        var task = (await Task.WhenAll(listOfResults)).ToList();

        return task;
    }
}

这是您的代码的一种可能的异步重构:

    static void Main(string[] args)
    {
        const string directory = "files";
        IEnumerable<string> files = FindFiles(directory).ToList();
        Stopwatch chrono = new Stopwatch();
        chrono.Start();
        var tasks = files.Select(f => ProcessOneFileAsync(f)).ToArray();
        Task.WaitAll(tasks);
        chrono.Stop();
        foreach (var t in tasks)
        {
            Console.WriteLine(t.Result);
        }
        Console.WriteLine(chrono.ElapsedMilliseconds);
        Console.ReadKey();
    }

    private static IEnumerable<string> FindFiles(string directoy)
    {
        return Directory.GetFiles(directoy).ToList();
    }

    private static async Task<Tuple<string, int>> ProcessOneFileAsync(string name)
    {
        int sum = 0;
        using (TextReader file = File.OpenText(name))
        {
            String line = null;
            while ((line = await file.ReadLineAsync()) != null)
            {
                sum += line.Split(' ').Length;
            }
        }
        return new Tuple<string, int>(name, sum);
    }

PS :我将 DateTime 替换为 Stopwatch 以获得一致的计时结果。

Am I blocking anywhere?

不,看来您对 "async all the way" 流程的使用是正确的。 但是,当你可以通过[=12=使用其async兄弟时,你正在使用阻塞API(例如File.ReadLines) ].

Am I doing anything wrong?

我能立即发现的是:

  1. 你用的是async over sync anti-pattern。与其将对 Task.Run 的调用封装在 "async" 方法中,不如公开一个同步方法并让调用者显式调用 Task.Run。那样的话,你就不会让他假设这个方法自然是异步的,而实际上它不是。
  2. 命名约定。任何异步方法都应以 Async 后缀结尾。

Can it be made better somehow?

您可以使用 API 来自然地公开异步文件读取,而不是使用 Task.Run 来执行 IO 绑定操作,例如 FileStreamTextReader 类。这样一来,您 真的 公开了 async 方法,而不是 "async over sync" 方法。

我发现它有几个问题。通常,当一个人在工作 async 时,这是因为他们希望在可用时处理逐渐流入的结果(想想:yield),而不是等待整个集合完全形成。

每次使用 ToList 都是对 asyncyield 的一记耳光。您实质上是在声明您希望代码在继续之前等待。

考虑在您的 FindFiles 方法中使用 GetFiles。如果您省略 ToListGetFiles returns 和 string[],它仍然会实现 IEnumerable<string>。但更重要的是,您应该将 GetFiles 完全替换为 EnumerateFiles。欢迎您在闲暇时阅读 MSDN,但我确实引用了这句话:

The EnumerateFiles and GetFiles methods differ as follows: When you use EnumerateFiles, you can start enumerating the collection of names before the whole collection is returned; when you use GetFiles, you must wait for the whole array of names to be returned before you can access the array. Therefore, when you are working with many files and directories, EnumerateFiles can be more efficient.

所以FindFiles变成:

private static IEnumerable<string> FindFiles(string directory)
{
    return Directory.EnumerateFiles(directory);
}

出于同样的原因,您还应该删除其他地方对 ToList 的所有引用。这是直接处理 async 的两个最大问题。这样,您的代码确实可以执行 async 并在项目可用时对其进行处理。

另一件事是 DateTime.UtcNow 应该用于任何内部计时。 UtcNow 不仅比本地化的 DateTime.Now 快得多,而且不受时区转换的任何影响。