在c#中使用async/await和httpclient的多线程



我写了一个控制台应用程序来下载YouTube预览图像。但我认为这个程序是同步运行的,而不是异步运行的。我做错了什么?我如何使用async/await从web进行多加载文件?

using System;
using System.IO;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace YoutubePreviewer
{
class Node
{
public string Path { get; private set; }
public string Title { get; private set; }
public string Source { get; private set; }
public string Id { get; private set; }
public Previews Previews { get; set; }
public Node(string p, string t, string s, string i)
{
Path = p;
Title = t;
Source = s;
Id = i;
}
}
class Previews
{
public string[] Urls { get; private set; }

public static Previews Get(Node n)
{
string[] resolutions = {"default", "hqdefault", "mqdefault", "maxresdefault"};
for (int i = 0; i < resolutions.Length; i++)
{
string end = resolutions[i] + ".jpg";
resolutions[i] = "https://img.youtube.com/vi/" + n.Id + "/" + resolutions[i] + ".jpg";
}
Previews pr = new Previews();
pr.Urls = resolutions;
return pr;
}
}
static class Operations
{
public static async Task<string> DownloadUrl(string address)
{
HttpClient http = new HttpClient();
return await http.GetStringAsync(address);
}
public static async Task<Node> Build(string url)
{
var source = await Operations.DownloadUrl(url);
var title = Regex.Match(source, "<title>(.*)</title>").Groups[1].Value;
var id = Regex.Match(url, @"watch?v=(.+)").Groups[1].Value;
Node node = new Node(url, title, source, id);
node.Previews =await Task<Previews>.Factory.StartNew(()=>Previews.Get(node);
return node;
}
public static async Task WriteToDisk(Node n, string path = "C:/Downloads")
{
Console.WriteLine($"Starting downloading {n.Path} previews");
var securedName = string.Join("_", n.Title.Split(Path.GetInvalidFileNameChars()));
Directory.CreateDirectory(Path.Combine(path, securedName));
HttpClient http = new HttpClient();
foreach (var preview in n.Previews.Urls)
{
try
{
var arr = await http.GetByteArrayAsync(preview);
await Task.Delay(100);
string name = preview.Substring(preview.LastIndexOf("/") + 1);
using (FileStream fs = new FileStream(Path.Combine(path, securedName, name), FileMode.Create,
FileAccess.ReadWrite))
{
await fs.WriteAsync(arr, 0, arr.Length);
}
}
catch (Exception e)
{
Console.WriteLine($"Can't download and save preview  {preview}");
Console.WriteLine(e.Message);
Console.WriteLine(new string('*', 12));
}
Console.WriteLine($"{preview} is saved!");
}
}
public static async Task Load(params string[] urls)
{
foreach (var url in urls)
{
Node n = await Build(url);
await WriteToDisk(n);
}
}
}
class Program
{
static  void Main(string[] args)
{
Task t= Operations.Load(File.ReadAllLines("data.txt"));
Task.WaitAll(t);
Console.WriteLine("Done");
Console.ReadKey();

}

}
}

您的代码正在下载URL并一次一个地将其写入磁盘。它是异步操作的,但却是串行操作的。

如果您希望它异步运行并且并发,那么您应该使用类似Task.WhenAll:的东西

public static async Task LoadAsync(params string[] urls)
{
var tasks = urls.Select(url => WriteToDisk(Build(url)));
await Task.WhenAll(tasks);
}

(此代码假定Build是一个同步方法,它应该是这样的)。

还有一些不相关的问题会跳出来:

  • node.Previews =await Task<Previews>.Factory.StartNew(()=>Previews.Get(node);正在毫无实际原因地向线程池发送琐碎的工作。它应该是node.Previews = Previews.Get(node);
  • 这意味着Operations.Build不需要是async,事实上也不应该是
  • 您应该使用HttpClient的单个共享实例,而不是为每个请求创建一个新实例
  • Task.WaitAll(t);很奇怪。它可以只是t.Wait();
  • CCD_ 10也不常见

为了补充@Stephen Cleary的出色回答,正如他所说,这在技术上是异步运行的,但这实际上对你没有任何帮助,因为它是串行运行的,也就是说,它是异步的,但性能并不比它实际上只是同步运行好。

这里需要记住的关键是,只有当async/await确实允许机器在一定时间内完成比其他情况下更多的工作时(或者如果它允许机器更快地完成一组任务),它才会对您有所帮助。

用我最喜欢的比喻:假设你和另外9个人在一家餐厅。当服务员过来点菜时,他招呼的第一个人还没有准备好。显然,最有效的做法是听从其他9个人的命令,然后回到他身边。然而,假设第一个人说:"只要你等我先准备好点餐,稍后再回来找我也没关系。"(这基本上就是你上面所说的——"只要你先等我完成下载,稍后再回到我的方法处理下载也没关系")。这种类比无论如何都不完美,但我认为这抓住了这里需要发生的事情的本质。

需要记住的关键是,只有服务员能够在相同的时间内完成更多的任务,或者能够更快地完成特定的任务,这里才会有改进。在这种情况下,只有减少他处理餐桌订单的总时间,他才能节省时间。

需要记住的另一件事是:在控制台应用程序中执行类似Task.WaitAll(...)的操作是可以接受的(只要您没有使用同步上下文),但您需要确保在WPF应用程序或其他具有同步上下文的应用程序中不会执行类似操作,因为这可能会导致死锁。

控制并发非常重要,这样可以有效地利用网络通道,而不会受到限制。因此,我建议使用AsyncEnumerator NuGet包,其中包含以下代码:

using System.Collections.Async;
static class Operations
{
public static async Task Load(params string[] urls)
{
await urls.ParallelForEachAsync(
async url =>
{
Node n = await Build(url);
await WriteToDisk(n);
},
maxDegreeOfParallelism: 10);
}
}

最新更新