首页 > 解决方案 > 从 API 下载 JSON 文件并将其保存在文件中

问题描述

我尝试编写一个应用程序,它需要一堆 URL 并将它们的内容异步保存在单独的文件中。我将该代码编写为同步的,它工作得很好,所以我试图让它异步。问题是我遇到了一些异常:该进程无法访问该文件,因为它正在被另一个进程使用。我对流不太了解,但是否有可能 2 个线程共享同一个流并暂时关闭“他们的”文件但不完全关闭,这就是我遇到该错误的原因?如果不是,那会是什么?

public override async Task ExecuteCommandAsync(IEnumerable<string> urls)
{
    string directory = "some directory";
    int i = 0;
    foreach (var url in urls)
    {
        tasks.Add(Task.Run(async () =>
        {
            try
            {
                await DownloadJsonFromUrl(url, directory, i);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }));
        i++;
        Console.WriteLine($"task nr {i} started.");
    }
    await Task.WhenAll(tasks);
private async Task DownloadJsonFromUrl(string url, string directory, int fileNumber)
{
    using (var httpClient = _clientFactory.CreateClient())
    using (var response = await httpClient.GetAsync(url,
        HttpCompletionOption.ResponseHeadersRead))
    using (FileStream fileStream = File.Open(directory + fileNumber.ToString() + ".json",
        FileMode.Create, FileAccess.Write, FileShare.None))
    using (var clientStream = await response.Content.ReadAsStreamAsync())
    {
        await clientStream.CopyToAsync(fileStream);
    }
}

标签: c#asynchronoustaskfilestream

解决方案


您的代码存在很多问题。但是,直接的问题是您ilambda中使用,这会创建一个闭包。闭包关闭变量,而不是值。因此,它试图同时一次又一次地写入同一个文件。

以您的原始代码为例,稍作修改。

foreach (var url in urls)
{
   tasks.Add(Task.Run(async () => 
   {
      try
      {
         Console.WriteLine($"task nr {i} started.");
         await Task.Delay(100);
      }
      catch(Exception ex)
      {
         Console.WriteLine(ex.Message);
      }
   }));
   i++;
}

输出将是

task nr 10 started.
task nr 10 started.
task nr 10 started.
task nr 10 started.
task nr 10 started.
task nr 10 started.
task nr 10 started.
task nr 10 started.
task nr 10 started.
task nr 10 started.

最简单的解决方案是创建一个副本:

foreach (var url in urls)
{
   var newI = i;

   ...

   tasks.Add(Task.Run(async () =>
     
       ...

       await DownloadJsonFromUrl(url, directory, newI);

       ...

但是,让我们更进一步,清理您的任务和 lamdas 并确保您拥有唯一的文件名。为简单起见,我们只使用Select具有index的重载,然后投影可以等待的任务:

给定

private async Task DownloadJsonFromUrl(string url, string path)
{
   using var httpClient = _clientFactory.CreateClient();

   using var response = await httpClient
      .GetAsync(url, HttpCompletionOption.ResponseHeadersRead)
      .ConfigureAwait(false);

   response.EnsureSuccessStatusCode();

   await using var fileStream = new FileStream(
      path,
      FileMode.Create, 
      FileAccess.Write,
      FileShare.None,
      1024*80,
      FileOptions.Asynchronous);

   await using var clientStream = await response.Content
      .ReadAsStreamAsync()
      .ConfigureAwait(false);

   await clientStream
      .CopyToAsync(fileStream)
      .ConfigureAwait(false);
}

用法

public async Task ExecuteCommandAsync(IEnumerable<string> urls)
{
   var directory = "some directory";

   async Task DownloadAsync(string url, int i)
   {
      try
      {
         Console.WriteLine($"task nr {i} started.");
         await DownloadJsonFromUrl(url, Path.Combine(directory, $"{i}.json"));
      }
      catch (Exception ex)
      {
         Console.WriteLine(ex.Message);
      }
   }

   var tasks = urls.Select(DownloadAsync);

   await Task.WhenAll(tasks);
}

推荐阅读