【问题标题】:Process List of Files Asynchronously using async and await in C# Console App在 C# 控制台应用程序中使用 async 和 await 异步处理文件列表
【发布时间】:2014-06-20 02:59:54
【问题描述】:

我正在一个简单的小控制台应用程序中使用 C# 中的 async 和 await。我的目标很简单:以异步方式处理文件列表,以便处理一个文件不会阻塞其他文件的处理。没有一个文件是相互依赖的,并且有(比方说)数千个文件要经过。

这是我目前拥有的代码。

public class MyClass
{
    public void Go()
    {
        string[] fileSystemEntries = Directory.GetFileSystemEntries(@"Path\To\Files");

        Console.WriteLine("Starting to read from files!");
        foreach (var filePath in fileSystemEntries.OrderBy(s => s))
        {
            Task task = new Task(() => DoStuff(filePath));
            task.Start();
            task.Wait();
        }
    }

    private async void DoStuff(string filePath)
    {
        await Task.Run(() =>
        {
            Thread.Sleep(1000);
            string fileName = Path.GetFileName(filePath);
            string firstLineOfFile = File.ReadLines(filePath).First();
            Console.WriteLine("{0}: {1}", fileName, firstLineOfFile);
        });
    }
}

而我的Main() 方法只是调用这个类:

public static class Program
{
    public static void Main()
    {
        var myClass = new MyClass();
        myClass.Go();
    }
}

我似乎遗漏了这个异步编程模式的某些部分,因为每当我运行程序时,实际处理的文件数量似乎是随机的,从一个都没有到全部六个(在我的示例文件集)。

基本上,主线程不会等待所有文件都被处理,我认为这是异步运行的一部分,但我不太希望这样。我想要的是:在尽可能多的线程中处理尽可能多的这些文件,但在完成之前仍然等待它们全部完成处理。

【问题讨论】:

  • 您开始并等待 foreach 循环中的每个任务...创建一个任务数组并使用 WaitAll。
  • 这段代码存在一些概念性问题,但主要的技术问题是:new Task(() => DoStuff(filePath)),其中DoStuff 是async void 方法。您在这里进行了一次即发即弃的调用,任务在 DoStuff 方法完成之前完成,myClass.Go() 也是如此。
  • 并行处理项目是一个非常基本的并发主题。事实上,据我所知,您的问题是关于最基本的情况。如果你朝那个方向做一些研究,那可能是最好的。您会很快找到解决方案。
  • @Scott 是的,自从引入了 async/await 的东西以来,这些概念就变得模棱两可并且误导了初学者。我看到 async/await 一直在使用,而同步线程本来可以更简单并完成同样的事情。
  • 这个问题现在已经足够清晰,可以与现有材料共存。如果您愿意,请添加到您现有的答案中。关键是这个问题对未来的访问者更有用。接受您认为对他人最有帮助的答案。

标签: c# multithreading asynchronous async-await


【解决方案1】:

async/await 背后的主要设计目标之一是促进自然异步 I/O API 的使用。有鉴于此,您的代码可能会被这样重写(未经测试):

public class MyClass
{
    private int filesRead = 0;

    public void Go()
    {
        GoAsync().Wait();
    }

    private async Task GoAsync()
    {
        string[] fileSystemEntries = Directory.GetFileSystemEntries(@"Path\To\Files");

        Console.WriteLine("Starting to read from files! Count: {0}", fileSystemEntries.Length);

        var tasks = fileSystemEntries.OrderBy(s => s).Select(
            fileName => DoStuffAsync(fileName));
        await Task.WhenAll(tasks.ToArray());

        Console.WriteLine("Finish! Read {0} file(s).", filesRead);
    }

    private async Task DoStuffAsync(string filePath)
    {
        string fileName = Path.GetFileName(filePath);
        using (var reader = new StreamReader(filePath))
        {
            string firstLineOfFile = 
                await reader.ReadLineAsync().ConfigureAwait(false);
            Console.WriteLine("[{0}] {1}: {2}", Thread.CurrentThread.ManagedThreadId, fileName, firstLineOfFile);
            Interlocked.Increment(ref filesRead);
        }
    }
}

注意,它不会显式地产生任何新线程,但这可能在await reader.ReadLineAsync().ConfigureAwait(false) 的幕后发生。

【讨论】:

    【解决方案2】:

    我将上面的 cmets 组合起来以达到我的解决方案。事实上,我根本不需要使用async 或await 关键字。我只需要创建一个任务列表,将它们全部启动,然后调用 WaitAll。不需要用async 或await 关键字修饰任何内容。这是生成的代码:

    public class MyClass
    {
        private int filesRead = 0;
    
        public void Go()
        {
            string[] fileSystemEntries = Directory.GetFileSystemEntries(@"Path\To\Files");
    
            Console.WriteLine("Starting to read from files! Count: {0}", fileSystemEntries.Length);
            List<Task> tasks = new List<Task>();
            foreach (var filePath in fileSystemEntries.OrderBy(s => s))
            {
                Task task = Task.Run(() => DoStuff(filePath));
                tasks.Add(task);
            }
            Task.WaitAll(tasks.ToArray());
            Console.WriteLine("Finish! Read {0} file(s).", filesRead);
        }
    
        private void DoStuff(string filePath)
        {
            string fileName = Path.GetFileName(filePath);
            string firstLineOfFile = File.ReadLines(filePath).First();
            Console.WriteLine("[{0}] {1}: {2}", Thread.CurrentThread.ManagedThreadId, fileName, firstLineOfFile);
            filesRead++;
        }
    }
    

    在测试时,我添加了Thread.Sleep 调用,以及忙循环来固定我机器上的 CPU。打开任务管理器,我观察到所有内核都在繁忙的循环中被挂起,每次我运行程序时,文件都以不一致的顺序运行(一件好事,因为这表明唯一的瓶颈是可用的数量线程)。

    每次我运行程序时,fileSystemEntries.Length 总是匹配 filesRead。

    编辑:根据上面的评论讨论,我发现一个更清洁(并且,基于 cmets 中的链接问题,更有效)的解决方案是使用Parallel.ForEach:

    public class MyClass
    {
        private int filesRead;
    
        public void Go()
        {
            string[] fileSystemEntries = Directory.GetFileSystemEntries(@"Path\To\Files");
    
            Console.WriteLine("Starting to read from files! Count: {0}", fileSystemEntries.Length);
            Parallel.ForEach(fileSystemEntries, DoStuff);
            Console.WriteLine("Finish! Read {0} file(s).", filesRead);
        }
    
        private void DoStuff(string filePath)
        {
            string fileName = Path.GetFileName(filePath);
            string firstLineOfFile = File.ReadLines(filePath).First();
            Console.WriteLine("[{0}] {1}: {2}", Thread.CurrentThread.ManagedThreadId, fileName, firstLineOfFile);
            filesRead++;
        }
    }
    

    现在似乎有很多方法可以在 C# 中进行异步编程。在Parallel 和Task 和async/await 之间,有很多选择。基于这个线程,看起来对我来说最好的解决方案是Parallel,因为它提供了最干净的解决方案,比自己手动创建Task 对象更有效,并且不会用async 和@987654338 混淆代码@关键字同时获得相似的结果。

    【讨论】:

    • 提示:使用Task.Run 而不是new Task + Task.Start。
    • 谢谢!更新了答案以反映这一点。
    • Task.Run 在这里是错误的方法。使用 Task.Run 进行长时间运行的 CPU 绑定操作。使用 async/await 进行长时间运行的 I/O 绑定操作。 Noseratio 的回答是正确的做法。
    • 我实际上认为Parallel.ForEach 现在最符合我的目的。不过,您是对的:我认为,我不应该使用任务。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-10-12
    • 1970-01-01
    • 2019-01-08
    • 2010-10-20
    • 1970-01-01
    • 1970-01-01
    • 2013-07-11
    相关资源
    最近更新 更多