同时下载尽可能多的文件

时间:2014-04-10 18:14:20

标签: c# asynchronous parallel-processing

我想从随机生成的URI中下载图像,我找到的最快的方法是不可阻挡的。

我正在使用预生成的List<string> URI达到约400个/分钟(比使用标准线程时多8倍),但我希望它能够连续生成URI并下载新图像,直到我说它暂停。如何实现?

private void StartButton_Click(object sender, EventArgs e)
{
    List<string> ImageURI;
    GenerateURIs(out ImageURI); // creates list of 1000 uris
    ImageNames.AsParallel().WithDegreeOfParallelism(50).Sum(s => DownloadFile(s));
}

private int DownloadFile(string URI)
{
    try
    {
        HttpWebRequest webrequest = (HttpWebRequest)WebRequest.Create(URI);
        webrequest.Timeout = 10000;
        webrequest.ReadWriteTimeout = 10000;
        webrequest.Proxy = null;
        webrequest.KeepAlive = false;
        HttpWebResponse webresponse = (HttpWebResponse)webrequest.GetResponse();

        using (Stream sr = webrequest.GetResponse().GetResponseStream())
        {
            DownloadedImages++;
            using (MemoryStream ms = new MemoryStream())
            {
                sr.CopyTo(ms);
                byte[] ImageBytes = ms.ToArray();
                if (ImageBytes.Length == 503)
                {
                    InvalidImages++;
                    return 0;
                }
                else
                {
                    ValidImages++;
                    using (var Writer = new BinaryWriter(new FileStream("images/" + (++FilesIndex).ToString() + ".png", FileMode.Append, FileAccess.Write)))
                    {
                        Writer.Write(ImageBytes);
                    }
                }
            }
        }          
    }
    catch (Exception e)
    {
        return 0;
    }            

    return 0;
}

2 个答案:

答案 0 :(得分:1)

您正在寻找的是生产者/消费者模型,在该模型中,您有一个生产者将项目添加到队列中,而消费者将项目从中拉出。 BlockingCollection让这很容易。创建BlockingCollection,让生产者在生成项目时继续向其添加项目,完成后调用CompleteAdding,让消费者使用GetConsumingEnumerable,您可以将其称为确切的可枚举的代码。

您需要将生产者代码和消费代码都移动到非UI线程中,这样它们都不会阻止UI并且可以并行生成/使用数据。

另请注意,目前在DownloadFile方法中,您正在变异并访问实例数据,尽管可能会同时从不同的线程调用此方法。执行诸如递增索引之类的操作并不安全,因为它不是原子操作,这会导致代码产生可能的副作用。您需要避免在这些不同的线程之间使用共享状态,或者正确地同步对该共享状态的访问。

答案 1 :(得分:1)

首先,您当前的代码不是线程安全的。 InvalidImagesDownloadedImagesValidImages都需要同步。

话虽这么说,你可以使用异步而不是线程更有效地做到这一点。因为几乎所有的&#34;工作&#34;在这种情况下是IO绑定,async可能是一个更好,更可扩展的方法。

请改为尝试:

private async void StartButton_Click(object sender, EventArgs e)
{
    List<string> ImageURI;
    GenerateURIs(out ImageURI); // creates list of 1000 uris

    var requests = ImageURI
        .Select(uri => (new WebClient()).DownloadDataTaskAsync(uri))
        .Select(SaveImageFile);

    await Task.WhenAll(requests);
}

private Task SaveImageFile(Task<byte[]> data)
{
    try
    {
        byte[] ImageBytes = await data;
        DownloadedImages++;

        if (ImageBytes.Length == 503)
        {
            InvalidImages++;
            return;
        }

        ValidImages++;
        using (var file = new FileStream("images/" + (++FilesIndex).ToString() + ".png", FileMode.Append, FileAccess.Write))
        {
            await Writer.WriteAsync(ImageBytes, 0, ImageBytes.Length);
        }
    }
    catch (Exception e)
    {
    }            

    return;
}

请注意,使用async / await,您不再需要担心同步,因为这些值仍将在主UI线程上设置。

至于暂停,有各种选项 - 您可以添加是否继续执行数据的标志,或使用CancellationTokenSource在整个操作中提供取消支持。