Метод WebClient DownloadData замораживает форму

Я использую объект DownloadData from WebClient для загрузки фавиконов с нескольких веб-сайтов.

Я получаю ответ в виде массива байтов, и все работает хорошо, за исключением одного: когда метод DownloadData будет выполнен, он заморозит мою форму до тех пор, пока метод не вернется.

Теперь я решил эту проблему, используя объект BackgroundWorker, чтобы выполнить работу, но мне любопытно, как добиться того же, используя System.Threading.Thread.

Я попытался создать еще один поток, который загружает значки, а затем зациклил мой mainThread до тех пор, пока поток не завершит обработку, а затем использовал метод Abort() для прерывания потока, но пока моя форма зависает во время выполнения другого потока.

Это код, который я использовал для создания другого потока:

    bool downloadFavIcon_Completed = false;
    private void downloadFavIcon()
    {
        downloadFavIcon_Completed = false;
        Byte[] dl;
        System.IO.MemoryStream dlMem;
        Bitmap favCollection = new Bitmap(96, 64);
        Graphics g = Graphics.FromImage(favCollection);
        Bitmap dlImg;
        String[] addr = new String[24];
        addr[0] = @"http://google.com/favicon.ico";
        addr[1] = @"http://microsoft.com/favicon.ico";
        addr[2] = @"http://freesfx.com/favicon.ico";
        addr[3] = @"http://yahoo.com/favicon.ico";
        addr[4] = @"http://downloadha.com/favicon.ico";
        addr[5] = @"http://hp.com/favicon.ico";
        addr[6] = @"http://bing.com/favicon.ico";
        addr[7] = @"http://webassign.com/favicon.ico";
        addr[8] = @"http://youtube.com/favicon.ico";
        addr[9] = @"https://twitter.com/favicon.ico";
        addr[10] = @"http://cc.com/favicon.ico";
        addr[11] = @"http://stackoverflow.com/favicon.ico";
        addr[12] = @"http://vb6.us/favicon.ico";
        addr[13] = @"http://facebook.com/favicon.ico";
        addr[14] = @"http://flickr.com/favicon.ico";
        addr[15] = @"http://linkedin.com/favicon.ico";
        addr[16] = @"http://blogger.com/favicon.ico";
        addr[17] = @"http://blogfa.com/favicon.ico";
        addr[18] = @"http://metal-archives.com/favicon.ico";
        addr[19] = @"http://wordpress.com/favicon.ico";
        addr[20] = @"http://metallica.com/favicon.ico";
        addr[21] = @"http://wikipedia.org/favicon.ico";
        addr[22] = @"http://visualstudio.com/favicon.ico";
        addr[23] = @"http://evernote.com/favicon.ico";
        for (int i = 0; i < addr.Length; i++)
        {
            using (System.Net.WebClient client = new System.Net.WebClient())
            {
                try
                {
                    dl = client.DownloadData(addr[i]);
                    dlMem = new System.IO.MemoryStream(dl);
                    dlImg = new Bitmap(dlMem);
                }
                catch (Exception)
                {
                    dlImg = new Bitmap(Properties.Resources.defaultFavIcon);
                }
            }
            g.DrawImage(dlImg, (i % 6) * 16, (i / 6) * 16, 16, 16);
        }
        passAddDisplay.Image = favCollection;
        downloadFavIcon_Completed = true;
    }

    private void button2_Click(object sender, EventArgs e)
    {
        Thread downloader = new Thread(new ThreadStart(downloadFavIcon));
        downloader.Start();
        while (!downloader.IsAlive) ;
        while (!downloadFavIcon_Completed) ;
        downloader.Abort();
    }

ПРИМЕЧАНИЕ. passAddDisplay — это элемент pictureBox, уже размещенный в моей форме.

Как я могу улучшить свое приложение, чтобы избежать зависания во время выполнения WebClient.DownloadData? (Я не хочу использовать Application.DoEvents())


person Ramtin Soltani    schedule 08.04.2015    source источник
comment
должны быть ошибки времени выполнения, так как вы обращаетесь к элементам пользовательского интерфейса из фонового потока downloader, что не разрешено. Как вы справляетесь с этим? вы подавляете его, устанавливая this.CheckForIllegalCrossThreadCalls = false?   -  person kennyzx    schedule 08.04.2015
comment
Какие элементы пользовательского интерфейса? Вы говорите о passAddDisplay?   -  person Ramtin Soltani    schedule 08.04.2015
comment
да, я имею в виду Picturebox.   -  person kennyzx    schedule 08.04.2015
comment
Код находится в классе Form1, использующем то же пространство имен. При запуске этого кода нет ни ошибок компилятора, ни ошибок времени выполнения. Все работает как надо, кроме проблемы с зависанием, которая является моей проблемой.   -  person Ramtin Soltani    schedule 08.04.2015
comment
Пожалуйста, никогда не звоните .Abort() в темы — это опасно.   -  person Enigmativity    schedule 08.04.2015
comment
@Enigmativity Можете ли вы объяснить, почему это опасно?   -  person Ramtin Soltani    schedule 09.04.2015
comment
@RamtinSoltani - читайте здесь - stackoverflow.com/a/1560567/259769   -  person Enigmativity    schedule 09.04.2015
comment
@Enigmativity Понятно. Спасибо.   -  person Ramtin Soltani    schedule 09.04.2015


Ответы (3)


Добро пожаловать в Stack Overflow...

Изучив свой цикл, становится очевидным, что вы блокируете поток пользовательского интерфейса в своих циклах.

while (!downloader.IsAlive) ;
while (!downloadFavIcon_Completed) ;

Это причина, по которой ваш пользовательский интерфейс блокируется. В идеале это должна быть работа для фонового работника, поскольку он предназначен для работы в фоновом режиме и предоставляет события для обратной связи с вашим потоком пользовательского интерфейса. Это можно написать с помощью потока, однако следует использовать фонового рабочего.

Теперь, если вы действительно хотите написать это, используя объект Thread, я предлагаю вам создать класс для вашего загрузчика и создать события. Поэтому мы можем написать простые события, такие как

  1. Завершенное событие
  2. Отмененное событие
  3. Событие прогресса пользовательского интерфейса

Я НЕ РЕКОМЕНДУЮ ЭТОТ ПОДХОД (читайте далее)

Просто начните с создания нового класса Downloader, и вот пример (минимум)

public class Downloader
{

    /// <summary>
    /// Delegate Event Handler for the downloading progress
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    public delegate void DownloaderProgressEventHandler(Downloader sender, DownloaderProgressEventArgs e);

    /// <summary>
    /// Delegate Event Handler for the completed event
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    public delegate void DownloaderCompletedEventHandler(Downloader sender, DownloaderCompletedEventArgs e);

    /// <summary>
    /// The completed event
    /// </summary>
    public event DownloaderCompletedEventHandler Completed;

    /// <summary>
    /// The cancelled event
    /// </summary>
    public event EventHandler Cancelled;

    /// <summary>
    /// the progress event
    /// </summary>
    public event DownloaderProgressEventHandler Progress;

    /// <summary>
    /// the running thread
    /// </summary>
    Thread thread;

    /// <summary>
    /// the aborting flag
    /// </summary>
    bool aborting = false;

    //the addresses
    String[] addr = new String[] { 
        "http://google.com/favicon.ico", 
        "http://microsoft.com/favicon.ico", 
        "http://freesfx.com/favicon.ico", 
        "http://yahoo.com/favicon.ico", 
        "http://downloadha.com/favicon.ico",
        "http://hp.com/favicon.ico", 
        "http://bing.com/favicon.ico", 
        "http://webassign.com/favicon.ico", 
        "http://youtube.com/favicon.ico", 
        "https://twitter.com/favicon.ico", 
        "http://cc.com/favicon.ico", 
        "http://stackoverflow.com/favicon.ico", 
        "http://vb6.us/favicon.ico", 
        "http://facebook.com/favicon.ico", 
        "http://flickr.com/favicon.ico", 
        "http://linkedin.com/favicon.ico",
        "http://blogger.com/favicon.ico",
        "http://blogfa.com/favicon.ico",
        "http://metal-archives.com/favicon.ico",
        "http://wordpress.com/favicon.ico",
        "http://metallica.com/favicon.ico",
        "http://wikipedia.org/favicon.ico", 
        "http://visualstudio.com/favicon.ico",
        "http://evernote.com/favicon.ico" 
    };


    /// <summary>
    /// Starts the downloader
    /// </summary>
    public void Start()
    {
        if (this.aborting)
            return;
        if (this.thread != null)
            throw new Exception("Already downloading....");
        this.aborting = false;

        this.thread = new Thread(new ThreadStart(runDownloader));
        this.thread.Start();

    }

    /// <summary>
    /// Starts the downloader
    /// </summary>
    /// <param name="addresses"></param>
    public void Start(string[] addresses)
    {
        if (this.aborting)
            return;

        if (this.thread != null)
            throw new Exception("Already downloading....");

        this.addr = addresses;
        this.Start();
    }

    /// <summary>
    /// Aborts the downloader
    /// </summary>
    public void Abort()
    {
        if (this.aborting)
            return;
        this.aborting = true;
        this.thread.Join();
        this.thread = null;
        this.aborting = false;

        if (this.Cancelled != null)
            this.Cancelled(this, EventArgs.Empty);
    }

    /// <summary>
    /// runs the downloader
    /// </summary>
    void runDownloader()
    {
        Bitmap favCollection = new Bitmap(96, 64);
        Graphics g = Graphics.FromImage(favCollection);

        for (var i = 0; i < this.addr.Length; i++)
        {
            if (aborting)
                break;

            using (System.Net.WebClient client = new System.Net.WebClient())
            {
                try
                {
                    byte[] dl = client.DownloadData(addr[i]);
                    using (var stream = new MemoryStream(dl))
                    {
                        using (var dlImg = new Bitmap(stream))
                        {
                            g.DrawImage(dlImg, (i % 6) * 16, (i / 6) * 16, 16, 16);
                        }
                    }
                }
                catch (Exception)
                {
                    using (var dlImg = new Bitmap(Properties.Resources.defaultFacIcon))
                    {
                        g.DrawImage(dlImg, (i % 6) * 16, (i / 6) * 16, 16, 16);
                    }
                }
            }
            if (aborting)
                break;

            if (this.Progress != null)
                this.Progress(this, new DownloaderProgressEventArgs
                {
                    Completed = i + 1,
                    Total = this.addr.Length
                });
        }

        if (!aborting && this.Completed != null)
        {
            this.Completed(this, new DownloaderCompletedEventArgs
            {
                Bitmap = favCollection
            });
        }
        this.thread = null;
    }

    /// <summary>
    /// Downloader progress event args
    /// </summary>
    public class DownloaderProgressEventArgs : EventArgs
    {
        /// <summary>
        /// Gets or sets the completed images
        /// </summary>
        public int Completed { get; set; }

        /// <summary>
        /// Gets or sets the total images
        /// </summary>
        public int Total { get; set; }
    }

    /// <summary>
    /// Downloader completed event args
    /// </summary>
    public class DownloaderCompletedEventArgs : EventArgs
    {
        /// <summary>
        /// Gets or sets the bitmap
        /// </summary>
        public Bitmap Bitmap { get; set; }
    }

}

Теперь это выделение кода, но давайте быстро посмотрим на него. Для начала мы определили двух делегатов для наших событий Completed и Progress. Эти делегаты принимают экземпляр загрузчика в качестве отправителя и специальных классов аргументов событий, перечисленных внизу. Вслед за нашими 3 событиями (как указано выше) эти события будут использоваться для сигнализации об изменениях в загрузчике.

Далее мы определяем наши поля.

Thread thread; Это ссылка на поток, который будет создан при вызове методов Start().

bool aborting = false; Это простой флаг, сигнализирующий потоку, что мы должны прервать его выполнение. Теперь я решил использовать флаг и позволить потоку изящно завершиться, а не вызывать метод Thread.Abort(). Это гарантирует, что вся уборка может быть проведена должным образом.

string[] addres =.... Наши первоначальные адреса.

Вот пока все просто. Далее идут наши Start() методы. Я предоставил два разных метода. Один из методов принимает новые string[] адресов для загрузки (не так уж важно).

Вы заметите в этом методе

/// <summary>
/// Starts the downloader
/// </summary>
public void Start()
{
    if (this.aborting)
        return;
    if (this.thread != null)
        throw new Exception("Already downloading....");
    this.aborting = false;

    this.thread = new Thread(new ThreadStart(runDownloader));
    this.thread.Start();
}

Первое, что мы делаем, это проверяем, установлен ли флаг прерывания. Если это так, то игнорируйте вызов start (вы можете создать исключение). Затем мы проверяем, не является ли поток нулевым. Если поток не нулевой, то наш загрузчик работает. Наконец, мы просто сбрасываем наш прерывающий флаг на false и запускаем новый Thread.

Переходим к методу Abort(). Этот метод сначала проверит, установлен ли флаг aborting. Если так, то ничего не делать. Затем мы устанавливаем для нашего флага aborting значение true. Следующим шагом, и я предупреждаю вас, что БУДЕТ блокировать вызывающий поток, является вызов метода Thread.Join(), который присоединит поток к нашему вызывающему потоку. В основном ждет выхода потока.

Наконец, мы просто устанавливаем экземпляр потока в null, сбрасываем флаг aborting в false и запускаем событие Cancelled (если подписано).

Далее идет основной метод, который выполняет загрузку. Во-первых, вы заметите, что я переместил ваши переменные и использовал операторы using для одноразовых объектов. (это уже другая тема).

Большая часть метода runDownloader() заключается в том, что он периодически проверяет флаг прерывания. Если этот флаг когда-либо установлен на true, downloader останавливается на этом. Теперь обратите внимание, что у вас может возникнуть ситуация, когда прерывание вызывается, когда WebClient загружает изображение. В идеале вы должны позволить своему WebClient завершить запрос, правильно избавиться от него, а затем выйти из цикла.

После каждой загрузки изображения запускается событие progress (если на него оформлена подписка). Наконец, когда итерации завершены и все изображения загружены, запускается событие «Completed» с скомпилированным растровым изображением.

ПАУЗА ПЕРЕДЫШАТЬ

Теперь это все здорово .. НО как вы это используете. Ну просто я создал форму с кнопкой, индикатором выполнения и картинкой. Кнопка будет использоваться для запуска и остановки загрузчика, индикатор выполнения для обработки событий выполнения и окно изображения для готового изображения.

Вот пример программы, которую я прокомментировал.

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();

        this.progressBar1.Visible = false;
        this.progressBar1.Enabled = false;
    }

    Downloader downloader;

    /// <summary>
    /// starts \ stop button pressed
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void button1_Click(object sender, EventArgs e)
    {
        //if downloader is not null then abort it
        if (downloader != null)
        {
            downloader.Abort();
            return;
        }

        //setup and start the downloader
        this.progressBar1.Value = 0;
        this.progressBar1.Minimum = 0;
        this.progressBar1.Enabled = true;
        this.progressBar1.Visible = true;
        this.downloader = new Downloader();
        this.downloader.Progress += downloader_Progress;
        this.downloader.Completed += downloader_Completed;
        this.downloader.Cancelled += downloader_Cancelled;
        this.downloader.Start();
    }

    /// <summary>
    /// downloader cancelled event handler
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void downloader_Cancelled(object sender, EventArgs e)
    {
        this.unhookDownloader();

        if (this.InvokeRequired)
            this.Invoke((MethodInvoker)delegate
            {
                this.progressBar1.Enabled = false;
                this.progressBar1.Visible = false;
                MessageBox.Show(this, "Cancelled");
            });
        else
        {
            this.progressBar1.Enabled = false;
            this.progressBar1.Visible = false;
            MessageBox.Show(this, "Cancelled");
        }

    }

    /// <summary>
    /// downloader completed event handler
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void downloader_Completed(Downloader sender, Downloader.DownloaderCompletedEventArgs e)
    {
        this.unhookDownloader();
        if (this.InvokeRequired)
            this.Invoke((MethodInvoker)delegate
            {
                this.progressBar1.Enabled = false;
                this.progressBar1.Visible = false;
                this.pictureBox1.Image = e.Bitmap;
                MessageBox.Show(this, "Completed");
            });
        else
        {
            this.progressBar1.Enabled = false;
            this.progressBar1.Visible = false;
            this.pictureBox1.Image = e.Bitmap;
            MessageBox.Show(this, "Completed");
        }
    }

    /// <summary>
    /// downloader progress event handler
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void downloader_Progress(Downloader sender, Downloader.DownloaderProgressEventArgs e)
    {
        if (this.progressBar1.InvokeRequired)
            this.progressBar1.Invoke((MethodInvoker)delegate
            {
                this.progressBar1.Value = e.Completed;
                this.progressBar1.Maximum = e.Total;
            });
        else
        {
            this.progressBar1.Value = e.Completed;
            this.progressBar1.Maximum = e.Total;
        }

    }

    /// <summary>
    /// unhooks the events handlers and sets the downloader to null
    /// </summary>
    void unhookDownloader()
    {
        this.downloader.Progress -= downloader_Progress;
        this.downloader.Completed -= downloader_Completed;
        this.downloader.Cancelled -= downloader_Cancelled;
        this.downloader = null;
    }
}

Это простая реализация того, как вы можете использовать объект Thread для выполнения своей работы. На мой взгляд, это слишком много работы. Теперь давайте сделаем это в Background Worker.

Я НАСТОЯТЕЛЬНО РЕКОМЕНДУЮ ЭТОТ ПОДХОД

Почему вы могли бы сказать? Well with Background Worker предоставляет нам протестированные и поддерживаемые методы (и многое другое), которые мы пытались реализовать. Вы заметите, что фактическая работа по загрузке изображений и обнаружению отмены относительно одинакова. Однако вы заметите, что я не беспокоюсь (настолько) о проблемах с перекрестными потоками при публикации завершенных и текущих событий.

private void button2_Click(object sender, EventArgs e)
{
    if (this.worker != null && this.worker.IsBusy)
    {
        this.worker.CancelAsync();
        return;
    }

    string[] addr = new string[] {".... our full addresss lists" };

    this.progressBar1.Maximum = addr.Length;
    this.progressBar1.Value = 0;
    this.progressBar1.Visible = true;
    this.progressBar1.Enabled = true;
    this.worker = new BackgroundWorker();
    this.worker.WorkerSupportsCancellation = true;
    this.worker.WorkerReportsProgress = true;
    this.worker.ProgressChanged += (s, args) =>
    {
        this.progressBar1.Value = args.ProgressPercentage;
    };

    this.worker.RunWorkerCompleted += (s, args) =>
    {
        this.progressBar1.Visible = false;
        this.progressBar1.Enabled = false;

        if (args.Cancelled)
        {
            MessageBox.Show(this, "Cancelled");
            worker.Dispose();
            worker = null;
            return;
        }

        var img = args.Result as Bitmap;
        if (img == null)
        {
            worker.Dispose();
            worker = null;
            return;
        }
        this.pictureBox1.Image = img;

        MessageBox.Show(this, "Completed");
        worker.Dispose();
        worker = null;
    };

    this.worker.DoWork += (s, args) =>
    {
        Bitmap favCollection = new Bitmap(96, 64);
        Graphics g = Graphics.FromImage(favCollection);

        for (var i = 0; i < addr.Length; i++)
        {
            if (worker.CancellationPending)
                break;

            using (System.Net.WebClient client = new System.Net.WebClient())
            {
                try
                {
                    byte[] dl = client.DownloadData(addr[i]);
                    using (var stream = new MemoryStream(dl))
                    {
                        using (var dlImg = new Bitmap(stream))
                        {
                            g.DrawImage(dlImg, (i % 6) * 16, (i / 6) * 16, 16, 16);
                        }
                    }
                }
                catch (Exception)
                {
                    using (var dlImg = new Bitmap(Properties.Resources.defaultFacIcon))
                    {
                        g.DrawImage(dlImg, (i % 6) * 16, (i / 6) * 16, 16, 16);
                    }
                }
            }
            if (worker.CancellationPending)
                break;

            this.worker.ReportProgress(i);
        }

        if (worker.CancellationPending)
        {
            g.Dispose();
            favCollection.Dispose();
            args.Cancel = true;
            return;
        }
        args.Cancel = false;
        args.Result = favCollection;
    };
    worker.RunWorkerAsync();
}

Я надеюсь, что это поможет понять несколько возможных способов реализации того, чего вы хотели бы достичь.

-Нико

person Nico    schedule 08.04.2015
comment
спасибо, что нашли время и подробно ответили на мой вопрос, я ценю это. Это действительно помогло мне с моим замешательством. Как я уже упоминал, я уже решил эту проблему с помощью BackgroundWorker, но я должен сказать, что ваш вариант намного полнее моего. Я знаю, как это сделать прямо сейчас! Еще раз спасибо. :) - person Ramtin Soltani; 08.04.2015

Да, это синхронный метод, который заставляет поток ждать без обработки сообщений до тех пор, пока он не вернется; вы должны попробовать его асинхронную версию, которая этого не делает.

person Gavin Fang    schedule 08.04.2015
comment
Спасибо за ответ @nathan - person Ramtin Soltani; 08.04.2015

Я бы посмотрел на использование Microsoft Reactive Framework для этого. Он автоматически обрабатывает фоновые потоки и очень эффективно очищает все одноразовые ссылки. NuGet "Rx-Main" и "Rx-WinForms"/"Rx-WPF".

Во-первых, начните с вашего массива адресов:

var addr = new []
{
    "http://google.com/favicon.ico",
    // DELETED FOR BREVITY
    "http://evernote.com/favicon.ico",
};

Теперь определите запрос для асинхронного получения ваших изображений:

var query =
    from a in addr.ToObservable().Select((url, i) => new { url, i })
    from dl in Observable
        .Using(
            () => new System.Net.WebClient(),
            wc => Observable.FromAsync(() => wc.DownloadDataTaskAsync(a.url)))
    from bitmap in Observable
        .Using(
            () => new System.IO.MemoryStream(dl),
            ms => Observable.Start(() => new Bitmap(ms)))
        .Catch(ex => Observable.Return(new Bitmap(Properties.Resources.defaultFavIcon)))
    select new { x = (a.i % 6) * 16, y = (a.i / 6) * 16, bitmap };

Наконец, дождитесь поступления всех изображений, затем в потоке пользовательского интерфейса создайте составное изображение и назначьте его элементу управления passAddDisplay.

query
    .ToArray()
    .ObserveOn(passAddDisplay)
    .Subscribe(images =>
    {
        var favCollection = new Bitmap(96, 64);
        using(var g = Graphics.FromImage(favCollection))
        {
            foreach (var image in images)
            {
                g.DrawImage(image.bitmap, image.x, image.y, 16, 16);
                image.bitmap.Dispose();
            }
        }
        passAddDisplay.Image = favCollection;
    });

Я проверил запрос, и он отлично работает.

person Enigmativity    schedule 08.04.2015