进程文件列表在C#控制台应用程序中异步使用async和await

时间:2014-05-03 13:09:36

标签: c# multithreading asynchronous async-await

我在一个简单的小型控制台应用程序中使用C#中的asyncawait。我的目标很简单:以异步方式处理文件列表,以便处理一个文件不会阻止其他文件的处理。这些文件都不依赖于彼此,而且(让我们说)数千个文件可以通过。

这是我目前的代码。

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();
    }
}

这个异步编程模式中有一些东西我似乎缺少了,但是,每当我运行程序时,似乎随机有多少文件被实际处理,从任何一个到任何一个都没有。他们(在我的示例文件集中)。

基本上,主线程并没有等待处理所有文件,我认为这是异步运行的一部分,但我并不是很想要。我想要的只是:在尽可能多的线程中处理尽可能多的这些文件,但仍然等待它们全部完成处理才能完成。

2 个答案:

答案 0 :(得分:6)

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)的场景后面发生。

答案 1 :(得分:3)

我结合上面的评论以达到我的解决方案。实际上,我根本不需要使用asyncawait个关键字。我只需创建一个任务列表,启动所有任务,然后调用WaitAll。无需使用asyncawait关键字进行修饰。以下是生成的代码:

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匹配。

编辑:基于上面的评论讨论,我找到了一个更清洁的(并且,基于评论中的链接问题,更有效)解决方案是使用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#中进行异步编程。在ParallelTask以及async / await之间,有很多选择。基于这个线程,看起来对我来说最好的解决方案是Parallel,因为它提供了最干净的解决方案,比自己手动创建Task个对象更有效,并且不会使代码与{async混乱。 1}}和await关键字,同时实现类似的结果。