使用C#并行下载多个文件

8

我希望使用C#实现文件的并行下载。为此,我编写了以下代码,虽然能正常工作,但是界面却会出现卡顿问题。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;

namespace FileDownloader
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private static int count = 1;
        private static string f= "lecture";
        private string URL = "www.someexample.com";

        public MainWindow()
        {
            InitializeComponent();
        }

        public static string GetDirectoryListingRegexForUrl(string url)
        {
            if (url.Equals(URL))
            {
                return "<a href=\".*\">(?<name>.*)</a>";
            }
            throw new NotSupportedException();
        }

        public  void DownloadP(string[] urls)
        {
            Parallel.ForEach(urls.ToList(), new ParallelOptions { MaxDegreeOfParallelism = 10 }, DownloadFile);
        }

        private void DownloadFile(string url)
        {
           using(WebClient client=new WebClient())
           {
               if (url.EndsWith(".pdf"))
               {
                   int nextIndex = Interlocked.Increment(ref count);

                   client.DownloadFile(url, f + nextIndex + ".pdf");
                   this.Dispatcher.Invoke(() => {
                       listbox.Items.Add(url);
                   });
               }
           }
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            DownloadP(listofFiles);
        }
    }
}

1
你应该使用异步而不是并行。你没有任何计算密集型的工作。 - SLaks
你希望获得什么?这不会受到 CPU 的限制。 - David Heffernan
@SLaks和David Heffernan:我希望能够在不冻结用户界面的情况下并行下载文件。 - santosh singh
@HamletHakobyan: 只想把这些文件保存到本地文件系统上。 - santosh singh
你使用哪个版本的.NET? - Hamlet Hakobyan
显示剩余4条评论
4个回答

6

你可以使用 async/await 与新的 WebClient 方法 DownloadFileTaskAsync 结合使用。

private async Task DownloadFile(string url)
{
    if (!url.EndsWith(".pdf"))
    {
        return;
    }

    using (var client = new WebClient())
    {
        int nextIndex = Interlocked.Increment(ref count);

        await client.DownloadFileTaskAsync(url, "lecture" + nextIndex + ".pdf");
        listBox.Items.Add(url);

    }
}

private async void Button_OnClick(object sender, RoutedEventArgs e)
{
    button.IsEnabled = false;
    await DownloadFiles(urlList);
    button.IsEnabled = true;
}

private async Task DownloadFiles(IEnumerable<string> urlList)
{
    foreach (var url in urlList)
    {
        await DownloadFile(url);
    }
}

你为什么在这里使用 Interlocked.Increment?除了 DownloadFileTaskAsync 内部之外,代码中的所有内容都在同一个 UI 线程上运行。 - noseratio - open to work
@HamletHakobyan,啊,好的 :) - noseratio - open to work
6
您的代码不会冻结UI线程,但是由于在循环中使用了await,它也不会并行运行。这有点误导,并且与原帖标题不一致。 - tsul

3

请使用以下代码替换您的DownloadP函数:

public async Task DownloadP(string[] urls)
{
  await Task.Factory.StartNew(() => Parallel.ForEach(urls.ToList(), new ParallelOptions { MaxDegreeOfParallelism = 10 }, DownloadFile));
}

6
异步无返回值函数 - Vein

2

不要使用client.DownloadFile,而是像这样使用client.DownloadFileAsync

var webClient=new WebClient();
webClient.DownloadFileCompleted += webClient_DownloadFileCompleted;
webClient.DownloadFileAsync("Your url","file_name");

事件
    private void webClient_DownloadFileCompleted(object sender, AsyncCompletedEventArgs e)
    {
        MessageBox.Show("Download Completed.");
    }

+1个好建议 - 请确保使用原始帖子中的通知代码更新您的答案 - MessageBox.Show不是设计用于并行执行多个操作的最佳选择。 - Alexei Levenkov
我将如何在Parallel.ForEach循环中处理这个问题。 - santosh singh

2
我知道这个问题有点老,但WebClient已经过时且不再使用。首先,WPF使用MVVM模式,你应该尽量遵循,但除此之外,你可以使用using System.Net.Http;中的HttpClient。
为了并行下载多个文件,你可以创建一个并行foreach来处理HttpClient需要执行的所有下载。你做得很对,但foreach会阻塞线程,所以在新线程中启动它Task.Run(()=>{// Your Code}); 如果你不想自己编写类似于上述代码的内容,你可以使用NuGet上的Shard Download Library。这个NuGet包也在GitHub上,如果你想看看它是如何工作的。它经常帮助我下载大量的文件,因为它不会阻塞UI线程,而且易于使用。要使用它,你需要写出类似于以下的代码:
void Download()
{
    string[] links = new string[]{ 
        "https://google.com",
        "https://speed.hetzner.de/100MB.bin",
        "https://file-examples.com/storage/fe88dacf086398d1c98749c/2017/04/file_example_MP4_1920_18MG.mp4" };
    foreach (var link in links)
    {
        _ = new LoadRequest(link);
    }
    Console.ReadLine();
}

但是您也可以设置MaxDegreeOfParalism,而且您可以在下载时更改它。您可以设置输出文件的路径并命名它。这也解决了HttpClient在下载完成和进度报告方面的问题。不好的一点是这个库的文档不是很好。以下是几个选项的示例。

async Task DownloadAsync()
{
    string[] links = new string[]{
        "https://google.com",
        "https://speed.hetzner.de/100MB.bin",
        "https://file-examples.com/storage/fe88dacf086398d1c98749c/2017/04/file_example_MP4_1920_18MG.mp4" };
    RequestHandler requestHandler = new()
    {
        MaxDegreeOfParallelism = 10
    };

    LoadRequestOptions option = new()
    {
        Progress = new Progress<float>(value => Console.WriteLine(value.ToString("0.0%"))),
        DestinationPath = "C:\\Users\\[UserName]\\Desktop\\",
        RequestCompleated = path => Console.WriteLine("Finished: " + path?.ToString()),
        RequestHandler = requestHandler
    };
    LoadRequest last = null;
    foreach (string link in links)
    {
        last = new LoadRequest(link, option);
    }
    await last.Task;
} 

我希望能帮助那些有同样问题但不想使用专用WebClient的人。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接