使用'收益率回报'在异步方法中实现流水线操作

时间:2018-04-07 10:52:15

标签: c# uwp async-await

我正在构建一个UWP应用程序,它从文件夹中获取文件列表,对它们进行一些处理,然后删除文件。

这很好用:

List<StorageFile> files;
public MainPage()
{
     this.InitializeComponent();
     files = new List<StorageFile>();
}

private async Task<List<StorageFile>> GetFiles(StorageFolder folder)
{
     var items = await folder.GetItemsAsync();          
     foreach (var item in items)
     {
         if (item.GetType() == typeof(StorageFile))
             files.Add(item);
         else
             await GetFiles(item as StorageFolder);
     }

     return files;
}


private async void GetFilesBtn_Click(object sender, RoutedEventArgs e)
{
     // opening folder picker, then selecting a folder
     var files = await GetFiles(folder);    
     // process files
     ProcessFiles(files);    
     // dispose
     DisposeFiles(files);
}

然而,当处理大量文件时,内存消耗非常高(显然)。

所以我想到的是使用yield return file并处理每个文件,然后一旦我完成了该文件,我就可以处理它,并开始处理下一个文件,所以上。

我试图做的是:

public async Task<IEnumerable<StorageFile>> GetFiles(StorageFolder folder)
{
       var items = await folder.GetItemsAsync();    
       foreach (var item in items)
       {
            if (item.GetType() == typeof(StorageFile))
                yield return item;
            else
               await GetFiles(item as StorageFolder);
       }
}

然后:

foreach (var file in GetFiles(folder))
{
      // process file
      ProcessFile(file);
      // dispose
      DisposeFile(file);
} 

这样做的时候我得到了:

  

GetFiles(StorageFolder)的主体&#39;不能成为迭代器块,因为任务IEnumerable StorageFile不是迭代器接口   类型。

我之前从未使用过yield return,所以我不确定如何做到这一点。

6 个答案:

答案 0 :(得分:2)

这是去Reactive的绝佳机会!

我创建了这个简单的程序,您可以轻松编辑该程序以使用StorageFolder和StorageFile而不是字符串作为路径:

class Program
{

    static void Main(string[] args)
    {
        Task.Run(async () =>
        {
            GetFilesFromDirectory(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)).Subscribe(
                file =>
                {
                    Console.WriteLine(file);
                });


            var files = await GetFilesFromDirectory(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData))
                .ToArray(); // you can also do this

            foreach (var file in files)
            {
                Console.WriteLine(file);
            }

            Console.ReadLine();
        }).Wait();
    }


    static IObservable<string> GetFilesFromDirectory(string path)
    {
         var files = new Subject<string>();
        var directories = new Subject<string>();

        directories.Select(x => new DirectoryInfo(x)).Subscribe(dir =>
        {
            foreach (var fileInfo in dir.GetFiles())
            {
                files.OnNext(fileInfo.FullName);
            }

            foreach (var directoryInfo in dir.GetDirectories())
            {
                directories.OnNext(directoryInfo.FullName);
            }
        }, () =>
        {
            files.OnCompleted();
        });

        Task.Run(() =>
        {
            directories.OnNext(path);
            directories.OnCompleted();
        });

        return files;
    }

Directory.GetFiles还有一个重载,它会递归搜索你:

var fileList = new DirectoryInfo(sDir).GetFiles("*", SearchOption.AllDirectories);

答案 1 :(得分:1)

你肯定知道如何让自己的生活变得困难 - 异步,屈服和递归! 不幸的是,async / await和yield目前在dotnet中不兼容。

我建议采用不同的方法,而不是让你的递归函数建立一个列表,传递一个动作,以便它适用于每个文件,如:

aaab

您可能希望使ProcessFile和DisposeFile异步,在这种情况下:

public async Task<IEnumerable<StorageFile>> ProcessFiles(StorageFolder folder, Action<StorageFile> process)
{
    var items = await folder.GetItemsAsync();          
    foreach (var item in items)
    {
        if (item.GetType() == typeof(StorageFile))
            process(item);
        else
           await ProcessFiles(item as StorageFolder);
    }
}

ProcessFiles(folder, file => {
    ProcessFile(file);
    DisposeFile(file);
});

如果您想单独定义动作,可以这样做:

ProcessFiles(folder, async file => {
    await ProcessFile(file);
    await DisposeFile(file);
});

答案 2 :(得分:1)

async关键字告诉C#编译器将该方法重写为异步工作的状态机(又名Task)。

yield return关键字告诉C#编译器将方法重写为一个懒惰地生成结果的状态机(又名Enumerator)。

你要做的是将两种方法结合起来,这会使C#编译器非常难过,因为它目前无法决定如何从一种方法生成两种状态机。在dotnet / csharplang的C#中有一个未解决的问题:Champion "Async Streams" (including async disposable)

您可以使用不同的方法Task.WhenAll,在此问题中进行了描述:Is it possible to "await yield return DoSomethingAsync()"

请注意,Task.WhenAll会将所有中间结果解析到内存中,因此如果您不小心,最终可能会遇到比原始解决方案更复杂且耗费更多内存的解决方案。

接下来,您的代码中也存在错误:

foreach (var item in items)
{
    if (item.GetType() == typeof(StorageFile))
        yield return item;
    else
        await GetFiles(item as StorageFolder); // <---- no return here
}

else分支中,您不会返回检索到的值。因此,即使编译了该代码,您很快就会发现它无法正常工作。

但是,如果您添加了return,则您的方法既需要为StorageFile生成状态机,也要在StorageFolder的情况下返回整个序列。这是不可能的,您需要采用一种称为展平的不同方法,只需添加另一个foreach(请注意,为了简单起见,异步性已被删除):

foreach (var item in items)
{
    if (item.GetType() == typeof(StorageFile))
    {
        yield return item;
    }
    else
    {
       foreach (var file in GetFiles(item as StorageFolder))
       {
           yield return file;
       }
    }
}

答案 3 :(得分:1)

从 C# 8 开始,现在可以使用 IAsyncEnumerable 来完成。

您只需将返回类型从 Task<IEnumerable<StorageFile>> 更改为 IAsyncEnumerable<StorageFile>,然后使用 await foreach 而不是 foreach 调用该方法。

所以你的例子现在看起来像这样:

public async IAsyncEnumerable<StorageFile> GetFiles(StorageFolder folder)
{
       var items = await folder.GetItemsAsync();    
       foreach (var item in items)
       {
            if (item.GetType() == typeof(StorageFile))
                yield return item;
            else
               await foreach (var item2 in GetFiles(item as StorageFolder))
                  yield return item2;
       }
}

那么:

await foreach (var file in GetFiles(folder))
{
      // process file
      ProcessFile(file);
      // dispose
      DisposeFile(file);
}

答案 4 :(得分:0)

我有一些类似的使用yield的例子。基本上将GetRandomStringWDelayAsync替换为您的GetFile代码。锁定不是必需的。

        object lockObj = new object();

        for (int i = 0; i < callCount; i++)
        {
            int j = i; // because of scope we can't use "i"  

            yield return Task.Run(async delegate {
                var pair = await StringService.RandomValues.GetRandomStringWDelayAsync(j);

                if (pair.Value != null)
                {
                    lock (lockObj)
                        dictionary[j] = pair.Value;
                }
            });
        }

完整代码在这里:https://github.com/sergeklokov/AsynchronousTasksDemo

答案 5 :(得分:-1)

由于yield支持其返回类型为IEnumerable<T>GetFiles()返回Task<IEnumerable<T>>的方法, 看看这个:

    public IEnumerable<StorageFile> GetFiles(StorageFolder folder)
    {
        // your code here
    }

    public Task<IEnumerable<StorageFile>> GetFilesAsync(StorageFolder folder)
    {
        return Task.Run(() => GetFiles(folder));
    }

然后你可以执行foreach (var file in GetFiles(folder))foreach (var file in await GetFilesAsync(folder))

编辑: 啊,你在谈论IAsyncEnumerable,这在你发布答案的日期不存在